摘要

  • IMAP消息序号表示当前邮箱中的相对位置;前面的邮件一旦被彻底删除,后续序号就会移动。UID可以跨会话保持,但它只在特定邮箱和特定UIDVALIDITY世代内有效。
  • UIDVALIDITY发生变化,等于服务器明确撤回旧标识的连续性保证。客户端必须清空相关缓存并放弃指向旧UID的待执行操作,不能让旧数字偶然命中新消息。

最危险的不是找不到,而是找到了另一个对象

一部手机在进入无信号区域前同步了收件箱。缓存里,UID 4821对应一张尚未报销的发票。用户离线时将它标记为删除。与此同时,邮件服务把邮箱迁移到另一套存储;邮件正文保住了,原来的UID元数据却没能保住。新存储重新编号,UID 4821恰好落到一封不同的邮件上。

手机重新联网时,登录身份正确,TLS连接正常,删除命令的语法也完全合法。如果服务器还沿用旧的UIDVALIDITY,双方就会把“数字相同”误当成“对象相同”。一条精确命令,反而会精确地删错邮件。

IMAP选择让这种断裂显性化。服务器提升UIDVALIDITY;客户端发现不一致后,丢弃这个邮箱的UID缓存和离线待办,不再重放那次删除。代价是重新同步,收益是用户意图不会被偷偷转嫁给另一个对象。

排在第几,只能描述眼前的队列

IMAP有两套消息编号。消息序号是当前邮箱里的相对位置。邮箱有20封邮件,序号就是1到20。它适合取一个范围,也适合做简单计算:原来有17封,服务器现在报告20封,客户端就知道末尾增加了三个位置。

但位置会随集合变化。彻底清除第6封后,原来的第7封变成第6封,其后的每一封都向前移动。一个数字可以在同一会话中先后属于不同邮件。RFC 2060在1996年的IMAP4rev1规范中明确写下这种重排;2021年的IMAP4rev2规范RFC 9051仍保留这个模型。

在线客户端可以接收服务器的变化通知,及时更新“位置—消息”映射。离线设备看不到这段过程。它回来时看到的“第6封”,只说明今天队列里的第六个位置,并不能证明这是昨天记住的那封邮件。

序号不是坏设计。把顺序坐标拿来当长期身份,才是类别错误。

1994年的IMAP4把身份从位置中拆了出来

1994年12月发布的RFC 1730定义了IMAP4。它要让远程邮箱像本地文件夹一样可操作,也要让离线客户端回来后能够重新对齐状态。为此,协议引入32位UID:它在单个邮箱中严格递增,可以不连续,并且跨会话保持。

有了UID,客户端不必每次回来都下载并比较所有正文。它可以记住“UID 4821已经读过”,也可以把一个尚未提交的标记、移动或删除操作绑定到UID 4821。

问题在于,协议的持久承诺必须落到真实存储上。许多旧邮件库最初并没有为IMAP UID预留可靠位置;非IMAP程序可能重排底层文件;管理员可能删除一个邮箱,后来又用同名创建新邮箱;恢复或迁移也可能保住消息,却丢掉编号元数据。

如果标准要求所有系统无条件永久保存UID,它会把一批现实存储拒之门外。如果允许服务器悄悄重用数字,离线操作就不再可信。IMAP采用了第三条路:允许连续性失效,但失效必须可见。

UIDVALIDITY标记的是世代,不是邮箱本身

每个邮箱都会报告一个32位UIDVALIDITY。如果早先会话中的UID不能继续保持,新值必须高于旧值。RFC建议过用邮箱创建时间,也允许使用递增计数器。具体算法不是公共含义;公共含义是“你缓存的那一代UID是否仍然存在”。

名称很容易诱发误解。RFC 2683在1999年的实现建议里专门纠正:UIDVALIDITY并不是邮箱ID。两个邮箱可以有相同的UIDVALIDITY;UID只在各自邮箱内唯一。把UIDVALIDITY和UID拼成64位,也不会自动得到服务器级、互联网级的全局标识。

长期引用真正需要的是邮箱名、UIDVALIDITY与UID三者。RFC 3501和RFC 9051进一步规定:在这个正确范围内,三元组必须始终指向服务器上的同一封不可变消息。正文、信封、内部日期、大小与结构不能悄悄换成另一封邮件;可编辑的旗标除外。邮件即便已被清除,其UID也不得在同一UIDVALIDITY下重新分配。

这条规则把“唯一”从营销式形容词变成可检验的边界:先说明在哪个邮箱、哪一代编号中唯一,再谈这个数字能做什么。

不完美的存储必须诚实地暴露不完美

改变UIDVALIDITY会让客户端重建缓存,因此规范强烈鼓励服务器保持UID。可是现实中确有无法做到的旧存储。RFC 4315定义UIDNOTSTICKY,用来告诉客户端这个存储不能提供持久UID;同一文档又明确说,新系统不应按这种方式设计。

