Litefs 邮件发送
更新日期: 2026-10-21 适用版本: v0.9.0(开发中)
概述
Litefs 内置邮件发送模块,基于 Python 标准库 smtplib 与 email.message.EmailMessage 实现,
零新增第三方依赖,提供:
消息模型:
Message/Attachment,与传输层解耦SMTP 客户端:
SMTPClient,支持 STARTTLS / 隐式 SSL、超时控制、失败重试与指数退避模板邮件:基于已有的 Mako 引擎,支持纯文本 + HTML 双模板配对
异步发送:优先投递 Celery 队列,Celery 不可用时自动降级为同步发送
设计原则:
原则 |
实现方式 |
|---|---|
零新增依赖 |
仅使用 |
传输解耦 |
消息模型不感知 SMTP,Celery 投递通过 |
优雅降级 |
Celery 未初始化或队列不可用时自动回退同步发送,业务代码无需分支判断 |
编码正确 |
非 ASCII 主题由标准库按 RFC 2047 编码,正文按 RFC 2046 自动选择编码 |
配置驱动 |
SMTP 参数统一来自应用配置,运行时快照化为 |
快速开始
最小示例
from litefs.core import Litefs
from litefs.mail import Message
app = Litefs(
mail_host='smtp.example.com',
mail_port=587,
mail_use_tls=True,
mail_username='noreply@example.com',
mail_password='secret',
mail_default_from='noreply@example.com',
)
message = Message(
subject='注册验证',
body='您的验证码是 123456',
to=['user@example.com'],
)
result = app.send_mail(message)
print(result.success, result.error)
应用级发送入口
Litefs.send_mail() 是应用级封装,自动从应用配置读取 SMTP 参数:
# 同步发送(阻塞当前请求线程)
result = app.send_mail(message)
# 异步发送(投递到 Celery 队列)
result = app.send_mail(message, asynchronous=True)
print(result.dispatched) # True 表示已进入队列
print(result.task_id) # Celery 任务 ID
返回语义:
MailSendResult.success表示「投递成功或同步发送成功」;dispatched区分两者——True表示已投递到 Celery 队列(实际发送在 worker 侧完成),False表示已同步发送完成。error在失败时给出可读的原因。
独立使用(不依赖应用实例)
from litefs.mail import SMTPClient, get_mail_config, send_mail
from litefs.core import Litefs
config = get_mail_config(Litefs()) # 从应用配置读取
with SMTPClient(config) as client:
client.send(message)
MIME 结构规则
框架根据传入内容自动选择 MIME 结构,无需手工拼装:
内容 |
MIME 结构 |
|---|---|
仅 |
|
|
|
含附件 |
外层 |
为什么纯文本在前:不支持 HTML 的邮件客户端会展示
multipart/alternative中的第一个部分, 因此纯文本必须排在前面,否则部分客户端只能看到 HTML 源码。
附件
Attachment 的内容来源二选一,同时提供会抛出 ValueError:
from litefs.mail import Attachment, Message
message = Message(
subject='月度报表',
body='报表已生成,详见附件。',
to=['user@example.com'],
from_addr='noreply@example.com',
)
# 来源一:磁盘文件
message.add_attachment(Attachment(filename='report.pdf', path='/tmp/report.pdf'))
# 来源二:内存内容
message.add_attachment(
Attachment(filename='data.csv', content='a,b\n1,2\n', mimetype='text/csv')
)
参数 |
说明 |
|---|---|
|
附件文件名(写入 |
|
内存内容,支持 |
|
磁盘文件路径,发送时读取 |
|
MIME 类型,缺省时按文件名推断,推断失败回退 |
|
文本附件字符集,默认 |
Celery 投递注意:
Attachment.to_dict()对内存内容做 base64 编码以保证 JSON 可序列化;path方式在 worker 侧读取文件,因此附件路径必须是 worker 可见的共享路径(如 NFS、对象存储挂载)。
模板邮件
邮件模板复用框架已有的 Mako 引擎,模板目录由 mail_template_dir 指定(默认 templates/email)。
templates/email/
├── welcome.txt # 纯文本正文
└── welcome.html # HTML 正文
from litefs.mail import build_mail_body, Message
text, html = build_mail_body(
'welcome', # 模板基础名(不带扩展名)
'templates/email',
username='张三',
code='123456',
)
message = Message(
subject='欢迎注册',
body=text,
html_body=html,
to=['user@example.com'],
)
配对规则:
存在的模板 |
结果 |
|---|---|
仅 |
|
仅 |
|
两者都有 |
|
都没有 |
|
也可以直接调用 render_mail_template(),缺失的模板对应项返回 None 而不抛异常。
模板查找器按「目录 + debug 标志」缓存,调试模式下自动关闭缓存,保证模板改动立即生效。
Celery 异步发送
from litefs.mail import send_mail_async, Message
message = Message(
subject='订单通知',
body='您的订单已发货。',
to=['user@example.com'],
)
result = send_mail_async(message)
异步链路:
send_mail_async
├─ Celery 实例存在 → 注册任务 litefs.mail.send → task.delay() → 队列 mail
│ └─ worker:Message.from_dict() → MailConfig.from_dict() → SMTP 投递
└─ Celery 未安装/未初始化 → 降级 send_mail()(dispatched=False)
要点:
项目 |
说明 |
|---|---|
任务名 |
|
队列 |
|
任务注册 |
延迟注册,首次调用时按 |
失败处理 |
worker 内发送失败抛出 |
启动 worker:
celery -A your_app worker -Q mail -c 4
配置项
所有配置项均已内置默认值,可通过构造参数、config.yaml 或环境变量提供。
配置项 |
默认值 |
说明 |
|---|---|---|
|
|
SMTP 服务器地址 |
|
|
SMTP 服务器端口 |
|
|
认证用户名,None 表示不认证 |
|
|
认证密码 |
|
|
是否在连接后升级为 STARTTLS |
|
|
是否使用隐式 SSL(465 端口) |
|
|
连接与发送超时(秒) |
|
|
默认发件人地址,邮件未指定 |
|
|
邮件模板目录 |
|
|
发送失败的最大重试次数 |
config.yaml 示例:
mail_host: smtp.example.com
mail_port: 587
mail_use_tls: true
mail_username: noreply@example.com
mail_password: ${MAIL_PASSWORD}
mail_default_from: noreply@example.com
mail_template_dir: templates/email
mail_max_retries: 3
TLS 与 SSL 的选择
场景 |
配置 |
|---|---|
明文(仅内网) |
|
STARTTLS(主流) |
|
隐式 SSL |
|
SMTPClient.connect()在use_ssl为真时不会再执行 STARTTLS(两个开关同时开启会与smtplib的实现冲突)。
重试与超时
SMTPClient.send() 内置失败重试,退避时间为 min(2 ** attempt, 30) 秒:
第 1 次失败 → 等待 1s → 第 2 次失败 → 等待 2s → 第 3 次失败 → 等待 4s → 等待 8s …
全部重试失败后抛出 MailSendError,消息形如:
邮件发送失败(已重试 3 次): <最后一次异常>
超时是硬边界:
mail_timeout同时作用于建连与send_message,避免因 SMTP 服务器无响应 而长期占用请求线程。在高并发场景下,建议把发送动作交给 Celery,避免阻塞工作线程。
API 参考
litefs.mail.Message
参数 |
类型 |
说明 |
|---|---|---|
|
|
邮件主题 |
|
|
纯文本正文 |
|
|
收件人、抄送、密送,自动去空白 |
|
|
发件人地址 |
|
|
回复地址 |
|
|
HTML 正文 |
|
|
附件列表 |
|
|
附加邮件头 |
主要方法:
方法 |
说明 |
|---|---|
|
追加附件,返回自身以支持链式调用 |
|
返回 To + Cc + Bcc 的信封收件人列表 |
|
JSON 可序列化的字典互转(Celery 投递使用) |
litefs.mail.SMTPClient
方法 |
说明 |
|---|---|
|
建立 SMTP 连接,返回 |
|
发送邮件,失败自动重试;全部失败抛 |
|
关闭连接,异常静默处理 |
|
上下文管理器支持 |
其他
名称 |
说明 |
|---|---|
|
从应用实例 / Config 对象构建 |
|
|
|
邮件发送失败异常 |
|
发送结果( |
最佳实践
发送动作不要阻塞请求线程。注册验证、订单通知等场景使用
asynchronous=True投递到 Celery, 把 SMTP 的网络延迟从请求关键路径上摘除。密码不要硬编码。通过环境变量或密钥管理服务注入
mail_password。生产环境务必启用加密。公网 SMTP 至少使用 STARTTLS(587),回源内网建议隐式 SSL(465)。
附件走共享存储。Celery worker 与 Web 进程不在同一台机器时,
path附件需放在共享挂载上。用
Content-Language做 CDN 缓存区分需要与国际化模块配合,参见 :doc:i18n。
相关文档
:doc:
i18n—— 国际化与请求级 locale 协商:doc:
celery-integration—— Celery 任务队列集成:doc:
configuration—— 配置项完整列表