摘要

  • RFC 9738 允许 SEARCH、FETCH、STORE、MOVE 或 UID EXPUNGE 只处理 UID 较高的一段后返回 OK;MESSAGELIMIT 同时说明处理上限与本次抵达的最低 UID。
  • COPY 与 MULTIAPPEND 必须保持原子性,超限时不产生部分结果;其他命令可能已经造成真实变化,客户端必须按各自语义续跑并对账。
  • CAPABILITY 中的数字是会话协作边界,不是严格执行已经全面启用、所有客户端已经适配、邮箱已经冻结或用户结果已经完整的证明。

结果末尾出现了 OK。这不是误码,也不是服务器自相矛盾。它表示本次命令已经成功处理自己实际覆盖的那一段。若同一行还带着 MESSAGELIMIT 1000 23221,它也同时表示:原请求中低于这道边界的部分还没有被本次调用覆盖。

很多系统只保留一个布尔值:成功或失败。RFC 9738 说明,面向大邮箱时,这种数据模型会主动制造误判。调用状态回答“刚才执行的工作是否成立”,完成状态回答“最初想覆盖的集合是否已经耗尽”。两者不能共用一个绿灯。

上限约束一次命令,不约束邮箱大小

服务器用 MESSAGELIMIT=N 声明单条 SEARCH、FETCH、STORE、COPY、MOVE 及其 UID 版本,以及 APPEND、UID EXPUNGE 最多处理多少封邮件。若只限制 COPY 与 APPEND 这两类保存操作,则使用范围更窄的 SAVELIMIT=N。规范建议 N 不低于一千。

这个数字不是邮箱容量,不是保留期限,不是配额,也不是单封邮件大小。它只描述一次命令愿意处理的邮件数量。

SEARCH 还有一个容易被忽略的细节:计算的是被检查的邮件,而不是匹配结果。服务器检查一千封后可能只返回十三个 UID。十三个匹配全部真实,但不能据此宣布整个目标区间只有十三个匹配。更旧的邮件尚未进入分母。

一旦返回 MESSAGELIMIT,服务器必须从最高 UID 向最低 UID 处理。响应中的最后一个 UID 是本次实际抵达的最低点。它适合作为下一段查询的接缝,却不是剩余邮件的清单、快照编号或数量证明。并发到达与 expunge 都可能改变集合;若 UIDVALIDITY 改变,旧接缝更不能继续解释新的邮箱代次。

OK 证明本次效果,不证明原始意图完成

FETCH 可以先返回最新一千封的属性,再以带限额的 OK 结束。SEARCH 可以返回这一千封中命中的集合。STORE 可能已经改变这一段的 flags。MOVE 可能已经把这一段复制到目标邮箱并从源邮箱 expunge。UID EXPUNGE 则可能已经永久移除这一段中带 \Deleted 的邮件。

这些都不是预演,而是真实效果。把它们一概记为“失败”,会丢掉已经发生的变化;把它们一概记为“完成”,会丢掉仍待执行的部分。

续跑方式必须服从命令语义。UID STORE 的下一次范围应排除响应给出的最低 UID。UID MOVE 可以重复原始 UID 集合,因为已移动的邮件已经不在源邮箱。使用 sequence number 的 MOVE 则必须重新计算范围,expunge 已经改变后续位置。UID EXPUNGE 可以重复同一 UID 参数,直到响应不再出现限额,或明确返回 NO、BAD。

因此,“自动重试”不是一个足够具体的设计。系统要知道上一次处理过哪些 UID、发生了什么效果、下一次为什么既不重复又不遗漏。如果把 UID 与 sequence number 混在同一个游标字段里,一次看似正常的续跑就可能作用于另一封邮件。

限额信号也不总在最终 tagged response 内。当服务器还要返回 EXPUNGEISSUED 时,后者必须占据最终 tagged OK,而 MESSAGELIMIT 放进 untagged NO。只读最后一行的解析器会保留“成功”,却扔掉“未完成”。完整凭据应包含命令 tag、全部 untagged response、返回数据、response code 与最后状态。

相同数字不会产生相同事务结果

COPY 与 UID COPY 必须是原子的。目标集合超限时,服务器返回 NO [MESSAGELIMIT …],一封也不能复制。MULTIAPPEND 同样需要全有或全无,过大的组不能先追加前一部分。

MOVE 没有这项整体原子保证。它可以先移动最新一段,再告诉客户端还有剩余。STORE 与 UID EXPUNGE 也能留下部分修改。于是,同一个 MESSAGELIMIT 数字至少对应两种完全不同的现场:没有任何效果的原子拒绝,以及已经发生效果、需要续跑的部分成功。