这是一个很典型的互联网工程取舍:让正常路径高效,同时给无法守住不变量的实现一个诚实出口。出口不能是“看起来还能用”。一旦服务器丢失原UID,却继续报告原UIDVALIDITY,它省下的同步流量,实际上来自对身份连续性的虚构。

RFC 2683指出过最坏后果:客户端用过期UID删除了错误邮件。风险并不限于删除。旧UID还可能把“已读”“移动”“复制”或内容获取施加到另一封邮件上。离线重放越自动,身份边界就越不能含糊。

清空缓存不是惩罚,而是撤销旧状态的权限

RFC 4549在2006年系统说明了离线IMAP客户端的同步步骤。客户端打开邮箱后,必须先比较服务器给出的UIDVALIDITY和本地记录。若不一致,它应清空该邮箱的本地缓存,删除所有引用旧UID的待办,并把这些操作视为失败。

协议没有鼓励客户端按主题、日期或正文相似度“猜回去”。这些属性适合搜索,不足以证明旧删除命令仍然有权作用于当前对象。邮箱进入新世代后,缓存里的不仅是旧数据,也是失去证据支撑的旧意图。

失效范围被限制在单个邮箱。RFC 2683也不赞成整台服务器共用一个UID池:全局计数器更快耗尽32位空间,一次回卷还可能迫使所有邮箱同时更换UIDVALIDITY。局部命名空间既节约编号,也把故障半径收在局部。

一条可保存的链接也要带上失效条件

1997年的RFC 2192定义IMAP URL时,允许链接同时携带邮箱、UIDVALIDITY和UID。解释该链接的程序应检查当前UIDVALIDITY,用它判断保存的URL是否已经陈旧。

UIDVALIDITY不是邮件的过期时间,也不证明发件人身份、访问授权或密码学完整性。它提供的是更朴素的证据:这条引用是否仍处在当初赋予它意义的编号世代中。

一个数字很容易复制和传递。安全的长期引用还要携带“什么时候应该停止相信我”。

QRESYNC再快,也不能越过身份检查

邮箱越来越大,移动连接又经常中断,完整比对的代价开始突出。RFC 7162把CONDSTORE和QRESYNC结合起来:修改序列追踪旗标等元数据的变化,VANISHED紧凑地报告已清除UID,客户端常常能在一个往返内完成重新同步。

但客户端交给QRESYNC的第一个状态值,就是它最后知道的UIDVALIDITY。若不匹配,服务器会忽略后面的修改序列与已知UID集合。那些增量即使计算得完全准确,也属于另一代对象。

因此,协议把问题排了顺序:先问“还是不是同一批东西”,再问“它们发生了什么变化”。性能优化只能服务于身份连续性,不能替代它。

三十年后,UIDONLY尝试让相对位置退出会话

2024年5月发布的实验性RFC 9586定义UIDONLY。客户端启用后,会话不再接受消息序号命令,服务器也不再在响应中返回消息序号;UID形式和VANISHED接管相应操作。双方可以少维护一层“移动位置—稳定UID”映射。

这不是所有部署都必须采用的新基线,RFC也没有给出普及率。它的方向却很清楚:IMAP4引入UID三十年后,协议仍在减少对相对位置的依赖。

UIDONLY并没有让UID变成永恒的全球身份。UID仍属于一个邮箱和一个UIDVALIDITY世代。拿掉易变坐标,只是让引用更明确,并没有取消世代边界。

标识符能长久,正因为协议允许它诚实地死亡

IMAP没有把所有权力交给一个数字。服务器掌握邮件存储,因此由它声明UID连续性是否幸存;客户端掌握缓存和延迟执行的用户意图,因此由它在世代改变时撤销旧状态的资格。任何一方都不能把便利当作证据。

公共协议保持得很薄:它不规定唯一存储引擎、迁移方法或客户端数据库,只规定不变量、断裂信号与安全反应。运行中的系统仍可各自选择实现,但不能让旧编号在无声中取得支配新对象的权力。

UID之所以能跨过会话,是因为UIDVALIDITY能明确宣布:它没有跨过这次邮箱重生。

来源与边界

1994年的设计来自RFC 1730;消息序号、UID及三元组不变量来自RFC 2060、RFC 3501与RFC 9051;实现经验来自RFC 2683;持久链接、UIDNOTSTICKY、离线同步、QRESYNC和UIDONLY分别来自RFC 2192、RFC 4315、RFC 4549、RFC 7162与RFC 9586。这些文档不能证明今天各机制的部署比例、某个具体产品的行为、邮件真实性、密码学完整性,也没有规定唯一的UIDVALIDITY生成算法。