摘要
- 邮箱搜索完成,不代表服务器接下了持续维护搜索结果的工作。IMAP 允许拒绝 UPDATE,同时兑现其他返回选项。
- 屏幕只显示几行,不能证明后台只维护几行;PARTIAL 的结果窗口不会自动缩小 UPDATE 所覆盖的匹配集合。
- 是否接纳持续更新、何时结束、断开后怎样恢复,应当成为产品可以解释的状态,而不是藏在一次“成功”之下的成本转移。
企业为一个邮箱产品付费,通常不会分别购买“查一次未答复邮件”和“以后替我盯着这些邮件”的两个按钮。界面把它们合在了一起:打开视图,出现数字和列表,之后看上去便理应一直正确。用户感受到的是一项完整服务,服务器承担的却可能是两类不同工作。
第一次查询有一个结束点。服务器找到符合条件的邮件,返回所要求的信息,这项工作就可以完成。持续视图没有同样简单的结束点:新邮件到达、标记改变、旧邮件被清除,都可能要求重新判断成员资格,调整排序,再把变化告知客户端。第一次答对了,不会自动支付以后每一次变化的成本。
这里讨论的是一种可能的产品结构,并非某家邮件服务已经发生的事故。它之所以值得拆开,是因为“搜索成功”与“视图实时”之间,并不存在一条可以省略的等号。
2008 年发布的 RFC 5267 给出了明确例子。客户端可以在搜索或排序时请求 UPDATE,也就是要求服务器继续报告结果集合的变化。服务器可能因为内部更新上下文数量达到限制而拒绝这项请求。它发出带有 NOUPDATE 响应码的未标记 NO,并在其中指明原搜索命令的标记。与此同时,其他返回选项仍必须得到兑现,命令本身可以以成功结束。
这不是服务器同时说“成功”和“失败”而自相矛盾。它回答了两个问题:这次查询可以做,之后的持续维护不接。客户端若只保留最终成功、丢掉前面的拒绝,制造歧义的便不再是协议。
先把“盯着”的工作记到账上
持续搜索不是简单地把已经生成的页面多放一会儿。服务器至少需要保留足以判断后续变化与哪些查询有关的信息。某些实现会保存完整结果,有些会根据旧标记、新标记和搜索条件做增量判断。内存、处理时间与发送更新的成本可以有不同组合,却不会因为用户没有滚动屏幕就自然消失。
RFC 5267 并没有因此把拒绝权写成无限自由。服务器必须为每个客户端提供至少一个持续更新上下文,并且应该提供更多。这个最低要求很重要:实现不能一边声称支持这项扩展,一边把所有更新请求都当作可以随意拒绝的额外服务。
但最低要求也不是无限配额。它没有规定任意数量的保存视图都要获准,没有给出每个上下文应占多少字节,更没有替商业服务承诺统一的更新延迟。协议划出了最低能力,具体服务仍要确定自己的可行范围。
排序还会改变成本。同一份文件指出,排序上下文的实现代价高于未排序的搜索上下文。因此,服务器拒绝 SORT 的更新,并不意味着 SEARCH 的更新也一定会被拒绝。改用未排序搜索或取消另一个上下文,是客户端可以考虑的应对方式。
然而,改用未排序结果也不能偷偷完成。用户如果是按某种顺序处理工作,排序本身就是任务的一部分。降低服务器成本与保持用户原有工作方式,未必是同一个决定。产品可以提供替代方案,但不能把替代方案伪装成原请求已经完整实现。
三个名字,不是一项能力
CONTEXT、UPDATE、PARTIAL 经常围绕同一次搜索出现,很容易被理解为一种“实时分页缓存”。这种理解恰好会掩盖最重要的边界。
CONTEXT 是提示。客户端用它表达未来可能再次使用相同搜索条件,服务器可以据此保留结果缓存或索引,也可以忽略。它不是建立快照的命令;没有使用 CONTEXT,也不妨碍客户端请求 UPDATE。
UPDATE 是持续报告匹配集合变化的请求。它关注的是哪些邮件加入或离开结果集合,而不是把最初返回的数据格式不断重发。最初只要了一个 COUNT,不代表后续更新就是一串新的总数。客户端可以根据成员增减维护数字,但维护数字的工作仍由客户端完成。
PARTIAL 则限定本次返回哪个结果区间。它限制的是被取出的部分,不是后续 UPDATE 的作用范围。用户眼前只显示几十封邮件,后台仍可能需要处理整个匹配集合的变化。列表的视觉密度,不是持续工作量的可靠代理。
2023 年的 RFC 9394 扩展了 PARTIAL,允许从结果末端计数,也为 UID FETCH 增加了相应修饰符。它明确保留了一个关键事实:PARTIAL 与 UPDATE 结合时,更新仍覆盖所有匹配结果。支持 PARTIAL 的能力声明也不等于支持 CONTEXT=SEARCH,组合使用之前仍须确认相应能力。
这份更新文件还有一个很能暴露误判的细节:在规定的 SAVE 与 PARTIAL 组合中,如果同时请求 COUNT,保存的搜索集合会包含所有匹配邮件,而不是只有已经返回的那一页。小输出不等于小语义范围。它不能证明每一种服务器都采用同样的内存布局,却足以否定“网络上只回来几行,所以后台只记几行”的推断。
拒绝必须落到那个视图上
NOUPDATE 携带原命令标记,并非装饰。在同一条连接上,客户端可能有多个搜索和多个持续视图。它需要知道,服务器拒绝的是哪一项新请求,哪个视图不能被标记为持续更新。
这项拒绝发生在命令处理期间。不能把它扩大解释为“所有已经接纳的更新都被取消”,也不能把它缩小成一条看完就丢的提示。对用户而言,最重要的结果不是日志里有没有错误文字,而是界面是否仍在暗示它会自动跟上变化。
原命令的标记在第一次查询结束后仍然有作用。服务器后续发送的结果增减,要靠它关联到正确搜索。因此,RFC 5267 要求拒绝复用仍被某个更新上下文占用的命令标记,以 BAD 响应阻止混淆。
这个标记不是长期业务编号,不是身份凭证,也不是某个邮箱的永久名字。它承担的是连接内持续关联。把“一次命令已结束”误认为“这个标记已经失去作用”,就可能让之后的信息失去可靠归属。这样的错误不是靠给视图换一个更漂亮的名称就能修复的。
维持一份列表,不等于保存一部历史
接纳 UPDATE 之后,更新内容也不能任意重排。客户端必须按照收到的顺序处理 ADDTO 和 REMOVEFROM,哪怕多个数据项在同一个响应内;服务器则必须让这些操作维持所请求的结果顺序。
使用邮件序号时,更新与邮箱基础通知之间还有先后依赖。新到达或追加邮件导致的 ADDTO,必须在 EXISTS 通知之后发送;因清除邮件而产生的 REMOVEFROM,必须在相应 EXPUNGE 之前发送。原因在于序号的含义会随着邮箱变化而改变,客户端要先用仍然有效的编号处理对应动作。
这类顺序约束服务于当前结果的维护,不等于系统保存了全局有序、永久可追溯的业务事件。一个“未答复”视图可以帮助人看清现在还有哪些邮件,不会因此自动记录每一次责任转移、每一次人工决定,以及客户端不在线时发生过什么。
规范要求更新应该及时交付,但没有在这里规定适用于所有部署的固定毫秒上限。产品若另行承诺一个明确的新鲜度时限,就需要证明自己如何观察、维持并在失守时说明那个时限,不能只把 RFC 编号放进功能说明。
持续更新还会结束。邮箱不再被选中时,相应更新停止;客户端也可以用 CANCELUPDATE 指明一个或多个原命令标记,停止它们的更新。服务器可以释放相关资源,客户端之后仍可再次搜索。重新建立上下文可能是恢复当前视图的简单办法,却不等于找回断档期间所有重要变化。
缓存是实现选择,过期答案不是
服务器可以缓存搜索或排序结果,无论客户端是否给出了 CONTEXT 提示。但 RFC 5267 要求,有缓存与没有缓存时,对外表现必须相同。邮箱变化后,缓存需要被维护为适用状态,或者被丢弃;不能因为内部保留了一份旧结果,就把那份旧结果升格为对外有效的快照。
这使优化保有很大的空间,也有明确边界。服务器可以用内存减少重复计算,可以丢弃结果后重新搜索,也可以为后续部分查询记录进度。它不能把内部优化带来的陈旧性悄悄转嫁成客户端应当接受的新语义。
文件的实现讨论也说明,单看缓存占用容易误判。未排序上下文可能只需保存搜索程序,再比较一封邮件的旧、新标记是否改变匹配结果;排序上下文则更可能需要结果缓存。处理已经清除的邮件时,如果旧内容不再可用,还可能必须依赖先前保存的信息。
这些描述不是对某家产品的性能测量。它们揭示的是一组成本替换关系:删掉缓存可能减少内存,也可能增加重新发现结果的工作;保留缓存可以加快响应,却需要承担维持其正确性的成本。任何一项资源数字都不宜独自代表整个持续视图。
更新文件留下的两种警告
2009 年的 RFC 5465 引入更广泛的 NOTIFY 控制。在服务器同时支持相关上下文能力时,它还扩展 UPDATE,让客户端请求新邮件进入结果集合时附带哪些 FETCH 属性。
NOTIFY 有自己的拒绝和中止方式。请求过于昂贵时,可以在最初被带有 NOTIFICATIONOVERFLOW 的已标记 NO 拒绝;已经运行后,服务器若无法或不愿承担所要求的通知量,可以发出未标记的 OK 与 NOTIFICATIONOVERFLOW,并按收到 NOTIFY NONE 的方式处理。它们不能与 NOUPDATE 的命令级接纳决定混为一谈。本文也不据此推断某种通知关闭必然取消所有其他扩展建立的上下文。
更值得注意的是 RFC 5465 的勘误记录。已验证的技术勘误 2318 修正了一个先查询状态、再建立通知的示例。正确顺序应先安排通知,再查询状态,以免两项操作之间发生的变化落在观察空档中。这里有实质性的交接问题,不能靠“两个命令最后都成功”来排除。
另一条技术勘误 4833 仍是 Reported,指出文件两处对 FETCH 与 ESEARCH 相对次序的要求冲突。不能把这条报告写成已经定案的统一顺序。提交者关于部署实现的说明属于历史证言,不是今天的产品普查。已验证的几条编辑勘误修正的是拼写、括号和章节引用,也没有替代尚未解决的技术判断。
本次研究访问时,RFC 5267 的勘误查询 与 RFC 9394 的勘误查询 均没有返回匹配记录。这只描述检索结果,不意味着实际实现没有缺陷。本文没有连接真实邮箱执行命令,也没有测量供应商行为。
“实时”最终是一笔跨团队的账
持续更新有价值,正因为它让客户端不必反复从头查询。但是,合法登录的用户也可能建立大量上下文,耗尽资源。RFC 5267 因而讨论数量限制、实现策略和使用记录,而没有把经过身份验证等同于无限使用许可。
拒绝请求可以是维持服务的正常手段。问题不在于是否出现拒绝,而在于拒绝之后,谁负责改变对用户的承诺。服务器保护了处理能力,客户端交付了第一屏正确结果,两边都可能认为自己的工作做完了,用户却仍然在依据一个未必持续更新的界面安排业务。
Lu Heng 在关于代理问题的文章中强调控制权与后果承担之间的结构性关系。应用到这里,并不是断言邮件开发者存在某种动机,而是要求把承诺持续观察的人、批准持续资源的人和承担错误判断后果的人放到同一张责任图中。
他在关于 BTW 为何存在的文章中主张描述现实而非倡导阵营。一个清楚表达限制的协议不必被写成服务商的借口,一个未获接纳的请求也不必被包装成灾难。准确的起点是承认:第一批结果已经拿到,与以后仍有人替你盯着,是两项需要分别兑现的服务。
会员简报
档案背景详情
使用相应会员等级登录,即可解锁完整简报与来源注释。
仅限 Strategic Circle
Strategic Circle
所有读者均可浏览。加入并登录后可解锁档案简报。
加入 Strategic Circle仅限 Leadership Alliance
Leadership Alliance
符合条件的 IP 资产所有者和管理层可登录查看 Leadership Alliance 简报。
加入 Leadership Alliance