还有第三类:RFC 9738 不允许对 EXPUNGE、CLOSE 与 STATUS UNSEEN 套用这项邮件数量上限。这保留了它们在 IMAP 层的完整语义,但并不自动证明磁盘已经耐久写入、另一个客户端已经同步或读者已经看到变化。

RFC 9394 的 PARTIAL 能进一步说明差别。客户端若明确请求一个大于 N 的 PARTIAL 页,服务器应拒绝并且不做任何工作;不带 PARTIAL 的类似 FETCH 却可以隐式处理一段后返回部分成功。主动约定的页与服务器被动截断的一段,不能因为都“分批”就归入同一个状态。

$ 会把缺口保存下来

SEARCHRES 允许把一次 SEARCH 结果保存为 $。如果源 SEARCH 命中 MESSAGELIMIT,RFC 9738 要求保存的集合也必须截断。$ 精确地代表本次已检查范围内的匹配,但它不代表完整原请求的匹配。

危险往往在下一步出现。后续 COPY、STORE 或 SEARCH 使用 $ 时,集合可能已小于上限,整个调用可以干净地返回成功。新 transcript 中未必再次出现 MESSAGELIMIT,然而输入集合仍带着上一轮的缺口。后一次正确执行无法修复前一次未完成的分母。

因此,保存结果还要保存来源:邮箱、UIDVALIDITY、原始查询、目标区间、限额、最低处理 UID、时间以及“尚需续跑”的状态。只有变量名而没有这些信息,便利句柄很快会被误用为完整性证明。

RFC 9738 新增的 UIDAFTER 与 UIDBEFORE 能更方便地表达下一段范围。它们不会暂停新邮件到达,不会让已经消失的 UID 重新存在,也不会跨越邮箱代次。它们只是构造查询的工具,不是跨多条命令的事务锁。

声明上限可以先于严格执行

规范对旧客户端的处理尤其现实。若服务器突然严格限制大型邮箱,不理解扩展的客户端会只看到最新 N 封、得到错误的 SEARCH 计数,或在 COPY 时失败。规范明确指出,这相当于放弃这些客户端在大邮箱上的正常工作。

所以迁移可以分阶段。服务器先声明 MESSAGELIMIT,但暂时不严格拦截。理解扩展的客户端主动缩小命令,服务器负载已经下降;旧客户端仍可继续工作。之后,服务器可以声明一千这个软上限,却先把真正的硬上限设在未公开的一万。超出声明值的调用可被记录,用来观察客户端是否已经适配,再决定何时收紧。

这说明 CAPABILITY 是协作信号,不是执行测量。看到 MESSAGELIMIT=1000,可以要求新客户端以一千为设计边界,却不能只凭这个字符串断言服务器目前每次都在第一千封切断。真正的采用状态要从客户端请求、服务器响应与实际硬阈值共同观察。

软上限与硬上限可以并存,但必须分别记录。超过一千的命令,可能来自拒绝适配的新客户端、仍被宽容的旧客户端,也可能发生在严格执行启用之前。若报表把这些情况都叫“违规”,它会把过渡期变成虚假的合规故事。

完成需要给原始集合结账

可靠的全邮箱任务应先冻结“意图”,而不是假装冻结邮箱。记录开始时希望读取、修改、移动或删除的范围;随后为每次调用保存邮箱、UIDVALIDITY、命令、请求集合、原子类别、当时的 capability、返回 UID、已观察效果、untagged response、最终状态、限额与最低 UID。

收尾时至少要区分四个集合:开始时计划覆盖的邮件;实际检查或改变的邮件;续跑前已经消失的邮件;开始后新到达而不属于原范围的邮件。无法解释的差异必须保留为未决,而不能因为最后一次命令没有限额就自动清零。

相邻扩展也应保留各自职责。UIDBATCHES 用来规划有界 UID 区间;PARTIAL 请求明确的一页;MESSAGELIMIT 报告执行时遇到的单命令上限;SEARCHRES 保存结果集合;QRESYNC 协助同步变化。把它们都叫“批处理”,会让计划、请求、执行与同步之间的边界消失。

RFC 9738 最重要的不是让服务器少做一些,而是让“少做一些”仍能被客户端诚实地完成。服务器可以写下成功,因为这一页确实写完了;客户端必须继续保管整本书的目录,直到旧邮件也有明确去向。

来源