摘要

  • RFC 5321 规定,接收方在 DATA 最终结束符之后返回肯定完成答复,表示它接受完整消息,并承担投递或中继责任。这是保管权交接,不是终点回执。
  • 250 必须与命令和会话状态绑定。EHLO、MAIL、RCPT、RSET、NOOP 之后都可能出现相同数字,但它们没有接受完整邮件正文。
  • 可审计证据要串起信封、逐收件人答复、DATA 最终答复、持久队列、每一次下游交接和本地结果。垃圾箱、隔离区和用户阅读仍属于其他控制者。

三个数字不能脱离它回答的问题

一条日志写着:远端 250 OK。如果没有上一行,这个记录几乎无法回答“邮件发生了什么”。

它可能是 EHLO 的肯定答复,表示双方处在初始状态,服务端还可能列出扩展能力。它可能接受 MAIL FROM 中的反向路径,也可能接受某一个 RCPT TO,把该地址留在当前事务里。RSET 清空事务后会得到 250;NOOP 什么也不改变,同样可以得到 250。

只有 DATA 的最后一道肯定答复承担完整消息。客户端先收到 354,获得发送正文的许可;这还不是接收。客户端发送内容和结束标记,服务端再处理保存的反向路径、已接受的收件人和邮件数据。成功就肯定完成,失败就返回失败。在这个边界,基础 SMTP 模型不允许把同一事务说成一半接受、一半未接受。

肯定答复一旦发出,责任换手。发送方之所以可以删除本地副本,不是因为已经看见收件箱,而是因为接收方承诺继续投递、转交,或者在之后失败时承担相应处理。

因此,单独保存 250 会毁掉它最重要的上下文。收据至少需要连接与事务标识、回答的具体命令、MAIL FROM、已接受和拒绝的 RCPT、DATA 结束时刻、远端身份以及队列标识。

John Klensin 在这里的角色也必须这样限定。IETF 官方 Datatracker 将其标识为 Dr. John C. Klensin;本次研究抓取时页面列出60份 RFC。RFC 5321 署名他为作者。这证明他参与了集体标准文本,不证明他控制某个正在运行的邮件系统,更不为厂商实现背书。

先持久,再承诺

责任交接要求运行代码把顺序做对。接收服务应先达到自身承诺的持久性,再发送肯定答复。如果先说“好”,再尝试写入队列,就会制造一个危险窗口:发送方已经合法释放副本,接收方却还没有可靠保存。

RFC 5321 要求,接收方不能因为后来宕机或可预见的资源不足等轻率原因丢失已经接受的邮件。它并没有规定全世界使用同一种数据库、日志或复制架构。标准给出责任;本地实现选择 fsync、事务、镜像或其他机制。

这正是 Heng Lu 所说运行代码优先可以澄清的地方。RFC 说明答复应当意味着什么,不能证明某一次执行确实完成了持久写入。厂商文档、作者姓名和语法正确的响应都不能替代队列产生的执行证据。

如果服务承诺“复制后才应答”,审计就应当保留队列句柄、写入或复制阶段、配置版本与应答时刻。证据不必泄露邮件内容,但必须足以判断承诺发生在应答之前还是之后。

“投递或中继”留下两条路

RFC 5321 对责任的描述故意包含两个动词:投递,或者中继。接收方可能是最终邮件环境,也可能只是链路上的一个 MTA。中继接手后,会以客户端身份发起下一次 SMTP 事务,再从下一跳取得新的肯定答复。

每次答复只覆盖自己的交接。上一跳的成功不能自动灌入下一跳,更不能一直传到用户界面。

Received 字段很重要,它记录发送主机与接收主机的声明身份和时间,多跳邮件会累积多行。它却不是端到端签名证明。它无法单独证明下一队列持久、别名展开正确、网关没有改写含义,或邮箱提供商把邮件展示给了用户。

“最终投递”在 SMTP 里也有协议边界:邮件离开 SMTP 环境。通常它进入用户或相关 mail drop,但其他邮件系统仍可能继续加工和传送。协议结束,不等于人的结果结束。

