摘要

  • RFC4865 允许用户把等待邮件释放的工作交给提交服务器,但“到某时刻之前不能释放”不等于“在那个时刻准时送达”。与 DELIVERBY 同用时,客户端必须让投递期限晚于释放时间。
  • 服务器对已发现的时间矛盾有拒收义务;存储额度、时钟、超时后的处理模式和通知证据仍须分别管理。通过身份验证、时间窗口合法,都不是未来传输能力的预约凭证。

如果运维报表把所有拒收都列为服务质量下降,一种正确执行协议的行为就会被误算为故障。某些请求本来就不应进入队列。服务器越努力让它们显示“成功”,越可能是在替发送人放弃一项从未获准放弃的条件。

可以用一个假设说明:上午提交的邮件要求中午之后才能释放,同时要求十一点过后停止投递尝试。这不是本文发现的真实事故,也不是一次已经执行的测试。它只是把两项约束摆在同一条时间线上。加内存、扩容出口、提高调度优先级,都不能让中午之前禁止离开的邮件在更早的截止点之前完成投递。问题存在于承诺本身。

2007年5月发布的 RFC4865 为 SMTP 提交服务定义了 Future Message Release。客户端可以提前把邮件交给服务器,由后者保存到约定的未来时间,不必依赖客户端一直在线或保有本地存储。对提醒、通知和预约发送而言,这是一种便利;对运营者而言,则是提前接下一批尚不能处理完毕的工作。

值得分析的不是“定时发送很方便”,而是谁有权承诺这段等待、谁提供存储、谁判断等待已经结束,以及另一项投递期限是否容得下这些义务。规范没有把这些责任合并成一个绿色图标。

先判断承诺能否同时成立

DELIVERBY 和 FUTURERELEASE 处理的是不同方向的约束。前者由 RFC2852 定义,要求在某个时间范围内投递,并指定未能及时投递时如何处理;后者规定什么时候之前不得释放。一个限制太早,另一个处理太晚,只有把两者的含义分开,才谈得上共同使用。

RFC4865 第5.2.1节对客户端的要求很明确:同一封邮件同时使用这两个扩展时,指定的投递期限必须比指定或推算出来的释放时间更晚。这不是简单地填入两个都在未来的值。两个单独看起来合法的时间,可以组合成一个无法履行的请求。

同一规范第5.2.2节规定,支持两个扩展的提交服务器如果判定释放时间晚于投递期限,就必须拒绝 MAIL 命令。对于这一情形,规范建议使用501回复和5.5.4增强状态码。服务器不应悄悄提前释放,也不应擅自忽略截止要求,以便把统计中的失败数降下来。

但这里还有一个不能用口语抹平的细节。客户端的约束是投递期限严格晚于释放时间;服务器这一条明确写出的拒绝条件,则是释放时间晚于投递期限。两句不是完全相同的比较式。不能把服务器条文自行改写成“大于或等于时必须拒收”,也不能反过来把服务器条文没有在此明确覆盖的相等情况,说成客户端可以合规提交的零长度窗口。

如果某个实际产品如何处理相等值会影响业务,就应单独观察、记录它的行为。客户端是否遵守自身义务,与服务器在边界输入上返回什么,是两个验收问题。用一边条文的空隙替另一边创造许可,会丢掉规范真正表达的责任分工。

这种精确不是文字游戏。假设发布部门关心“不能提前”,提醒业务关心“过时不要再送”,由谁在冲突中作选择就是一项业务决定。提交服务可以拒绝无法同时满足的条件,但接受率指标不应赋予它替用户决定哪一项不重要的权力。

定时服务卖的是等待,不是整条路径的预约

服务器在 EHLO 回复中声明 FUTURERELEASE,并给出两个上限:最长可等待的间隔,以及最远可接受的未来释放日期时间。客户端先检查能力,再在 MAIL 中使用且只使用一个等待参数。HOLDFOR 表示间隔,HOLDUNTIL 表示日期时间;各自都不能超出对应的已声明上限。缺少等待参数,并不暗示某个默认等待期。

