Skip to content

Latest commit

 

History

History
321 lines (241 loc) · 10.9 KB

mail-service.md

File metadata and controls

321 lines (241 loc) · 10.9 KB

Litefs 邮件发送

更新日期: 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 结构,无需手工拼装:

内容 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 标志」缓存,调试模式下自动关闭缓存,保证模板改动立即生效。

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)

要点:

项目 说明
任务名 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

TLS 与 SSL 的选择

场景 配置
明文(仅内网) 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,避免阻塞工作线程。

API 参考

litefs.mail.Message

参数 类型 说明
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 投递使用)

litefs.mail.SMTPClient

方法 说明
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)

最佳实践

  1. 发送动作不要阻塞请求线程。注册验证、订单通知等场景使用 asynchronous=True 投递到 Celery, 把 SMTP 的网络延迟从请求关键路径上摘除。
  2. 密码不要硬编码。通过环境变量或密钥管理服务注入 mail_password。
  3. 生产环境务必启用加密。公网 SMTP 至少使用 STARTTLS(587),回源内网建议隐式 SSL(465)。
  4. 附件走共享存储。Celery worker 与 Web 进程不在同一台机器时,path 附件需放在共享挂载上。
  5. 用 Content-Language 做 CDN 缓存区分需要与国际化模块配合,参见 :doc:i18n。

相关文档

  • :doc:i18n —— 国际化与请求级 locale 协商
  • :doc:celery-integration —— Celery 任务队列集成
  • :doc:configuration —— 配置项完整列表