Skip to content

fix(smtp): 修复下游投递失败后 SMTP DATA 仍返回成功,导致上游误判为已发送 - #356

Merged
Jinnrry merged 2 commits into
Jinnrry:masterfrom
0clickjacking0:fix/propagate-downstream-delivery-errors
Aug 17, 2026
Merged

Jinnrry merged 2 commits into
Jinnrry:masterfrom
0clickjacking0:fix/propagate-downstream-delivery-errors

Conversation

@0clickjacking0

Copy link
Copy Markdown
Contributor

问题

已认证的 SMTP 客户端提交邮件时,Session.Data 会同步调用 send.Send。如果下游投递失败,PMail 虽然会在自己的数据库中记录 status=2,但函数最后仍会执行 return nil。因此提交方收到的是 DATA 成功响应,无法区分邮件已经成功投递、被下游拒绝,还是暂时无法投递。

出站 SMTP 客户端还存在一个相关边界:下游服务器返回 DATA 最终 250 响应时,已经正式接收该邮件并承担后续责任。此后即使没有返回 QUIT 的 221 响应,或 QUIT 发生错误,也不能把已经被接收的邮件重新判定为投递失败,否则上游重试可能造成重复邮件。

修改内容

  • 在已认证的 Session.Data 发送路径中,把同步下游投递错误返回给提交方。
  • 对临时、未知或混合类型的下游失败返回 451 4.4.0,允许提交方重试。
  • 当所有已报告的下游失败均为永久错误时返回 550 5.0.0,包括 SMTP 5xx 响应和 NXDOMAIN。
  • 保留临时 MX 查询错误和明确的 SMTP 5xx 拒绝原因,避免被后续 fallback 错误覆盖。
  • 收到明确的永久 SMTP 拒绝后,不再尝试其他 SMTP 传输方式。
  • 保护并发域名投递过程中共享的失败收件人集合,消除数据竞争。
  • 对返回给提交方的错误文本进行确定性排序、单行清洗和 UTF-8 边界截断,并限制在 128 字节以内。
  • 把 DATA 最终 250 响应作为投递结果;随后只执行有写入超时限制的尽力 QUIT,不等待 221 响应。

入站收信路径及现有 SPF/DKIM 认证追踪逻辑保持不变。本修复不需要新增配置或数据库迁移。

已知限制

PMail 会在向提交方返回一个 DATA 状态之前,同步向多个收件域投递邮件。如果某个域已经成功接收,而另一个域发生临时失败,事务级 451 可能使提交方重试整封邮件,从而让已经成功的收件人再次收到邮件。在 PMail 具备可按域记录重试状态的内部投递队列之前,需要精确重试行为的客户端应在每个 SMTP 事务中只提交一个收件域。

测试

  • 覆盖临时错误、永久错误、NXDOMAIN、缺少详细原因和混合错误的分类。
  • 验证错误文本的确定性排序、CR/LF 单行清洗和字节长度限制。
  • 验证保留临时 MX 查询原因和明确的永久 SMTP 拒绝原因。
  • 验证 DATA 拒绝会正确返回,同时下游已返回 DATA 250 但不返回 QUIT 响应时仍判定投递成功。
  • 对所有修改包执行启用 race detector 的定向测试。

已执行:

go test -race ./listen/smtp_server -run 'Test(DownstreamDeliverySMTPError|DeliverySMTPMessageSanitizesAndBoundsDetails)$' -count=1
go test -race ./utils/send -run 'Test(IsPermanentSMTPResponse|DeliveryFailureCausePreservesTemporaryMXError|DeliveryFailureCauseKeepsExplicitSMTPRejection)$' -count=1
go test -race ./utils/smtp -run '^TestSendMailUnsafeUsesFinalDataResponseAsDeliveryResult$' -count=1
go vet ./listen/smtp_server ./utils/send ./utils/smtp
git diff --check

@Jinnrry
Jinnrry merged commit 1da3e17 into Jinnrry:master Aug 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants