邮件发出去了却没收到:沿着 SMTP 回复和退信找原因

从应用提交到收件方退信,区分 SMTP 回复、增强状态码与 DSN,解释队列重试、身份认证和用户收不到邮件时的排查步骤。

把应用部署到土耳其|BRNCHOST · 土耳其 VDS
云服务器,积分可续期|雨云 · 国内外节点 · 积分兑换权益
低价年付,大流量 VPS|RackNerd · SSD 存储 · 1Gbps 端口
香港轻量,搭个小站|晚安云 · 香港云服务器
香港 VPS,大带宽可选|野草云 · BGP 直连
大陆访问,精品线路|搬瓦工 · CN2 GIA / CTGNet 套餐
资料归档,交给 AI 整理|WorkBuddy · 本地文件处理
建站起步,先看应用镜像|腾讯云 · 轻量应用服务器
CN2 GIA,中国方向优化|DMIT · Premium 网络
双 ISP 原生住宅 IP|丽萨主机 · 美国 9929 精品线路
高频 CPU,多地部署|Evoxt · 云服务器 · 每周异地备份
京东云轻量云主机:129元/年,新人专享,限购1台

用户点了“发送验证码”,应用日志显示成功,邮箱里却迟迟没有新邮件。客服拿到一张写着 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 截图重新猜起。