feat: 实现对外邮件投递(外发队列 + MX 直投 + DKIM + 退信 + 管理后台)

- 新增 internal/outbound 模块:MX 查询、SMTP 出站客户端(EHLO/STARTTLS/
  MAIL/RCPT/DATA/QUIT)、4xx 临时失败与 5xx 永久失败分类、8BITMIME 支持
- 新增 outbound_messages 队列表与 OutboundStore,后台 worker 指数退避重试
- 永久失败/超限退信到发件人收件箱,包含原因与目标收件人
- 外发邮件使用域名 DKIM 私钥签名(go-msgauth)
- SMTP 提交集成:认证用户可发外部收件人,MAIL FROM 必须等于登录用户邮箱,
  未认证外部投递明确拒绝(防开放中继)
- Web 发信集成:外部收件人自动入队,附件以 multipart/mixed + base64 编码
  加入邮件正文
- 每用户每分钟/每日发送限速(max_per_day=0 可禁用外部投递)
- 管理后台新增外发队列页面:状态统计、失败原因、手动重试/取消
- 新增 [outbound] 配置段并更新 README / todo.md
This commit is contained in:
dsh
2026-08-15 17:18:54 -04:00
parent 0545a71ba2
commit 5eb9bc2c71
28 changed files with 1657 additions and 186 deletions
+72 -72
View File
@@ -14,60 +14,60 @@
### 安全策略
- [ ] 仅允许已认证用户外发。
- [ ] `MAIL FROM` 必须等于登录用户邮箱。
- [ ] Web 发信的 From 必须等于当前登录用户邮箱。
- [ ] 限制单封邮件最大外部收件人数。
- [ ] 限制单封邮件大小。
- [ ] 明确拒绝未认证外部投递,防止开放中继。
- [x] 仅允许已认证用户外发。
- [x] `MAIL FROM` 必须等于登录用户邮箱。
- [x] Web 发信的 From 必须等于当前登录用户邮箱。
- [x] 限制单封邮件最大外部收件人数。
- [x] 限制单封邮件大小。
- [x] 明确拒绝未认证外部投递,防止开放中继。
### DNS / MX 查询
- [ ] 从外部收件人地址解析目标域名。
- [ ] 使用 `net.LookupMX(domain)` 查询 MX 记录。
- [ ] 按 MX 优先级排序。
- [ ] 如果没有 MX,可按 RFC 规则尝试直接连接域名本身。
- [ ] 记录 MX 查询失败原因。
- [x] 从外部收件人地址解析目标域名。
- [x] 使用 `net.LookupMX(domain)` 查询 MX 记录。
- [x] 按 MX 优先级排序。
- [x] 如果没有 MX,可按 RFC 规则尝试直接连接域名本身。
- [x] 记录 MX 查询失败原因。
### SMTP 出站发送
- [ ] 新增内部 outbound mailer 模块,例如 `internal/outbound/`
- [ ] 实现连接目标 MX 的 `:25` 端口。
- [ ] 发送 `EHLO`
- [ ] 解析对方 SMTP 能力。
- [ ] 如支持 `STARTTLS`,执行 STARTTLS。
- [ ] TLS 成功后重新 `EHLO`
- [ ] 发送 `MAIL FROM`
- [ ] 发送 `RCPT TO`
- [ ] 发送 `DATA`
- [ ] 发送邮件原始内容。
- [ ] 发送 `QUIT`
- [ ] 区分临时失败和永久失败。
- [x] 新增内部 outbound mailer 模块,例如 `internal/outbound/`
- [x] 实现连接目标 MX 的 `:25` 端口。
- [x] 发送 `EHLO`
- [x] 解析对方 SMTP 能力。
- [x] 如支持 `STARTTLS`,执行 STARTTLS。
- [x] TLS 成功后重新 `EHLO`
- [x] 发送 `MAIL FROM`
- [x] 发送 `RCPT TO`
- [x] 发送 `DATA`
- [x] 发送邮件原始内容。
- [x] 发送 `QUIT`
- [x] 区分临时失败和永久失败。
### SMTP 客户端提交集成
- [ ] 修改 `internal/smtp_server/server.go`
- [ ] 对认证用户提交的外部收件人,不再直接拒绝。
- [ ] 本地收件人仍走本地 INBOX 投递。
- [ ] 外部收件人调用 outbound mailer。
- [ ] 外部投递成功后保存 Sent 副本。
- [ ] 外部投递失败时向 SMTP 客户端返回明确错误。
- [x] 修改 `internal/smtp_server/server.go`
- [x] 对认证用户提交的外部收件人,不再直接拒绝。
- [x] 本地收件人仍走本地 INBOX 投递。
- [x] 外部收件人调用 outbound mailer。
- [x] 外部投递成功后保存 Sent 副本。
- [x] 外部投递失败时向 SMTP 客户端返回明确错误。
### Web 发信集成
- [ ] 修改 `internal/web/handlers/mail.go`
- [ ] Web 发信支持外部收件人。
- [ ] 本地收件人走本地投递。
- [ ] 外部收件人调用 outbound mailer。
- [ ] 外部投递失败时页面显示错误。
- [ ] 成功后保存 Sent 副本。
- [x] 修改 `internal/web/handlers/mail.go`
- [x] Web 发信支持外部收件人。
- [x] 本地收件人走本地投递。
- [x] 外部收件人调用 outbound mailer。
- [x] 外部投递失败时页面显示错误。
- [x] 成功后保存 Sent 副本。
### 日志与错误
- [ ] 记录每次外部投递的目标域名、MX、收件人、结果。
- [ ] 记录 SMTP 响应码和响应文本。
- [ ] 临时失败返回可识别错误。
- [ ] 永久失败返回可识别错误。
- [x] 记录每次外部投递的目标域名、MX、收件人、结果。
- [x] 记录 SMTP 响应码和响应文本。
- [x] 临时失败返回可识别错误。
- [x] 永久失败返回可识别错误。
### 验证
@@ -82,61 +82,61 @@
### 出站队列表
- [ ] 新增 outbound queue 数据表。
- [ ] 保存发件人、收件人、RawData、状态、重试次数、下一次重试时间。
- [ ] 保存最后一次 SMTP 响应。
- [ ] 保存创建时间、更新时间、完成时间。
- [x] 新增 outbound queue 数据表。
- [x] 保存发件人、收件人、RawData、状态、重试次数、下一次重试时间。
- [x] 保存最后一次 SMTP 响应。
- [x] 保存创建时间、更新时间、完成时间。
### 后台投递 worker
- [ ] 实现后台 worker 扫描待投递队列。
- [ ] 实现指数退避重试。
- [ ] 临时失败进入重试。
- [ ] 永久失败进入失败状态。
- [ ] 超过最大重试周期后生成失败状态。
- [x] 实现后台 worker 扫描待投递队列。
- [x] 实现指数退避重试。
- [x] 临时失败进入重试。
- [x] 永久失败进入失败状态。
- [x] 超过最大重试周期后生成失败状态。
### 退信
- [ ] 为永久失败生成退信邮件。
- [ ] 为超过重试周期的临时失败生成退信邮件。
- [ ] 将退信投递到发件人 INBOX。
- [ ] 退信中包含原始错误和目标收件人。
- [x] 为永久失败生成退信邮件。
- [x] 为超过重试周期的临时失败生成退信邮件。
- [x] 将退信投递到发件人 INBOX。
- [x] 退信中包含原始错误和目标收件人。
### DKIM 签名
- [ ] 使用域名 DKIM 私钥为外发邮件签名。
- [ ] 添加 `DKIM-Signature` 头。
- [ ] 支持当前域名的 selector。
- [ ] 验证 DNS 中 DKIM TXT 记录匹配。
- [x] 使用域名 DKIM 私钥为外发邮件签名。
- [x] 添加 `DKIM-Signature` 头。
- [x] 支持当前域名的 selector。
- [x] 验证 DNS 中 DKIM TXT 记录匹配。
### 发送限制与防滥用
- [ ] 每用户每分钟发送限制。
- [ ] 每用户每日发送限制。
- [ ] 单封最大收件人数限制。
- [ ] 单封最大大小限制。
- [ ] 记录异常发送行为。
- [x] 每用户每分钟发送限制。
- [x] 每用户每日发送限制。
- [x] 单封最大收件人数限制。
- [x] 单封最大大小限制。
- [x] 记录异常发送行为。
- [ ] 管理员可禁用用户外发能力。
### 管理后台
- [ ] 增加外发队列页面。
- [ ] 显示投递状态。
- [ ] 显示失败原因。
- [ ] 支持手动重试。
- [ ] 支持取消队列任务。
- [x] 增加外发队列页面。
- [x] 显示投递状态。
- [x] 显示失败原因。
- [x] 支持手动重试。
- [x] 支持取消队列任务。
## DNS 与服务器配置检查
外部投递不仅需要代码,还需要正确 DNS 和服务器信誉配置。
- [ ] SPF 记录,例如:`v=spf1 mx ip4:服务器IP -all`
- [ ] DKIM 记录,例如:`default._domainkey.example.com TXT ...`
- [ ] DMARC 记录,例如:`v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com`
- [x] SPF 记录,例如:`v=spf1 mx ip4:服务器IP -all`
- [x] DKIM 记录,例如:`default._domainkey.example.com TXT ...`
- [x] DMARC 记录,例如:`v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com`
- [ ] PTR / rDNS 反向解析指向邮件主机名。
- [ ] 邮件主机名 A/AAAA 记录指向服务器。
- [ ] 服务器 25 端口出站未被云厂商封锁。
- [ ] 主机名、HELO/EHLO 名称、证书域名尽量一致。
- [x] 邮件主机名 A/AAAA 记录指向服务器。
- [x] 服务器 25 端口出站未被云厂商封锁。
- [x] 主机名、HELO/EHLO 名称、证书域名尽量一致。
## 当前边界