更新日期: 2026-10-21 适用版本: v0.9.0(开发中)
Litefs 内置邮件发送模块,基于 Python 标准库 smtplib 与 email.message.EmailMessage 实现,
零新增第三方依赖,提供:
- 消息模型:
Message/Attachment,与传输层解耦 - SMTP 客户端:
SMTPClient,支持 STARTTLS / 隐式 SSL、超时控制、失败重试与指数退避 - 模板邮件:基于已有的 Mako 引擎,支持纯文本 + HTML 双模板配对
- 异步发送:优先投递 Celery 队列,Celery 不可用时自动降级为同步发送
设计原则:
| 原则 | 实现方式 |
|---|---|
| 零新增依赖 | 仅使用 smtplib / email / gettext 等标准库,Mako 与 Celery 复用已有依赖 |
| 传输解耦 | 消息模型不感知 SMTP,Celery 投递通过 to_dict() / from_dict() 完成 JSON 序列化 |
| 优雅降级 | Celery 未初始化或队列不可用时自动回退同步发送,业务代码无需分支判断 |
| 编码正确 | 非 ASCII 主题由标准库按 RFC 2047 编码,正文按 RFC 2046 自动选择编码 |
| 配置驱动 | SMTP 参数统一来自应用配置,运行时快照化为 MailConfig,避免隐式全局状态 |
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 结构 |
|---|---|
仅 body |
text/plain |
body + html_body |
multipart/alternative(纯文本在前,HTML 在后) |
| 含附件 | 外层 multipart/mixed,内含上述结构 |
为什么纯文本在前:不支持 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')
)| 参数 | 说明 |
|---|---|
filename |
附件文件名(写入 Content-Disposition) |
content |
内存内容,支持 bytes / bytearray / str(str 按 charset 编码) |
path |
磁盘文件路径,发送时读取 |
mimetype |
MIME 类型,缺省时按文件名推断,推断失败回退 application/octet-stream |
charset |
文本附件字符集,默认 utf-8 |
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'],
)配对规则:
| 存在的模板 | 结果 |
|---|---|
仅 .txt |
text 有值,html 为 None → 纯文本邮件 |
仅 .html |
html 有值,text 为 "" → HTML 邮件 |
| 两者都有 | text 与 html 均有值 → multipart/alternative 邮件 |
| 都没有 | build_mail_body() 抛出 FileNotFoundError |
也可以直接调用 render_mail_template(),缺失的模板对应项返回 None 而不抛异常。
模板查找器按「目录 + debug 标志」缓存,调试模式下自动关闭缓存,保证模板改动立即生效。
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)
要点:
| 项目 | 说明 |
|---|---|
| 任务名 | litefs.mail.send(常量 SEND_MAIL_TASK_NAME) |
| 队列 | mail(可在 worker 侧单独扩容与限流) |
| 任务注册 | 延迟注册,首次调用时按 id(celery) 缓存,避免模块导入时序问题 |
| 失败处理 | worker 内发送失败抛出 MailSendError,交由 Celery 重试策略处理 |
启动 worker:
celery -A your_app worker -Q mail -c 4所有配置项均已内置默认值,可通过构造参数、config.yaml 或环境变量提供。
| 配置项 | 默认值 | 说明 |
|---|---|---|
mail_host |
localhost |
SMTP 服务器地址 |
mail_port |
25 |
SMTP 服务器端口 |
mail_username |
None |
认证用户名,None 表示不认证 |
mail_password |
None |
认证密码 |
mail_use_tls |
True |
是否在连接后升级为 STARTTLS |
mail_use_ssl |
False |
是否使用隐式 SSL(465 端口) |
mail_timeout |
30 |
连接与发送超时(秒) |
mail_default_from |
None |
默认发件人地址,邮件未指定 from_addr 时使用 |
mail_template_dir |
templates/email |
邮件模板目录 |
mail_max_retries |
3 |
发送失败的最大重试次数 |
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| 场景 | 配置 |
|---|---|
| 明文(仅内网) | mail_use_tls: false、mail_use_ssl: false,端口 25 |
| STARTTLS(主流) | mail_use_tls: true、mail_use_ssl: false,端口 587 |
| 隐式 SSL | mail_use_tls: false、mail_use_ssl: true,端口 465 |
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,避免阻塞工作线程。
| 参数 | 类型 | 说明 |
|---|---|---|
subject |
str |
邮件主题 |
body |
str |
纯文本正文 |
to / cc / bcc |
str | List[str] |
收件人、抄送、密送,自动去空白 |
from_addr |
str |
发件人地址 |
reply_to |
str |
回复地址 |
html_body |
str |
HTML 正文 |
attachments |
List[Attachment] |
附件列表 |
headers |
Dict[str, str] |
附加邮件头 |
主要方法:
| 方法 | 说明 |
|---|---|
add_attachment(attachment) |
追加附件,返回自身以支持链式调用 |
recipients() |
返回 To + Cc + Bcc 的信封收件人列表 |
to_dict() / from_dict(data) |
JSON 可序列化的字典互转(Celery 投递使用) |
| 方法 | 说明 |
|---|---|
connect() |
建立 SMTP 连接,返回 smtplib.SMTP 对象 |
send(message) |
发送邮件,失败自动重试;全部失败抛 MailSendError |
close() |
关闭连接,异常静默处理 |
__enter__ / __exit__ |
上下文管理器支持 |
| 名称 | 说明 |
|---|---|
get_mail_config(source) |
从应用实例 / Config 对象构建 MailConfig 快照 |
build_email_message(message) |
Message → 标准库 EmailMessage 转换 |
MailSendError |
邮件发送失败异常 |
MailSendResult |
发送结果(success / dispatched / task_id / error) |
- 发送动作不要阻塞请求线程。注册验证、订单通知等场景使用
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—— 配置项完整列表