规范同时保留相对和绝对两种写法,是为了容纳不同客户端的时间能力。有的设备掌握本地时间却缺少可靠时区信息,有的设备没有足够准确的时钟。给出两种方式能降低一部分客户端的使用门槛,却不会让时间从基础设施中消失。

对于已接受、带有未来释放要求的邮件,服务器不得在间隔结束或指定时间到来之前释放。这是一条“最早允许”的边界,而不是“必定恰好发生”的事件记录。到点以后什么时候真正执行释放、下一跳是否接受、最终邮箱何时收到,都还需要各自的运行事实。

因此,一个合法且非空的窗口也不能直接成为交付保证。它只说明这两项时间要求没有在这一层互相否定,并没有为出口带宽、后续服务器、收件邮箱或人的注意力建立预约。把日历中可选的最远日期解释为相应日期一定有足够能力,是把能力声明读成了资源预留。

这里的时间依赖还有一个常见误区:把 HOLDFOR 看成天然不怕时钟异常的选项。RFC4865 的安全讨论明确指出,提交服务器时间不准或发生变化,会让两种请求都面临过早或延后释放的风险。相对写法缓解的客户端问题,与服务器如何保持时间可信,不是同一道题。

本文没有调整时钟,也没有向某个邮件产品发出试验请求。时钟跳变后具体如何执行、如何恢复、误差多大,都需要对实际实现进行受控验证,不能从参数名称推导。

“过期”必须保留处理模式

RFC2852 不把投递期限当作要求优先处理的工具。服务器可以自行赋予优先级,但扩展本身没有强制这种待遇。投递期限也不是一个延后释放时间,更不会把中继服务器原有的不可投递邮件保留期自动延长。

尤其要保留 Return 与 Notify 两种模式的差别。选择 Return 时,如果在期限前还没有完成相应投递或中继,后续投递尝试不得继续;对符合通知条件的收件人,须生成相应的失败通知。选择 Notify 时,期限到了并不意味着工作必须停止,系统应按站点策略继续尝试,并对符合条件的收件人生成延迟通知。

运维页面可以为了简洁显示“已过截止点”,但不能因此把两种状态合并成同一个处置动作。否则,一种错误是丢弃仍可继续尝试的工作;另一种错误则是继续发送本来应该停止尝试的邮件。它们可能对应完全相反的业务损失。

路径能力也不一样。Return 模式不能交给不支持 DELIVERBY 的下一跳,也不能交给其固定最小间隔大于当前剩余时间的服务器。Notify 模式可以跨越不支持该扩展的一跳,但会带来规范规定的通知后果。一个标签能否继续携带原来的义务,取决于下一段链路实际承接什么,不取决于上一段日志是否写了“已转交”。

而且,停止尝试与发送人得知停止是两件事。RFC2852 明言,失败通知本身不必获得加急处理,所以发送人不能仅凭一个很短的投递期限,就预期在同样短的时间内收到失败消息。期限可以约束某项处理行为,却无法自动约束有关处理结果的知识何时到达。

这也要求区分 SMTP 命令阶段。MAIL 得到正面回复,不等于最终邮件已经送达,甚至不等于整个提交过程已经成功结束。RFC2852 指出,有些不能履行的条件要到收件人处理或数据传输完成时才会显现。只保留一个“接受”事件,会把尚在协商和已经承担后续责任混在一起。

身份通过了,额度仍可能没有

定时发送把等待从终端搬到服务端,同时把存储成本也搬了过去。恶意的未授权提交需要防护,但一个已经获准使用服务的账户,同样可能积累足够多的未来邮件,使提交服务器的存储承压。身份验证只能回答谁在使用,不能回答这份使用是否在资源预算之内。

RFC4865 建议,对未来释放邮件的存储实行按用户划分的额度。如果服务器确实实施了这种额度,并且检测到新邮件将超过它,就必须拒绝 MAIL。这里的条件要完整保留:规范中“应当设置”的建议级别,与在已实施额度且发现超限后的“必须拒收”,不是同一种强度。

