邮箱错误码解析指南:把 SMTP、Enhanced Status 和 DSN 变成结构化事件

面向开发者的邮件错误码解析方案:如何从 bounce 中抽取 SMTP 回复、X.Y.Z、DSN 字段,并驱动规则引擎、重试和告警。

SMTPDSNparsermail deliveryobservability

从工程实现角度看,邮箱错误码不是“展示给人看”的文本,而是一套可解析、可归类、可回放的失败协议。你真正要存的不是一整段 bounce,而是 SMTP reply、enhanced status code、DSN 字段、对端域名、重试次数、发件身份和策略上下文。否则你永远只能在工单里说“邮件发不出去”。

RFC 5321 的三位数回复适合做粗分类;RFC 3463 的 X.Y.Z 适合做机器判断;RFC 3464 的 DSN 适合关联原始消息和投递结果。IANA 的 registry 则告诉你 enhanced code 是有注册语义的,不是随便猜的。

建议的解析链路

raw bounce text -> SMTP reply parser -> enhanced status extractor -> DSN field parser -> rule engine -> retry / fail / escalate

一个常见误区是只解析第一行。实际生产里,对端经常同时给出一行标准码、一行人类可读解释、若干 DSN 头部,甚至把有用信息放进 MIME part。解析器应该尽量保留原文,同时抽取结构化字段。

字段来源用途
smtp_replySMTP 对话区分 4xx / 5xx
enhanced_statusRFC 3463细分原因
final_recipientRFC 3464确认最终目标地址
actionRFC 3464failed / delayed / delivered / relayed
diagnostic_codeRFC 3464保存对端原始诊断文本
remote_mtaRFC 3464定位问题对端
reporting_mtaRFC 3464定位本方报告来源

推荐的代码抽取规则

import re

SMTP_RE = re.compile(r'([245]\d\d)(?:[ -](\d\.\d+\.\d+))?')
DSN_RE = re.compile(r'([245]\d\.\d+\.\d+)')

# 例:550 5.1.1 user unknown
# 例:421 4.4.1 no answer from host

抽取时不要假定一定有 enhanced code。有些旧系统只返回 550 + 文本;有些网关会在同一条文本里放多个代码。建议优先抓最靠近 SMTP reply 的那个。

规则引擎怎么分层

  1. 4xx:标记为 transient_failure,加入延迟队列。
  2. 5.1.1 / 5.1.2 / 5.1.3:地址类错误,立刻失败。
  3. 5.2.2:邮箱满或存储不足,失败并提示重试窗口。
  4. 5.3.x / 5.4.x:系统、网络、路由问题,视对端性质决定是否重试。
  5. 5.7.1 / 5.7.8:认证、授权或策略拒绝,通常需要人工排查。

不要把 retry 逻辑写成“任何失败都重试三次”。邮件系统对反复重试非常敏感,尤其是策略拒绝和认证失败。对这些码值,应该先修配置再重新投递。

和 SPF / DKIM / DMARC 的关系

很多投递失败不是传输链路坏了,而是认证策略不通过。SPF / DKIM / DMARC 失败未必都以同一错误码出现;有的接收方会直接退 550 5.7.1,有的会给自定义文本,还有的会放进 Diagnostic-Code 里。因此解析器最好把邮件头里的认证结果也一起存下来。

auth_results = {
  "spf": "pass",
  "dkim": "fail",
  "dmarc": "fail"
}

如果转发链路参与其中,SPF 可能因为发送 IP 改变而失效,DKIM 可能因为修改正文而失效,DMARC 可能因为对齐不一致而被判拒绝。程序上最稳妥的做法,是把这些结果和退信码一起聚合分析。

最小可用的事件模型

{{
  "message_id": "abc123",
  "recipient": "user@example.com",
  "smtp_reply": "550 5.7.1",
  "enhanced_status": "5.7.1",
  "action": "failed",
  "diagnostic_code": "smtp; 550 5.7.1 delivery not authorized",
  "remote_mta": "mx.example.net",
  "attempt": 1,
  "retryable": false
}}

官方来源

实现层面的关键不是“识别多少个码”,而是把错误码变成稳定的事件模型,让队列、报警、重试和工单都能消费同一份事实。