fix(email): support implicit SSL (port 465) for SMTP test; add use_ssl field + encryption selector

This commit is contained in:
qyyfw committed 2026-07-10 09:54:49 +08:00
1 parent 2eeaf1d293
commit 7c71d193fc
3 files changed
+73 -17

No files matched your search

+32 -5
View File
@@ -9,6 +9,7 @@ from ...auth import get_current_admin
from ...middleware.log_service import LogService from ...middleware.log_service import LogService
import uuid import uuid
import smtplib import smtplib
import ssl
import hmac import hmac
import hashlib import hashlib
import base64 import base64
@@ -37,6 +38,7 @@ class EmailConfigUpdate(BaseModel):
password: Optional[str] = None password: Optional[str] = None
from_address: Optional[str] = None from_address: Optional[str] = None
use_tls: Optional[bool] = None use_tls: Optional[bool] = None
use_ssl: Optional[bool] = None
class SmsConfigUpdate(BaseModel): class SmsConfigUpdate(BaseModel):
@@ -116,6 +118,7 @@ async def get_email_config(
"password": "", "password": "",
"from_address": "", "from_address": "",
"use_tls": True, "use_tls": True,
"use_ssl": False,
}, "message": "ok"} }, "message": "ok"}
return {"code": 0, "data": { return {"code": 0, "data": {
@@ -126,6 +129,7 @@ async def get_email_config(
"password": config.password, "password": config.password,
"from_address": config.from_address, "from_address": config.from_address,
"use_tls": config.use_tls, "use_tls": config.use_tls,
"use_ssl": bool(config.use_ssl) if config.use_ssl is not None else False,
}, "message": "ok"} }, "message": "ok"}
@@ -178,6 +182,8 @@ async def update_email_config(
config.from_address = req.from_address config.from_address = req.from_address
if req.use_tls is not None: if req.use_tls is not None:
config.use_tls = req.use_tls config.use_tls = req.use_tls
if req.use_ssl is not None:
config.use_ssl = req.use_ssl
config.updated_at = datetime.now() config.updated_at = datetime.now()
db.commit() db.commit()
@@ -373,29 +379,50 @@ async def test_email_config(
logger.info(f"测试邮件: SMTP={config.smtp_host}:{config.smtp_port}, " logger.info(f"测试邮件: SMTP={config.smtp_host}:{config.smtp_port}, "
f"from={config.from_address}, to={recipient}") f"from={config.from_address}, to={recipient}")
# 选择加密方式:use_ssl 优先;未显式设置时按端口推断(465=隐式SSL)
try: try:
use_ssl = bool(config.use_ssl) if config.use_ssl is not None else (config.smtp_port == 465)
except Exception:
use_ssl = (config.smtp_port == 465)
use_tls = bool(config.use_tls)
msg = MIMEMultipart() msg = MIMEMultipart()
msg["From"] = config.from_address msg["From"] = config.from_address
msg["To"] = recipient msg["To"] = recipient
msg["Subject"] = subject msg["Subject"] = subject
msg.attach(MIMEText(body, "plain")) msg.attach(MIMEText(body, "plain"))
if config.use_tls: try:
server = smtplib.SMTP(config.smtp_host, config.smtp_port, timeout=10) server = None
server.starttls() try:
if use_ssl:
# 端口465等:隐式SSL(SMTPS),需 SMTP_SSL 而非 STARTTLS
server = smtplib.SMTP_SSL(
config.smtp_host, config.smtp_port, timeout=20,
context=ssl.create_default_context(),
)
elif use_tls:
# 端口587/25:先明文连接再 STARTTLS 升级
server = smtplib.SMTP(config.smtp_host, config.smtp_port, timeout=20)
server.starttls(context=ssl.create_default_context())
else: else:
server = smtplib.SMTP(config.smtp_host, config.smtp_port, timeout=10) server = smtplib.SMTP(config.smtp_host, config.smtp_port, timeout=20)
server.login(config.username, config.password) server.login(config.username, config.password)
server.sendmail(config.from_address, recipient, msg.as_string()) server.sendmail(config.from_address, recipient, msg.as_string())
finally:
if server is not None:
try:
server.quit() server.quit()
except Exception:
pass
LogService.log_audit( LogService.log_audit(
admin_id=admin_user.id, admin_id=admin_user.id,
admin_name=admin_user.username, admin_name=admin_user.username,
action="test_email_config", action="test_email_config",
target_type="email_config", target_type="email_config",
detail=f"测试邮件发送成功 -> {recipient} via {config.smtp_host}", detail=f"测试邮件发送成功 -> {recipient} via {config.smtp_host}:{config.smtp_port}",
) )
return {"code": 0, "data": {"message": f"测试邮件已发送到 {recipient}", "recipient": recipient}, "message": "ok"} return {"code": 0, "data": {"message": f"测试邮件已发送到 {recipient}", "recipient": recipient}, "message": "ok"}
+2
View File
@@ -84,6 +84,7 @@ class Conversation(Base):
status = Column(String(10), nullable=False, default="active") # active / archived status = Column(String(10), nullable=False, default="active") # active / archived
pinned = Column(Boolean, default=False) pinned = Column(Boolean, default=False)
knowledge_graph_count = Column(Integer, default=0) knowledge_graph_count = Column(Integer, default=0)
knowledge_graph_base_id = Column(String(64), default=None, comment="已生成的知识图谱知识库ID")
created_at = Column(DateTime, default=datetime.now) created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
@@ -626,6 +627,7 @@ class EmailConfig(Base):
password = Column(String(200), nullable=False) password = Column(String(200), nullable=False)
from_address = Column(String(200), nullable=False) from_address = Column(String(200), nullable=False)
use_tls = Column(Boolean, default=True) use_tls = Column(Boolean, default=True)
use_ssl = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.now) created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
+39 -12
View File
@@ -23,7 +23,7 @@ const SiteSettings: React.FC = () => {
logo: '', siteName: '', announcement: '', icpNumber: '', contactPhone: '', logo: '', siteName: '', announcement: '', icpNumber: '', contactPhone: '',
}); });
const [mail, setMail] = useState({ const [mail, setMail] = useState({
smtp_host: '', smtp_port: 587, username: '', password: '', from_address: '', use_tls: true, smtp_host: '', smtp_port: 587, username: '', password: '', from_address: '', use_tls: true, use_ssl: false,
}); });
const [sms, setSms] = useState({ const [sms, setSms] = useState({
provider: '', api_key: '', api_secret: '', sign_name: '', template_code: '', provider: '', api_key: '', api_secret: '', sign_name: '', template_code: '',
@@ -94,13 +94,20 @@ const SiteSettings: React.FC = () => {
setLogoPreview(siteRes.data.logo || siteRes.data.site_logo || ''); setLogoPreview(siteRes.data.logo || siteRes.data.site_logo || '');
} }
if (emailRes?.data) { if (emailRes?.data) {
const port = emailRes.data.smtp_port || 587;
// 根据端口推断加密方式(后端也会兜底:465=SSL,587/25=STARTTLS)
let useSsl = emailRes.data.use_ssl === true;
let useTls = emailRes.data.use_tls !== false;
if (port === 465) { useSsl = true; useTls = false; }
else if (port === 587 || port === 25) { if (!useTls) useTls = true; }
setMail({ setMail({
smtp_host: emailRes.data.smtp_host || '', smtp_host: emailRes.data.smtp_host || '',
smtp_port: emailRes.data.smtp_port || 587, smtp_port: port,
username: emailRes.data.username || '', username: emailRes.data.username || '',
password: emailRes.data.password || '', password: emailRes.data.password || '',
from_address: emailRes.data.from_address || '', from_address: emailRes.data.from_address || '',
use_tls: emailRes.data.use_tls !== false, use_tls: useTls,
use_ssl: useSsl,
}); });
} }
if (smsRes?.data) { if (smsRes?.data) {
@@ -197,6 +204,7 @@ const SiteSettings: React.FC = () => {
password: mail.password, password: mail.password,
from_address: mail.from_address, from_address: mail.from_address,
use_tls: mail.use_tls, use_tls: mail.use_tls,
use_ssl: mail.use_ssl,
}); });
showSaved('邮件配置'); showSaved('邮件配置');
} catch (e: any) { } catch (e: any) {
@@ -247,6 +255,7 @@ const SiteSettings: React.FC = () => {
password: mail.password, password: mail.password,
from_address: mail.from_address, from_address: mail.from_address,
use_tls: mail.use_tls, use_tls: mail.use_tls,
use_ssl: mail.use_ssl,
}); });
const res = await adminAPI.testEmail({ const res = await adminAPI.testEmail({
test_to: emailTestTo.trim(), test_to: emailTestTo.trim(),
@@ -267,6 +276,25 @@ const SiteSettings: React.FC = () => {
setShowEmailTest(true); setShowEmailTest(true);
}; };
// 端口变化自动推荐加密方式
const handlePortChange = (port: number) => {
let next: typeof mail = { ...mail, smtp_port: port };
if (port === 465) {
next = { ...next, use_ssl: true, use_tls: false };
} else if (port === 587 || port === 25) {
next = { ...next, use_ssl: false, use_tls: true };
}
setMail(next);
};
// 当前加密方式(用于下拉联动)
const encryptionMode = mail.use_ssl ? 'ssl' : (mail.use_tls ? 'starttls' : 'none');
const handleEncryptionChange = (mode: string) => {
if (mode === 'ssl') setMail({ ...mail, use_ssl: true, use_tls: false });
else if (mode === 'starttls') setMail({ ...mail, use_ssl: false, use_tls: true });
else setMail({ ...mail, use_ssl: false, use_tls: false });
};
const sendTestSms = async () => { const sendTestSms = async () => {
try { try {
const res = await adminAPI.testSms(); const res = await adminAPI.testSms();
@@ -484,7 +512,7 @@ const SiteSettings: React.FC = () => {
</FormGroup> </FormGroup>
<FormGroup label="端口"> <FormGroup label="端口">
<input className="admin-form-input" type="number" value={mail.smtp_port} <input className="admin-form-input" type="number" value={mail.smtp_port}
onChange={e => setMail({ ...mail, smtp_port: Number(e.target.value) })} /> onChange={e => handlePortChange(Number(e.target.value))} />
</FormGroup> </FormGroup>
</FormRow> </FormRow>
@@ -505,14 +533,13 @@ const SiteSettings: React.FC = () => {
onChange={e => setMail({ ...mail, from_address: e.target.value })} onChange={e => setMail({ ...mail, from_address: e.target.value })}
placeholder="noreply@example.com" /> placeholder="noreply@example.com" />
</FormGroup> </FormGroup>
<FormGroup label="安全协议"> <FormGroup label="加密方式">
<label style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-sm)', <select className="admin-form-input" value={encryptionMode}
padding: '10px 0', cursor: 'pointer', color: 'var(--color-text-secondary)', fontSize: 'var(--text-sm)' }}> onChange={e => handleEncryptionChange(e.target.value)}>
<input type="checkbox" checked={mail.use_tls} <option value="none">无加密(端口 25)</option>
onChange={e => setMail({ ...mail, use_tls: e.target.checked })} <option value="ssl">SSL 隐式加密(端口 465)</option>
style={{ accentColor: 'var(--color-accent)' }} /> <option value="starttls">STARTTLS(端口 587)</option>
启用 TLS 加密 </select>
</label>
</FormGroup> </FormGroup>
</FormRow> </FormRow>