增强状态码又区分了用户额度不足与系统额度不足:X.7.16 和 X.7.17。前者指向该用户的队列预算,后者指向共享系统预算。等待某个账户已有工作排空,与等待系统整体恢复,是不同的诊断和协调任务;不能因为都包含“存储不足”,就统一给出一个没有依据的重试时间。

即使每个用户没有越界,也不能断言未来工作会均匀到来。许多邮件被安排在相近时刻释放,是值得受控测试的一种场景,而不是本文声称已经发生的事故。它说明约束可能从等待阶段的存储字节,转向释放阶段的处理能力和传输能力。单独合法的请求未必能组成没有压力的总负载。

Lu Heng 在第32篇笔记中提出,把控制权与经济后果分离,会塑造机构的行为。放在这个具体问题上,值得追问的是:谁可以扩大对外展示的预约期限,谁承担未来队列的成本,谁有权在资源不足时拒绝接单。这是借用其代理问题视角分析责任配置,不是据此指控某家厂商或工程师的动机。

第36篇笔记对 BTW.Media 的定位也构成约束:描述结构,不以倡议代替事实。定时服务的便利是真实的设计目的;存储和履约责任也是真实的设计负担。无需把任何一方写成反派,才能看清这笔交换。

留下原始请求,不要把它改写成结果

当提交服务器为一封带有未来释放请求的邮件生成 DSN 时,RFC4865 要求机器可读部分包含 Arrival-Date 和 Future-Release-Request。后者按照规定格式保留原先的等待参数值。这样,接到通知的一方才有机会区分“用户要求等待”与“系统本来想发却迟迟发不出去”。

这些字段保存的是语境和要求,不是释放动作的全部证据。它们不能单独证明实际离开等待状态的时刻、下一跳接受成功或最终投递成功。要回答那些问题,还必须把请求记录与后续观察关联起来。

规范同时禁止客户端在向提交服务器发送 DSN 或 MDN 时,再请求这项未来释放服务。关于处理结果的通知,不应通过同一机制又变成一份故意推迟的通知。这里说的是请求行为的明确限制,不是保证任何通知都不会在网络里延误。

邮件头的 Date 也不是可靠的队列事件替代物。RFC4865 的讨论指出,提交之后邮件头尤其是 Date 保持不变;发送客户端可以选择把未来释放时间放进去,而传输系统没有为了隐藏延后而修改 Received 等跟踪信息的要求或预期。一个显示日期无法独自说明邮件何时交给服务器、何时结束等待。

但不能把这段话扩大成“提交服务器永远不能修改日期”。替代 RFC4409 的 RFC6409 允许在提交阶段对缺少或语法错误的 Date 作有界补全或纠正。提交时的修整,与为了定时释放而事后伪造传输轨迹,应当区分。

RFC6409 对提交和中继的分工同样重要。提交通常使用587端口,其一般规则也允许把特定主机的25端口指定为提交服务;这不代表可以在普通中继端口宣告 FUTURERELEASE。调度扩展自己的“仅适用于提交”边界,不能被泛化的端口说法冲掉。

证据够回答什么

RFC4865 的已验证编辑性勘误2040修正了一个悬空的日期时间语法引用,改为引用 RFC3339 中的 date-time。报告人在2010年附带提到自己有运行中的服务器实现;这是有归属、有时间的历史陈述,不是今日部署规模或互通质量的测量。

RFC2852 的编辑性勘误2300则处于“留待文档更新”状态,内容是语法中的空格问题,不是对期限处理规则的修改。两项勘误都值得保留,但不能让修订记录承担它没有作出的行为承诺。

这份分析没有进入生产邮件队列,没有观测真实发送事故,也没有证明某一实现满足全部条件。它能做的是把规范中的义务拆清,并提出值得验证的边界。真正需要的不是把所有请求都送入队列,而是把无法共同履行的要求挡在入口,把已经接受的等待安排给有资源、有证据、有责任人的服务。

资料来源