2025-12-30 17:15:48 +08:00
|
|
|
|
"""
|
|
|
|
|
|
邮件发送服务
|
|
|
|
|
|
提供 SMTP 邮件发送功能
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import smtplib
|
2026-01-01 02:10:19 +08:00
|
|
|
|
import ssl
|
2025-12-30 17:15:48 +08:00
|
|
|
|
from email.mime.multipart import MIMEMultipart
|
|
|
|
|
|
from email.mime.text import MIMEText
|
|
|
|
|
|
from typing import Optional, Tuple
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
import aiosmtplib
|
|
|
|
|
|
|
|
|
|
|
|
AIOSMTPLIB_AVAILABLE = True
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
AIOSMTPLIB_AVAILABLE = False
|
|
|
|
|
|
aiosmtplib = None
|
|
|
|
|
|
|
2026-01-01 02:10:19 +08:00
|
|
|
|
|
|
|
|
|
|
def _create_ssl_context() -> ssl.SSLContext:
|
|
|
|
|
|
"""创建 SSL 上下文,使用 certifi 证书或系统默认证书"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
import certifi
|
|
|
|
|
|
|
|
|
|
|
|
context = ssl.create_default_context(cafile=certifi.where())
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
context = ssl.create_default_context()
|
|
|
|
|
|
return context
|
|
|
|
|
|
|
2025-12-30 17:15:48 +08:00
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
|
2026-01-01 02:10:19 +08:00
|
|
|
|
from src.core.crypto import crypto_service
|
2025-12-30 17:15:48 +08:00
|
|
|
|
from src.core.logger import logger
|
2026-01-01 02:10:19 +08:00
|
|
|
|
from src.services.system.config import SystemConfigService
|
2025-12-30 17:15:48 +08:00
|
|
|
|
|
|
|
|
|
|
from .email_template import EmailTemplate
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EmailSenderService:
|
|
|
|
|
|
"""邮件发送服务"""
|
|
|
|
|
|
|
2026-01-01 02:10:19 +08:00
|
|
|
|
# SMTP 超时配置(秒)
|
|
|
|
|
|
SMTP_TIMEOUT = 30
|
|
|
|
|
|
|
2025-12-30 17:15:48 +08:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _get_smtp_config(db: Session) -> dict:
|
|
|
|
|
|
"""
|
|
|
|
|
|
从数据库获取 SMTP 配置
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
db: 数据库会话
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
SMTP 配置字典
|
|
|
|
|
|
"""
|
2026-01-01 02:10:19 +08:00
|
|
|
|
# 获取加密的密码并解密
|
|
|
|
|
|
encrypted_password = SystemConfigService.get_config(db, "smtp_password")
|
|
|
|
|
|
smtp_password = None
|
|
|
|
|
|
if encrypted_password:
|
|
|
|
|
|
try:
|
|
|
|
|
|
smtp_password = crypto_service.decrypt(encrypted_password, silent=True)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
# 解密失败,可能是旧的未加密密码,直接使用
|
|
|
|
|
|
smtp_password = encrypted_password
|
|
|
|
|
|
|
2025-12-30 17:15:48 +08:00
|
|
|
|
config = {
|
2026-01-01 02:10:19 +08:00
|
|
|
|
"smtp_host": SystemConfigService.get_config(db, "smtp_host"),
|
|
|
|
|
|
"smtp_port": SystemConfigService.get_config(db, "smtp_port", default=587),
|
|
|
|
|
|
"smtp_user": SystemConfigService.get_config(db, "smtp_user"),
|
|
|
|
|
|
"smtp_password": smtp_password,
|
|
|
|
|
|
"smtp_use_tls": SystemConfigService.get_config(db, "smtp_use_tls", default=True),
|
|
|
|
|
|
"smtp_use_ssl": SystemConfigService.get_config(db, "smtp_use_ssl", default=False),
|
|
|
|
|
|
"smtp_from_email": SystemConfigService.get_config(db, "smtp_from_email"),
|
|
|
|
|
|
"smtp_from_name": SystemConfigService.get_config(db, "smtp_from_name", default="Aether"),
|
2025-12-30 17:15:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _validate_smtp_config(config: dict) -> Tuple[bool, Optional[str]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
验证 SMTP 配置
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
config: SMTP 配置字典
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
(是否有效, 错误信息)
|
|
|
|
|
|
"""
|
|
|
|
|
|
required_fields = ["smtp_host", "smtp_from_email"]
|
|
|
|
|
|
|
|
|
|
|
|
for field in required_fields:
|
|
|
|
|
|
if not config.get(field):
|
|
|
|
|
|
return False, f"缺少必要的 SMTP 配置: {field}"
|
|
|
|
|
|
|
|
|
|
|
|
return True, None
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
async def send_verification_code(
|
|
|
|
|
|
db: Session, to_email: str, code: str, expire_minutes: int = 30
|
|
|
|
|
|
) -> Tuple[bool, Optional[str]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
发送验证码邮件
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
db: 数据库会话
|
|
|
|
|
|
to_email: 收件人邮箱
|
|
|
|
|
|
code: 验证码
|
|
|
|
|
|
expire_minutes: 过期时间(分钟)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
(是否发送成功, 错误信息)
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 获取 SMTP 配置
|
|
|
|
|
|
config = EmailSenderService._get_smtp_config(db)
|
|
|
|
|
|
|
|
|
|
|
|
# 验证配置
|
|
|
|
|
|
valid, error = EmailSenderService._validate_smtp_config(config)
|
|
|
|
|
|
if not valid:
|
|
|
|
|
|
logger.error(f"SMTP 配置无效: {error}")
|
|
|
|
|
|
return False, error
|
|
|
|
|
|
|
|
|
|
|
|
# 生成邮件内容
|
2026-01-01 02:10:19 +08:00
|
|
|
|
# 优先使用 email_app_name,否则回退到 smtp_from_name
|
|
|
|
|
|
app_name = SystemConfigService.get_config(db, "email_app_name", default=None)
|
|
|
|
|
|
if not app_name:
|
|
|
|
|
|
app_name = SystemConfigService.get_config(db, "smtp_from_name", default="Aether")
|
2025-12-30 17:15:48 +08:00
|
|
|
|
|
|
|
|
|
|
html_body = EmailTemplate.get_verification_code_html(
|
2026-01-01 02:10:19 +08:00
|
|
|
|
code=code, expire_minutes=expire_minutes, db=db, app_name=app_name, email=to_email
|
2025-12-30 17:15:48 +08:00
|
|
|
|
)
|
|
|
|
|
|
text_body = EmailTemplate.get_verification_code_text(
|
2026-01-01 02:10:19 +08:00
|
|
|
|
code=code, expire_minutes=expire_minutes, db=db, app_name=app_name, email=to_email
|
2025-12-30 17:15:48 +08:00
|
|
|
|
)
|
2026-01-01 02:10:19 +08:00
|
|
|
|
subject = EmailTemplate.get_subject("verification", db=db)
|
2025-12-30 17:15:48 +08:00
|
|
|
|
|
|
|
|
|
|
# 发送邮件
|
|
|
|
|
|
return await EmailSenderService._send_email(
|
|
|
|
|
|
config=config, to_email=to_email, subject=subject, html_body=html_body, text_body=text_body
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
async def _send_email(
|
|
|
|
|
|
config: dict,
|
|
|
|
|
|
to_email: str,
|
|
|
|
|
|
subject: str,
|
|
|
|
|
|
html_body: Optional[str] = None,
|
|
|
|
|
|
text_body: Optional[str] = None,
|
|
|
|
|
|
) -> Tuple[bool, Optional[str]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
发送邮件(内部方法)
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
config: SMTP 配置
|
|
|
|
|
|
to_email: 收件人邮箱
|
|
|
|
|
|
subject: 邮件主题
|
|
|
|
|
|
html_body: HTML 邮件内容
|
|
|
|
|
|
text_body: 纯文本邮件内容
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
(是否发送成功, 错误信息)
|
|
|
|
|
|
"""
|
|
|
|
|
|
if AIOSMTPLIB_AVAILABLE:
|
|
|
|
|
|
return await EmailSenderService._send_email_async(
|
|
|
|
|
|
config, to_email, subject, html_body, text_body
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
return await EmailSenderService._send_email_sync_wrapper(
|
|
|
|
|
|
config, to_email, subject, html_body, text_body
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
async def _send_email_async(
|
|
|
|
|
|
config: dict,
|
|
|
|
|
|
to_email: str,
|
|
|
|
|
|
subject: str,
|
|
|
|
|
|
html_body: Optional[str] = None,
|
|
|
|
|
|
text_body: Optional[str] = None,
|
|
|
|
|
|
) -> Tuple[bool, Optional[str]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
异步发送邮件(使用 aiosmtplib)
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
config: SMTP 配置
|
|
|
|
|
|
to_email: 收件人邮箱
|
|
|
|
|
|
subject: 邮件主题
|
|
|
|
|
|
html_body: HTML 邮件内容
|
|
|
|
|
|
text_body: 纯文本邮件内容
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
(是否发送成功, 错误信息)
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 构建邮件
|
|
|
|
|
|
message = MIMEMultipart("alternative")
|
|
|
|
|
|
message["Subject"] = subject
|
|
|
|
|
|
message["From"] = f"{config['smtp_from_name']} <{config['smtp_from_email']}>"
|
|
|
|
|
|
message["To"] = to_email
|
|
|
|
|
|
|
|
|
|
|
|
# 添加纯文本部分
|
|
|
|
|
|
if text_body:
|
|
|
|
|
|
message.attach(MIMEText(text_body, "plain", "utf-8"))
|
|
|
|
|
|
|
|
|
|
|
|
# 添加 HTML 部分
|
|
|
|
|
|
if html_body:
|
|
|
|
|
|
message.attach(MIMEText(html_body, "html", "utf-8"))
|
|
|
|
|
|
|
|
|
|
|
|
# 发送邮件
|
2026-01-01 02:10:19 +08:00
|
|
|
|
ssl_context = _create_ssl_context()
|
2025-12-30 17:15:48 +08:00
|
|
|
|
if config["smtp_use_ssl"]:
|
|
|
|
|
|
await aiosmtplib.send(
|
|
|
|
|
|
message,
|
|
|
|
|
|
hostname=config["smtp_host"],
|
|
|
|
|
|
port=config["smtp_port"],
|
|
|
|
|
|
use_tls=True,
|
2026-01-01 02:10:19 +08:00
|
|
|
|
tls_context=ssl_context,
|
2025-12-30 17:15:48 +08:00
|
|
|
|
username=config["smtp_user"],
|
|
|
|
|
|
password=config["smtp_password"],
|
2026-01-01 02:10:19 +08:00
|
|
|
|
timeout=EmailSenderService.SMTP_TIMEOUT,
|
2025-12-30 17:15:48 +08:00
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
await aiosmtplib.send(
|
|
|
|
|
|
message,
|
|
|
|
|
|
hostname=config["smtp_host"],
|
|
|
|
|
|
port=config["smtp_port"],
|
|
|
|
|
|
start_tls=config["smtp_use_tls"],
|
2026-01-01 02:10:19 +08:00
|
|
|
|
tls_context=ssl_context if config["smtp_use_tls"] else None,
|
2025-12-30 17:15:48 +08:00
|
|
|
|
username=config["smtp_user"],
|
|
|
|
|
|
password=config["smtp_password"],
|
2026-01-01 02:10:19 +08:00
|
|
|
|
timeout=EmailSenderService.SMTP_TIMEOUT,
|
2025-12-30 17:15:48 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"验证码邮件发送成功: {to_email}")
|
|
|
|
|
|
return True, None
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_msg = f"发送邮件失败: {str(e)}"
|
|
|
|
|
|
logger.error(error_msg)
|
|
|
|
|
|
return False, error_msg
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
async def _send_email_sync_wrapper(
|
|
|
|
|
|
config: dict,
|
|
|
|
|
|
to_email: str,
|
|
|
|
|
|
subject: str,
|
|
|
|
|
|
html_body: Optional[str] = None,
|
|
|
|
|
|
text_body: Optional[str] = None,
|
|
|
|
|
|
) -> Tuple[bool, Optional[str]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
同步邮件发送的异步包装器
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
config: SMTP 配置
|
|
|
|
|
|
to_email: 收件人邮箱
|
|
|
|
|
|
subject: 邮件主题
|
|
|
|
|
|
html_body: HTML 邮件内容
|
|
|
|
|
|
text_body: 纯文本邮件内容
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
(是否发送成功, 错误信息)
|
|
|
|
|
|
"""
|
|
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
|
|
return await loop.run_in_executor(
|
|
|
|
|
|
None, EmailSenderService._send_email_sync, config, to_email, subject, html_body, text_body
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _send_email_sync(
|
|
|
|
|
|
config: dict,
|
|
|
|
|
|
to_email: str,
|
|
|
|
|
|
subject: str,
|
|
|
|
|
|
html_body: Optional[str] = None,
|
|
|
|
|
|
text_body: Optional[str] = None,
|
|
|
|
|
|
) -> Tuple[bool, Optional[str]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
同步发送邮件(使用标准库 smtplib)
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
config: SMTP 配置
|
|
|
|
|
|
to_email: 收件人邮箱
|
|
|
|
|
|
subject: 邮件主题
|
|
|
|
|
|
html_body: HTML 邮件内容
|
|
|
|
|
|
text_body: 纯文本邮件内容
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
(是否发送成功, 错误信息)
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 构建邮件
|
|
|
|
|
|
message = MIMEMultipart("alternative")
|
|
|
|
|
|
message["Subject"] = subject
|
|
|
|
|
|
message["From"] = f"{config['smtp_from_name']} <{config['smtp_from_email']}>"
|
|
|
|
|
|
message["To"] = to_email
|
|
|
|
|
|
|
|
|
|
|
|
# 添加纯文本部分
|
|
|
|
|
|
if text_body:
|
|
|
|
|
|
message.attach(MIMEText(text_body, "plain", "utf-8"))
|
|
|
|
|
|
|
|
|
|
|
|
# 添加 HTML 部分
|
|
|
|
|
|
if html_body:
|
|
|
|
|
|
message.attach(MIMEText(html_body, "html", "utf-8"))
|
|
|
|
|
|
|
|
|
|
|
|
# 连接 SMTP 服务器
|
|
|
|
|
|
server = None
|
2026-01-01 02:10:19 +08:00
|
|
|
|
ssl_context = _create_ssl_context()
|
2025-12-30 17:15:48 +08:00
|
|
|
|
try:
|
|
|
|
|
|
if config["smtp_use_ssl"]:
|
2026-01-01 02:10:19 +08:00
|
|
|
|
server = smtplib.SMTP_SSL(
|
|
|
|
|
|
config["smtp_host"],
|
|
|
|
|
|
config["smtp_port"],
|
|
|
|
|
|
context=ssl_context,
|
|
|
|
|
|
timeout=EmailSenderService.SMTP_TIMEOUT,
|
|
|
|
|
|
)
|
2025-12-30 17:15:48 +08:00
|
|
|
|
else:
|
2026-01-01 02:10:19 +08:00
|
|
|
|
server = smtplib.SMTP(
|
|
|
|
|
|
config["smtp_host"],
|
|
|
|
|
|
config["smtp_port"],
|
|
|
|
|
|
timeout=EmailSenderService.SMTP_TIMEOUT,
|
|
|
|
|
|
)
|
2025-12-30 17:15:48 +08:00
|
|
|
|
if config["smtp_use_tls"]:
|
2026-01-01 02:10:19 +08:00
|
|
|
|
server.starttls(context=ssl_context)
|
2025-12-30 17:15:48 +08:00
|
|
|
|
|
|
|
|
|
|
# 登录
|
|
|
|
|
|
if config["smtp_user"] and config["smtp_password"]:
|
|
|
|
|
|
server.login(config["smtp_user"], config["smtp_password"])
|
|
|
|
|
|
|
|
|
|
|
|
# 发送邮件
|
|
|
|
|
|
server.send_message(message)
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"验证码邮件发送成功(同步方式): {to_email}")
|
|
|
|
|
|
return True, None
|
|
|
|
|
|
finally:
|
|
|
|
|
|
# 确保服务器连接被关闭
|
|
|
|
|
|
if server is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
server.quit()
|
|
|
|
|
|
except Exception as quit_error:
|
|
|
|
|
|
logger.warning(f"关闭 SMTP 连接时出错: {quit_error}")
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_msg = f"发送邮件失败: {str(e)}"
|
|
|
|
|
|
logger.error(error_msg)
|
|
|
|
|
|
return False, error_msg
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
async def test_smtp_connection(
|
|
|
|
|
|
db: Session, override_config: Optional[dict] = None
|
|
|
|
|
|
) -> Tuple[bool, Optional[str]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
测试 SMTP 连接
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
db: 数据库会话
|
|
|
|
|
|
override_config: 可选的覆盖配置(通常来自未保存的前端表单)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
(是否连接成功, 错误信息)
|
|
|
|
|
|
"""
|
|
|
|
|
|
config = EmailSenderService._get_smtp_config(db)
|
|
|
|
|
|
|
|
|
|
|
|
# 用外部传入的配置覆盖(仅覆盖提供的字段)
|
|
|
|
|
|
if override_config:
|
|
|
|
|
|
config.update({k: v for k, v in override_config.items() if v is not None})
|
|
|
|
|
|
|
|
|
|
|
|
# 验证配置
|
|
|
|
|
|
valid, error = EmailSenderService._validate_smtp_config(config)
|
|
|
|
|
|
if not valid:
|
|
|
|
|
|
return False, error
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
2026-01-01 02:10:19 +08:00
|
|
|
|
ssl_context = _create_ssl_context()
|
2025-12-30 17:15:48 +08:00
|
|
|
|
if AIOSMTPLIB_AVAILABLE:
|
|
|
|
|
|
# 使用异步方式测试
|
2026-01-01 02:10:19 +08:00
|
|
|
|
# 注意: use_tls=True 表示隐式 SSL (端口 465)
|
|
|
|
|
|
# start_tls=True 表示 STARTTLS (端口 587)
|
|
|
|
|
|
use_ssl = config["smtp_use_ssl"]
|
|
|
|
|
|
use_starttls = config["smtp_use_tls"] and not use_ssl
|
|
|
|
|
|
|
2025-12-30 17:15:48 +08:00
|
|
|
|
smtp = aiosmtplib.SMTP(
|
|
|
|
|
|
hostname=config["smtp_host"],
|
|
|
|
|
|
port=config["smtp_port"],
|
2026-01-01 02:10:19 +08:00
|
|
|
|
use_tls=use_ssl,
|
|
|
|
|
|
start_tls=use_starttls,
|
|
|
|
|
|
tls_context=ssl_context if (use_ssl or use_starttls) else None,
|
|
|
|
|
|
timeout=EmailSenderService.SMTP_TIMEOUT,
|
2025-12-30 17:15:48 +08:00
|
|
|
|
)
|
|
|
|
|
|
await smtp.connect()
|
|
|
|
|
|
|
|
|
|
|
|
if config["smtp_user"] and config["smtp_password"]:
|
|
|
|
|
|
await smtp.login(config["smtp_user"], config["smtp_password"])
|
|
|
|
|
|
|
|
|
|
|
|
await smtp.quit()
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 使用同步方式测试
|
|
|
|
|
|
if config["smtp_use_ssl"]:
|
2026-01-01 02:10:19 +08:00
|
|
|
|
server = smtplib.SMTP_SSL(
|
|
|
|
|
|
config["smtp_host"],
|
|
|
|
|
|
config["smtp_port"],
|
|
|
|
|
|
context=ssl_context,
|
|
|
|
|
|
timeout=EmailSenderService.SMTP_TIMEOUT,
|
|
|
|
|
|
)
|
2025-12-30 17:15:48 +08:00
|
|
|
|
else:
|
2026-01-01 02:10:19 +08:00
|
|
|
|
server = smtplib.SMTP(
|
|
|
|
|
|
config["smtp_host"],
|
|
|
|
|
|
config["smtp_port"],
|
|
|
|
|
|
timeout=EmailSenderService.SMTP_TIMEOUT,
|
|
|
|
|
|
)
|
2025-12-30 17:15:48 +08:00
|
|
|
|
if config["smtp_use_tls"]:
|
2026-01-01 02:10:19 +08:00
|
|
|
|
server.starttls(context=ssl_context)
|
2025-12-30 17:15:48 +08:00
|
|
|
|
|
|
|
|
|
|
if config["smtp_user"] and config["smtp_password"]:
|
|
|
|
|
|
server.login(config["smtp_user"], config["smtp_password"])
|
|
|
|
|
|
|
|
|
|
|
|
server.quit()
|
|
|
|
|
|
|
|
|
|
|
|
logger.info("SMTP 连接测试成功")
|
|
|
|
|
|
return True, None
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-01-01 02:10:19 +08:00
|
|
|
|
error_msg = _translate_smtp_error(str(e))
|
|
|
|
|
|
logger.error(f"SMTP 连接测试失败: {error_msg}")
|
2025-12-30 17:15:48 +08:00
|
|
|
|
return False, error_msg
|
2026-01-01 02:10:19 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _translate_smtp_error(error: str) -> str:
|
|
|
|
|
|
"""将 SMTP 错误信息转换为用户友好的中文提示"""
|
|
|
|
|
|
error_lower = error.lower()
|
|
|
|
|
|
|
|
|
|
|
|
# 认证相关错误
|
|
|
|
|
|
if "username and password not accepted" in error_lower:
|
|
|
|
|
|
return "用户名或密码错误,请检查 SMTP 凭据"
|
|
|
|
|
|
if "authentication failed" in error_lower:
|
|
|
|
|
|
return "认证失败,请检查用户名和密码"
|
|
|
|
|
|
if "invalid credentials" in error_lower or "badcredentials" in error_lower:
|
|
|
|
|
|
return "凭据无效,请检查用户名和密码"
|
|
|
|
|
|
if "smtp auth extension is not supported" in error_lower:
|
|
|
|
|
|
return "服务器不支持认证,请尝试使用 TLS 或 SSL 加密"
|
|
|
|
|
|
|
|
|
|
|
|
# 连接相关错误
|
|
|
|
|
|
if "connection refused" in error_lower:
|
|
|
|
|
|
return "连接被拒绝,请检查服务器地址和端口"
|
|
|
|
|
|
if "connection timed out" in error_lower or "timed out" in error_lower:
|
|
|
|
|
|
return "连接超时,请检查网络或服务器地址"
|
|
|
|
|
|
if "name or service not known" in error_lower or "getaddrinfo failed" in error_lower:
|
|
|
|
|
|
return "无法解析服务器地址,请检查 SMTP 服务器地址"
|
|
|
|
|
|
if "network is unreachable" in error_lower:
|
|
|
|
|
|
return "网络不可达,请检查网络连接"
|
|
|
|
|
|
|
|
|
|
|
|
# SSL/TLS 相关错误
|
|
|
|
|
|
if "certificate verify failed" in error_lower:
|
|
|
|
|
|
return "SSL 证书验证失败,请检查服务器证书或尝试其他加密方式"
|
|
|
|
|
|
if "ssl" in error_lower and "wrong version" in error_lower:
|
|
|
|
|
|
return "SSL 版本不匹配,请尝试其他加密方式"
|
|
|
|
|
|
if "starttls" in error_lower:
|
|
|
|
|
|
return "STARTTLS 握手失败,请检查加密设置"
|
|
|
|
|
|
|
|
|
|
|
|
# 其他常见错误
|
|
|
|
|
|
if "sender address rejected" in error_lower:
|
|
|
|
|
|
return "发件人地址被拒绝,请检查发件人邮箱设置"
|
|
|
|
|
|
if "relay access denied" in error_lower:
|
|
|
|
|
|
return "中继访问被拒绝,请检查 SMTP 服务器配置"
|
|
|
|
|
|
|
|
|
|
|
|
# 返回原始错误(简化格式)
|
|
|
|
|
|
# 去掉错误码前缀,如 "(535, '5.7.8 ..."
|
|
|
|
|
|
if error.startswith("(") and "'" in error:
|
|
|
|
|
|
# 提取引号内的内容
|
|
|
|
|
|
start = error.find("'") + 1
|
|
|
|
|
|
end = error.rfind("'")
|
|
|
|
|
|
if start > 0 and end > start:
|
|
|
|
|
|
return error[start:end].replace("\\n", " ").strip()
|
|
|
|
|
|
|
|
|
|
|
|
return error
|