一个诚实的状态模型应当拆开:本跳已接受、已持久保存、下一跳已接受、已进入本地投递、已写入邮箱、已隔离、已按策略丢弃、用户是否可见未知。某个服务不开放后续状态时,应当写“未知”,而不是借用上一跳的绿灯。

没有退信,不是肯定回执

如果接收方在接受之后才发现无法投递,RFC 5321 通常要求生成通知,发往信封反向路径。通知本身使用空反向路径,以免通知失败继续制造通知。

负向通道并不总是存在。原邮件的反向路径本来就是空值时,SMTP 不会向它再发失败通知。中继、邮件列表、别名或临时 DNS 问题会让地址在事务期间无法验证。面对伪造发件人和垃圾邮件,标准也承认现实策略可能在不通知声称发件人的情况下丢弃不受欢迎的消息。

所以,“没有 bounce”可以对应成功、通知仍在队列、无可用返回地址、网关无法传播状态、静默策略或监测缺口。它不能单独证明入箱。

RFC 3461、3463 与 3464 定义了另一层证据:DSN 请求、增强状态码和逐收件人报告。报告可以区分 failed、delayed、delivered、relayed 和 expanded。请求报告不等于保证报告出现;relayed 明确不宣称最终入箱;delivered 描述邮件系统动作,不证明人类阅读。报告本身还要走完自己的投递路径。

因此,DATA 最终 250 与 DSN 是前后两张收据。第一张确认谁接下责任,第二张可能描述责任后来走到哪里。把两者压成一个“已送达”字段,会同时丢掉责任人与结果。

丢失的答复会制造副本歧义

设想接收方已安全保存邮件并发出 250,但 TCP 在发送方看到答复前中断。接收方拥有有效副本;发送方无法确认交接,也会保留副本并重试。最终可能出现两封相同邮件。

RFC 5321 为 DATA 结束等待留出较长时间,因为服务端可能需要处理;同时要求尽量缩短最终答复延迟。这个设计减少歧义,却不能让双方永远同时看到最后一个事件。

“未观察到最终答复”不等于“远端拒绝”。它是带有重复风险的未知状态。队列应保存原始消息句柄、连接中断、重试决策和可用于后续对账的标识。直接写成“未送达”或“只送达一次”,都超过了证据。

接受率指标同样受限。它计算被确认的交接,不计算收件箱;如果不单列重试和疑似重复,还会把成本藏在成功率后面。

LMTP 把结果拆到收件人

SMTP 在 DATA 之前逐条回答 RCPT,但 DATA 结束后的答复覆盖已接受的整个事务,不为每个地址分别返回最终结果。

面向本地投递的 LMTP 改变了形状。RFC 2033 要求在最终点号之后,按原顺序为每一条已成功的 RCPT 返回一条答复。肯定完成答复把对应收件人的责任交给服务端。

解析器必须知道协议与答复数量。把一个 SMTP 250 展开成多个逐收件人最终成功,是凭空增加精度;把一串 LMTP 结果压成一个总状态,又可能把失败绑定到错误地址。最低限度的收据要保留 RCPT 顺序和答复基数。

建立责任账,而不是送达徽章

第一段记录连接与事务,端点身份和 TLS 上下文要与邮件接受分开;随后记录 MAIL FROM、每个 RCPT 及答复、354、最终结束答复、时间和队列句柄。

交接之后由新责任方继续写:持久状态、尝试次数、下一跳、下一跳答复和歧义断线。本地投递写入代理或 LMTP 结果。DSN 加入逐收件人的 Action、Status,以及报告自身是否成功交出。垃圾箱、专有网关和阅读状态没有独立来源时继续未知。

这是一份最小初始规范,不是统一实现。运营者仍决定队列引擎、重试节奏、隐私和保留期。共同要求很窄:答复不能脱离命令,保管不能改名为送达,上一跳的确定性不能赠送给下一跳。

一份可信的事故结论可以这样写:本服务在 DATA 结束时接受了这组信封与正文,责任由此换手;之后已观察到这些中继或本地结果;其余逐收件人和用户结果仍未知。

来源