邮件(mailer)
2026/6/1大约 6 分钟
邮件(mailer)
系统提供完整的邮件发送与管理能力,支持 SMTP 真实发送和 Stub 测试模式,集成异步队列、数据库模板管理、发送日志等功能。
功能概览
| 模块 | 功能 | 说明 |
|---|---|---|
| 邮件发送 | 纯文本/HTML | 支持 plain + HTML 多部分邮件 |
| 模板邮件 | Tera 模板引擎 | 从数据库读取模板,变量渲染 |
| 发送队列 | Redis 异步队列 | 基于 Sidekiq 风格 Worker 的异步发送 |
| 发送日志 | 全生命周期追踪 | pending → success/failed 状态流转 |
| 邮件模板 | 数据库管理 | 从数据库读取模板,可视化增删改查,启用/禁用 |
| 配置查看 | 脱敏展示 | 从配置文件读取 SMTP 配置信息,脱敏后展示 |
| Stub 模式 | 测试模式 | 不真正发送,用于开发调试 |
数据库表结构
sys_mail_log(邮件日志)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | i64 (Snowflake) | 主键 |
| recipient | String(256) | 收件人邮箱 |
| subject | String(256) | 邮件主题 |
| content_text | String(2048) | 纯文本内容 |
| content_html | Text | HTML 内容 |
| from_addr | String(256) | 发件人 |
| reply_to | String(256) | 回复地址 |
| status | String(32) | 状态:pending / success / failed |
| error_message | String(1024) | 错误信息 |
| mail_type | String(32) | 类型:plain / template |
| created_by | i64 | 创建人 ID |
| created_at | DateTime | 创建时间 |
状态流转:
pending ──→ success (发送成功)
│
└──→ failed (发送失败,记录 error_message)sys_mail_template(邮件模板)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | i64 (Snowflake) | 主键 |
| name | String(128) | 模板名称 |
| code | String(64) (Unique) | 模板编码(用于 API 调用) |
| subject | String(256) | 邮件主题(支持 Tera 变量) |
| text_content | Text | 纯文本模板内容 |
| html_content | Text | HTML 模板内容 |
| description | String(512) | 模板描述 |
| status | i32 | 状态:0=禁用 1=启用 |
API 接口
| 路径 | 方法 | 说明 |
|---|---|---|
/mailer/send | POST | 发送纯文本/HTML 邮件 |
/mailer/send_template | POST | 发送模板邮件 |
/mailer/config | GET | 获取邮件配置(脱敏) |
/mailer/log_list | GET | 获取邮件日志列表 |
/mailer/template/list | GET | 获取邮件模板列表 |
/mailer/template/add | POST | 新增邮件模板 |
/mailer/template/edit | PUT | 编辑邮件模板 |
/mailer/template/del | DELETE | 删除邮件模板 |
发送邮件请求参数
// POST /mailer/send
{
"to": "user@example.com", // 收件人(必填,邮箱格式校验)
"subject": "邮件主题", // 主题(必填)
"text": "纯文本内容", // 纯文本(必填)
"html": "<p>HTML内容</p>", // HTML内容(可选)
"from": "显示名称", // 发件人显示名(可选,默认使用配置)
"reply_to": "reply@example.com" // 回复地址(可选,邮箱格式校验)
}发送模板邮件请求参数
// POST /mailer/send_template
{
"to": "user@example.com", // 收件人(必填)
"template_code": "welcome", // 模板编码(必填)
"from": "显示名称", // 发件人显示名(可选)
"reply_to": "reply@example.com", // 回复地址(可选)
"locals": { // 模板变量(JSON)
"name": "张三",
"code": "123456"
}
}邮件发送架构
纯文本/HTML 邮件
前端请求
│
▼
┌──────────────────────────┐
│ Service 层 │
│ s_sys_mailer::send_email │
│ │
│ 1. 记录 pending 日志 │
│ 2. 构造 Email 对象 │
│ 3. enqueue_sync 入队 │
└──────────┬───────────────┘
│ Redis BRPOP
▼
┌──────────────────────────┐
│ MailerWorker │
│ (mailer 队列消费者) │
│ │
│ 1. EmailSender 初始化 │
│ ├─ stub=true → Stub │
│ └─ stub=false → SMTP │
│ 2. 构造 lettre Message │
│ 3. 发送邮件 │
│ 4. 更新日志状态 │
│ ├─ success │
│ └─ failed + 错误信息 │
└──────────────────────────┘模板邮件
前端请求
│
▼
┌──────────────────────────────────┐
│ Service 层 │
│ s_sys_mailer::send_template_email│
│ │
│ 1. 从数据库读取模板 │
│ render_from_db(template_code) │
│ ├─ 查询 sys_mail_template │
│ ├─ 检查模板状态(是否启用) │
│ └─ Tera 渲染模板变量 │
│ 2. 记录 pending 日志 │
│ 3. 构造 Email 对象 │
│ 4. enqueue_sync 入队 │
└──────────┬───────────────────────┘
│ Redis BRPOP
▼
┌──────────────────────────┐
│ MailerWorker(同上) │
└──────────────────────────┘SMTP 配置
在 config/development.yaml 中配置:
# Mailer Configuration
mailer:
stub: true # true=测试模式(不真正发送),false=SMTP发送
smtp:
enable: true
host: smtp.163.com
port: 465
secure: true # true=TLS, false=明文
from: "Rust祺洛系统 <noreply@163.com>" # 默认发件人(显示名 + 邮箱)
auth:
user: your_email@163.com # SMTP认证用户名
password: your_password # SMTP认证密码/授权码发件人处理逻辑:
from字段支持显示名 + 邮箱格式(如"Rust祺洛系统 <noreply@163.com>"),也支持仅显示名(如"Rust祺洛系统")- 实际 SMTP 信封发件地址强制使用认证用户邮箱(auth.user)
- 前端传入的
from也仅作为显示名称 - 这是因为 163、QQ、Gmail 等主流 SMTP 服务器均拒绝使用非认证邮箱作为发件人
邮件模板引擎
系统使用 [Tera] 模板引擎,模板统一从数据库读取(sys_mail_template 表),通过 render_from_db 函数渲染。
数据库模板(主要方式)
通过 sys_mail_template 表管理,使用 code 字段标识:
| 字段 | 说明 |
|---|---|
| name | 模板名称 |
| code | 模板编码(唯一标识,如 welcome) |
| subject | 主题模板(支持 Tera 变量) |
| text_content | 纯文本模板(可选) |
| html_content | HTML 模板(可选) |
| description | 模板描述 |
| status | 状态:1=启用 0=禁用 |
模板语法(Jinja2 风格):
{# subject #}
欢迎注册 {{ name }}
{# text_content #}
尊敬的 {{ name }},您的验证码是:{{ code }}
{# html_content #}
<h1>欢迎注册</h1>
<p>{{ name }},您的验证码是:<strong>{{ code }}</strong></p>示例数据:
INSERT INTO sys_mail_template (name, code, subject, text_content, html_content, status)
VALUES ('欢迎邮件', 'welcome', '欢迎注册 {{ name }}', '验证码:{{ code }}', '<p>验证码:{{ code }}</p>', 1);调用方式:
POST /mailer/send_template
{
"to": "user@example.com",
"template_code": "welcome",
"locals": { "name": "张三", "code": "123456" }
}渲染流程:
- 根据
template_code查询sys_mail_template表 - 检查模板状态(
status != 1则返回"已禁用"错误) - 使用 Tera 引擎将
locals变量渲染到 subject / text_content / html_content - 返回渲染后的
Content { subject, text, html }
文件模板(已弃用)
⚠️ 文件模板方式(
src/data/mailers/目录)已移除,不再支持。所有模板统一通过数据库管理,支持在线增删改查和启用/禁用。
异步队列与 Worker
邮件发送基于 Redis 异步队列实现,核心组件:
| 组件 | 说明 |
|---|---|
MailerWorker | 邮件队列消费者,处理 mailer 队列 |
Processor | Worker 调度器,BRPOP 轮询队列 |
UnitOfWork | 任务单元,管理任务生命周期 |
Scheduled | 定时/重试调度器 |
EmailSender | 邮件发送器,封装 SMTP/Stub 传输 |
队列配置:
workers:
queues:
- default
- mailer # 邮件专用队列
num_workers: 2 # Worker 并发数失败重试机制:
- 指数退避:5s → 10s → 20s → 40s → ... → 最大 3600s
- 超过最大重试次数后不再重试,记录错误日志
代码示例
发送邮件(入队)
use crate::worker::mailer::{Email, MailerWorker};
let email = Email {
from: None,
to: "user@example.com".into(),
reply_to: None,
subject: "Hello".into(),
text: "纯文本内容".into(),
html: "<b>HTML 内容</b>".into(),
};
let _ = MailerWorker::enqueue_async(email).await;使用模板发送邮件
use crate::worker::mailer::{Args, mail_template};
let _ = mail_template(
"welcome".into(),
Args {
from: None,
to: "user@example.com".into(),
reply_to: None,
locals: serde_json::json!({ "username": "Alice", "link": "https://example.com" }),
}
).await;前端页面
| 页面路径 | 功能 |
|---|---|
/Monitor/Email | 邮件管理(配置/发送/模板/日志) |
/Test/Email | 邮件测试(简化版发送页面) |
邮件管理页面包含 4 个 Tab:
- 配置信息 — 展示 SMTP 配置(脱敏)、Stub 模式状态
- 发送邮件 — 纯文本/HTML 邮件发送表单
- 模板邮件 — 选择模板 + 填写变量发送
- 发送日志 — 邮件日志列表,支持按收件人/主题/状态筛选
技术要点
雪花 ID
邮件模块的所有表均使用雪花 ID 替代自增 ID:
- 分布式唯一:多实例部署不会 ID 冲突
- 不可猜测:避免通过 ID 遍历数据的安全风险
- 时间排序:ID 本身包含时间信息,天然有序
SMTP 发件人安全策略
为避免主流邮件服务商(163、QQ、Gmail 等)拒绝非认证发件人地址,系统实现以下策略:
- 配置中的
from字段支持显示名 + 邮箱格式(如"Rust祺洛 <noreply@163.com>"),也支持仅显示名 - 实际 SMTP 信封发件人强制使用 auth.user
- 前端传入的
from也仅作为显示名称,邮箱地址会被替换 - 若未配置 auth.user,则要求
from为完整邮箱格式