fix(email): support implicit SSL (port 465) for SMTP test; add use_ssl field + encryption selector
This commit is contained in:
1 parent
2eeaf1d293
commit
7c71d193fc
3 files changed
+73
-17
No files matched your search
@@ -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:
|
||||
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["From"] = config.from_address
|
||||
msg["To"] = recipient
|
||||
msg["Subject"] = subject
|
||||
msg.attach(MIMEText(body, "plain"))
|
||||
|
||||
if config.use_tls:
|
||||
server = smtplib.SMTP(config.smtp_host, config.smtp_port, timeout=10)
|
||||
server.starttls()
|
||||
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=10)
|
||||
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"}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in new issue
Block a user