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
+82 -26

No files matched your search

+41 -14
View File
@@ -9,6 +9,7 @@ from ...auth import get_current_admin
from ...middleware.log_service import LogService
import uuid
import smtplib
import ssl
import hmac
import hashlib
import base64
@@ -37,6 +38,7 @@ class EmailConfigUpdate(BaseModel):
password: Optional[str] = None
from_address: Optional[str] = None
use_tls: Optional[bool] = None
use_ssl: Optional[bool] = None
class SmsConfigUpdate(BaseModel):
@@ -116,6 +118,7 @@ async def get_email_config(
"password": "",
"from_address": "",
"use_tls": True,
"use_ssl": False,
}, "message": "ok"}
return {"code": 0, "data": {
@@ -126,6 +129,7 @@ async def get_email_config(
"password": config.password,
"from_address": config.from_address,
"use_tls": config.use_tls,
"use_ssl": bool(config.use_ssl) if config.use_ssl is not None else False,
}, "message": "ok"}
@@ -178,6 +182,8 @@ async def update_email_config(
config.from_address = req.from_address
if req.use_tls is not None:
config.use_tls = req.use_tls
if req.use_ssl is not None:
config.use_ssl = req.use_ssl
config.updated_at = datetime.now()
db.commit()
@@ -373,29 +379,50 @@ async def test_email_config(
logger.info(f"测试邮件: SMTP={config.smtp_host}:{config.smtp_port}, "
f"from={config.from_address}, to={recipient}")
# 选择加密方式:use_ssl 优先;未显式设置时按端口推断(465=隐式SSL)
try:
msg = MIMEMultipart()
msg["From"] = config.from_address
msg["To"] = recipient
msg["Subject"] = subject
msg.attach(MIMEText(body, "plain"))
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)
if config.use_tls:
server = smtplib.SMTP(config.smtp_host, config.smtp_port, timeout=10)
server.starttls()
else:
server = smtplib.SMTP(config.smtp_host, config.smtp_port, timeout=10)
msg = MIMEMultipart()
msg["From"] = config.from_address
msg["To"] = recipient
msg["Subject"] = subject
msg.attach(MIMEText(body, "plain"))
server.login(config.username, config.password)
server.sendmail(config.from_address, recipient, msg.as_string())
server.quit()
try:
server = None
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:
server = smtplib.SMTP(config.smtp_host, config.smtp_port, timeout=20)
server.login(config.username, config.password)
server.sendmail(config.from_address, recipient, msg.as_string())
finally:
if server is not None:
try:
server.quit()
except Exception:
pass
LogService.log_audit(
admin_id=admin_user.id,
admin_name=admin_user.username,
action="test_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"}
+2
View File
@@ -84,6 +84,7 @@ class Conversation(Base):
status = Column(String(10), nullable=False, default="active") # active / archived
pinned = Column(Boolean, default=False)
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)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
@@ -626,6 +627,7 @@ class EmailConfig(Base):
password = Column(String(200), nullable=False)
from_address = Column(String(200), nullable=False)
use_tls = Column(Boolean, default=True)
use_ssl = Column(Boolean, default=False)
created_at = Column(DateTime, default=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: '',
});
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({
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 || '');
}
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({
smtp_host: emailRes.data.smtp_host || '',
smtp_port: emailRes.data.smtp_port || 587,
smtp_port: port,
username: emailRes.data.username || '',
password: emailRes.data.password || '',
from_address: emailRes.data.from_address || '',
use_tls: emailRes.data.use_tls !== false,
use_tls: useTls,
use_ssl: useSsl,
});
}
if (smsRes?.data) {
@@ -197,6 +204,7 @@ const SiteSettings: React.FC = () => {
password: mail.password,
from_address: mail.from_address,
use_tls: mail.use_tls,
use_ssl: mail.use_ssl,
});
showSaved('邮件配置');
} catch (e: any) {
@@ -247,6 +255,7 @@ const SiteSettings: React.FC = () => {
password: mail.password,
from_address: mail.from_address,
use_tls: mail.use_tls,
use_ssl: mail.use_ssl,
});
const res = await adminAPI.testEmail({
test_to: emailTestTo.trim(),
@@ -267,6 +276,25 @@ const SiteSettings: React.FC = () => {
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 () => {
try {
const res = await adminAPI.testSms();
@@ -484,7 +512,7 @@ const SiteSettings: React.FC = () => {
</FormGroup>
<FormGroup label="端口">
<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>
</FormRow>
@@ -505,14 +533,13 @@ const SiteSettings: React.FC = () => {
onChange={e => setMail({ ...mail, from_address: e.target.value })}
placeholder="noreply@example.com" />
</FormGroup>
<FormGroup label="安全协议">
<label style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-sm)',
padding: '10px 0', cursor: 'pointer', color: 'var(--color-text-secondary)', fontSize: 'var(--text-sm)' }}>
<input type="checkbox" checked={mail.use_tls}
onChange={e => setMail({ ...mail, use_tls: e.target.checked })}
style={{ accentColor: 'var(--color-accent)' }} />
启用 TLS 加密
</label>
<FormGroup label="加密方式">
<select className="admin-form-input" value={encryptionMode}
onChange={e => handleEncryptionChange(e.target.value)}>
<option value="none">无加密(端口 25)</option>
<option value="ssl">SSL 隐式加密(端口 465)</option>
<option value="starttls">STARTTLS(端口 587)</option>
</select>
</FormGroup>
</FormRow>