用户点了“发送验证码”,应用日志显示成功,邮箱里却迟迟没有新邮件。客服拿到一张写着 550 的截图,开发者又发现服务商控制台把这封邮件标成“已接受”。这些记录可以同时成立:应用把邮件交给了中继,中继接受任务,收件方随后拒绝投递。排查的第一步,是把每条记录放回它发生的那一段链路。
本文讨论 SMTP 投递和退信。示例地址、主机名和回复均用于说明协议,不对应某次真实事故。核查资料的日期为 2026 年 9 月 23 日。
先找到最后一个明确接收邮件的系统
一封验证码邮件可能经过应用、发送服务商、收件域的邮件网关,最后进入用户邮箱。应用调用 HTTP API 得到的成功,与邮件网关在 SMTP 会话中返回的成功,发生在不同位置。把它们都记成一个 success=true,以后很难解释邮件停在哪里。
SMTP 会话又包含多个步骤。服务器在 RCPT TO 后返回 250,表示接受这位收件人;在邮件内容传输结束后返回 250,才表示接受了这次邮件交付的责任。这仍不能证明用户已经在收件箱看到邮件,后续处理可能产生退信,也可能将邮件归入垃圾邮件。RFC 5321对命令回复和接收责任作了区分。
排障记录至少保存应用请求 ID、服务商消息 ID、收件地址和时间。另有 Message-ID 时也保留,但不要假定它与服务商队列 ID 相同。一封邮件有多个收件人,还需要分别保存结果,否则其中一人拒收,会被误解成整封邮件全部失败。
设想给甲、乙两人发送会议通知,甲的地址被接受,乙的地址不存在。产品界面可以显示“甲已提交投递,乙地址被拒绝”,而不是让用户再按一次发送按钮,把甲也重复通知一遍。
错误码有三层,阅读时别丢上下文
最外层是 SMTP 三位数回复,例如 451 或 550。它告诉发送程序如何处理当前命令的结果。再往里,可能有 4.2.2、5.1.1 这样的增强状态码,提供更细的诊断。最后是服务端说明文本,可能包含策略原因、工单链接和队列标识。
| 收到的内容 | 可以先做的判断 | 还要确认什么 |
|---|---|---|
451 4.3.0 ... |
对方报告临时处理失败 | 发生在什么命令后,是否仍在队列中 |
550 5.1.1 ... |
地址类永久失败 | 实际投递地址、别名展开和域名拼写 |
550 5.7.1 ... |
授权或策略拒绝 | 发信路径、身份认证及对方说明 |
250 ... |
当前命令成功 | 是接受收件人,还是接受了邮件内容 |
增强码的第一段区分成功、持续临时失败和永久失败,后两段继续细分主题与原因。基础定义见 RFC 3463,新增条目需要查询 IANA 注册表。不要给所有 550 都贴上“用户不存在”的标签。
也不宜建立“自然语言永远优先”的解析规则。服务商可能改写文案、加入另一种语言,甚至输出前后不一致的码。自动程序依照协议和服务商已记录的规则分类,同时保存原始回复供人查看。出现矛盾时,应标记为待核查,避免凭一个关键词永久禁用用户邮箱。
退信是一份分收件人的报告
用户转发的退信常包含一段可读说明和一个机器可读部分。后者通常使用 message/delivery-status。在 RFC 3464定义的结构中,报告级字段和每位收件人的字段分开存放。下面是简化示意:
Reporting-MTA: dns; sender.example.org
Final-Recipient: rfc822; reader@example.net
Action: failed
Status: 5.1.1
Remote-MTA: dns; mx.example.net
Diagnostic-Code: smtp; 550 5.1.1 Recipient not found
Reporting-MTA 是生成报告的系统;Remote-MTA 指向相关远端。两者不同很常见,不能因为退信来自自己的发送服务商,就认定故障发生在那里。Action 说明投递结果,Status 给出分类,Diagnostic-Code 保存更接近原始会话的说明。
如果 Action 是 delayed,这封信可能还在排队。此时重新调用业务发送接口,会额外创建一封邮件;原来那封恢复后仍可能送达。验证码、账单和订单通知都应该区分“查看现有投递任务”与“新建一次发送”。
做解析时,还应保留重复字段、折行以及多个收件人块。用一个从头到尾搜索 Status: 的正则,容易把最后一位收件人的结果覆盖到所有人。成熟邮件解析库可以处理 MIME 结构,业务代码再将每个收件人块映射到本地任务。
4xx 等待重试,5xx 先处理原因
收到临时失败时,邮件传输程序通常保留队列并在以后重试。具体间隔、最长队列寿命和告警时间由邮件软件、服务商与业务时效共同决定,不宜把某套“5 分钟、15 分钟、1 小时”的数字写成 SMTP 通用规范。
先确认谁负责重试。如果已经把邮件交给发送服务商,而且它仍在排队,应用再开一套重试任务,只会制造重复。业务侧可以查询或接收投递回调,在超过自己的时效要求时向用户说明进展。例如验证码五分钟后失效,几个小时后送达的旧验证码没有帮助;界面需要提供重新申请,并使旧凭据按既定规则失效。
永久失败意味着不要原样重复同一请求。地址不存在,就请用户确认地址;策略拒绝,就检查发信身份和接收方要求。它不等于“这个邮箱永远不能用了”。用户修正拼写、管理员开通邮箱后,可以作为一次修复后的新投递处理。
需要抑制反复发送时,记录抑制原因、时间和解除条件。把一次临时网络故障永久写进黑名单,会让用户之后一直收不到通知。反过来,无视明确的无效地址持续发送,也会浪费队列并影响发信管理。
策略拒绝要沿着身份核对
看到 5.7.x,先看当前连接是谁发起的。应用连接提交服务器时,可能没有正确完成 SMTP AUTH;中继向外投递时,问题又可能出在域名认证、连接来源或收件方规则。同一个“未授权”,需要不同管理员处理。
SPF、DKIM 和 DMARC 也不是可以相互替代的三个开关。SPF 检查特定身份是否授权该发送来源;DKIM 校验签名;DMARC 将相关认证结果与可见发件域进行对齐判断。仅凭某处出现一个 pass,不足以说明整条链路符合接收方要求。具体规范分别见 RFC 7208、RFC 6376和 RFC 7489。
处理时选一封具体邮件,记录信封发件地址、可见 From、出口 IP、DKIM 签名域与完整认证结果。转发会改变投递路径,邮件列表还可能修改正文,所以直发通过、转发失败并不矛盾。先把变化点找出来,再调整对应配置;不要为了让一个测试通过,随意放宽整个域的认证策略。
一次排查记录可以怎样写
假设应用在 10:00 接受了发送请求,中继在 10:01 返回任务编号,10:03 的投递回调给出 550 5.1.1。这条时间线说明应用提交没有失败,最终收件地址在后续投递中被拒绝。应核对实际地址,停止对这份原始地址的重复投递,并把结果同步给用户;继续增加应用 HTTP 超时时间没有帮助。
如果另一封邮件在 10:03 得到 451,而服务商仍标记为排队中,处理就不同。先保留任务编号,观察后续回调,不立即创建新邮件。超过业务等待期限时,用户可以重新申请验证码,但界面要让他辨认最新一次申请,后端也不能接受已经失效的旧码。
两种情况都可能被用户描述为“没有收到”,内部却需要不同状态。将失败阶段、是否仍在排队、最近诊断和用户下一步动作一起保存,比单独增加一个“发送失败次数”指标更能帮助支持人员处理问题。
给客服和开发者各留一份能用的结果
客服需要知道用户能做什么:修改地址、稍后等待、检查垃圾邮件,或者由管理员处理。开发者需要原始回复、阶段、远端和关联 ID。这两份信息可以来自同一条投递记录,却不必展示相同内容。
例如地址失败可显示“收件服务器拒绝了这个地址,请核对拼写”,内部同时记录增强码与远端。对于策略拒绝,界面不要误导用户反复换密码;应说明通知暂未送达,后台创建可追踪的排查项。
日志中不要收集验证码明文、SMTP 密码或整封敏感邮件。诊断通常先需要时间、地址和投递标识。收件地址还涉及个人信息,支持人员的访问范围和保留期限也应明确。
修复后用同一条发送路径重做验证:应用任务是否建立,中继是否接受,最终回调是否到达,测试邮箱是否实际收到。记录每一步的结果,下一次用户问“为什么没收到”,就不必再从一张 550 截图重新猜起。











