摘要

  • RFC 9610 要求存续的 ContactCard 至少属于一个 AddressBook,但允许它同时属于多个地址簿;移除一项归属关系因此不等于销毁卡片。
  • 群组保存成员 UID。若用户暂时失去对相应卡片的访问,成员会从当前群组视图消失,但无法解析的 UID 仍应保留,访问恢复后成员可以重新出现。
  • 可辩护的删除结论需要账户、服务器对象 ID、UID、操作前的完整归属集合、/set 参数与响应,以及随后在正确账户中的权威查询。

一个空白页面越过了它的证据边界

项目结束后,管理员删除了为项目建立的共享地址簿。客户端返回空白列表,审计记录也写下“联系人已删除”。几个月后,其中一位供应商又出现在运营地址簿里。直觉会把这解释成恢复失败、备份回灌或违规保留;RFC 9610 提供了更基础的答案:此前发生的可能只是集合关系被移除。

在规范的数据模型中,AddressBook 是有名称的 ContactCard 集合。一张卡片可以同时属于“项目”“值班”和“续约”三个地址簿。使用 onDestroyRemoveContents:true 销毁项目簿时,服务器会把项目簿从相关卡片的 addressBookIds 中移除;只有在卡片不再属于任何其他地址簿时,它本身才会被销毁。

因此,界面清空、请求成功、对象继续存在可以同时为真。把这三个事实压缩成“已删除”,不是简化,而是把视图状态、关系变化和对象生命周期混成一个未经证明的结论。

地址簿组织卡片,但不天然拥有卡片

支持 JMAP 联系人的 Account 可以包含零个或多个 AddressBook,每个地址簿包含零个或多个 ContactCard。存续卡片的 addressBookIds 必须非空。这个结构刻意允许一张卡片被多个工作场景共同使用,而不是为每个视图制造一份可能漂移的副本。

这样的最小共享规则很重要。协议规定各实现都能理解的归属与同步状态,产品可以自行把它画成文件夹、标签或工作区。但界面隐喻不能扩大操作的权力:从一个文件夹消失,并不会自动取得在其他集合中销毁对象的授权。

运行中的代码所需的是清晰的边界。移除一条归属边,回答的是“这张卡片是否还在这个地址簿”;销毁对象回答的是“这张卡片是否还存在于这个账户”。二者不能共用一张没有范围说明的绿色成功徽章。

onDestroyRemoveContents 是有条件的级联

onDestroyRemoveContents 默认是 false。地址簿非空时,服务器会以 addressBookHasContents 拒绝销毁。这个错误证明当前参数下的集合销毁没有完成,却不能证明卡片永远保留,更不能说明其他账户或外部系统的状态。

参数为 true 时,服务器先从卡片的归属集合中移除目标地址簿;某张卡片只有在失去最后一个地址簿归属时才被销毁。于是同一次方法调用可能产生三类结果:地址簿被销毁、多归属卡片继续存续、失去最后归属的卡片被销毁。

审计若只记录“开启级联”,就丢掉了决定结果的前置条件。真正需要保存的是每张卡片操作前的完整 addressBookIds、请求对象、参数值、服务器返回的 destroyed、notDestroyed、updated 与错误字段。命令名称是意图,不是结果。

服务器 id 与 JSContact uid 维护不同的身份连续性

ContactCard 有不可变、由服务器赋值的 id,也有 JSContact 定义的 uid。RFC 9610 明确允许二者不同,并要求同一 Account 内不得有两个 ContactCard 使用相同 UID。

服务器 ID 指向 JMAP 方法实际读取或修改的对象。UID 则用于联系人表示中的连续性,也是群组成员保存的引用。日志只留显示名称,两种身份都丢失;只留 UID,无法证明哪个账户里的哪个服务器对象被改动;只留服务器 ID,又可能看不到群组或跨可访问账户通过 UID 维持的连续性。

可靠证据应同时保存二者,但也必须承认它们都不是现实中的人。销毁某个账户内的一张卡片,不会自动删除邮件、导出文件、其他设备、CRM、备份或被描述的人。

群组会保存当前用户看不见的线索

群组类型的 ContactCard 在 members 中保存一组 UID。客户端会在当前可访问且支持 JMAP Contacts 的账户中寻找匹配卡片。某个 UID 暂时找不到时,客户端应在显示上忽略它,却仍然保留这个 UID。

规范给出了直接示例:用户把共享地址簿中的联系人加入私人群组,随后暂时失去对共享地址簿的访问。由于 UID 无法解析,成员从群组中消失;权限恢复后,保留的 UID 再次找到对应卡片,成员便重新出现。

这里没有必然发生恢复写入。贯穿中断期的连续性证据一直存在,只是对当前主体不可解析。中断期间的截图最多证明“这个主体此刻看不到”,不能证明对象被销毁、UID 被清除,也不能证明其他主体同样不可见。

查询无结果只回答了查询范围内的问题

inAddressBook 过滤条件要求卡片属于指定地址簿。某张卡片没有命中,只能说明它不在该过滤范围;它仍可能属于另一个地址簿,或位于调用者当前无法访问的表面。

JMAP 的状态字符串与 /changes 能帮助客户端追踪变化,但前提是保留旧状态、检查创建、更新、销毁的对象 ID,并用正确 Account 下的 /get 消除不确定性。“列表里少了一行”不能替代这条证据链。

即便服务器明确确认 ContactCard 已销毁,结论也只覆盖该 JMAP Account 中的这个对象。它不能证明其他账户、复制卡片、备份、邮件历史或外部应用中的数据已经消失,更不能自动等同于法律意义上的擦除。

默认地址簿请求也说明意图不等于结果

onSuccessSetIsDefault 会在其他创建、更新和销毁全部成功后尝试设置默认地址簿。但如果 ID 不存在,或服务器政策不允许变更,服务器必须忽略该请求,不为这部分返回错误,并保留原默认簿。

这不是删除机制,却揭示同一条证据纪律:发出请求只能证明尝试,不能证明服务器最终状态。客户端必须读取返回的服务器属性,或再次获取当前状态后才能向用户宣告成功。

删除凭证必须是一条可复核的链

第一步是明确 Account、ContactCard 的服务器 id 与 UID。第二步保存操作前完整的 addressBookIds,以及群组中任何相关 UID 引用。第三步记录究竟销毁 AddressBook 还是直接销毁 ContactCard,并保留 onDestroyRemoveContents 的准确值。

随后保存方法响应、新状态和后续权威查询。最终结论应限定为四种之一:已从这个地址簿移除;当前对这个主体不可解析;这个账户内的 ContactCard 已销毁;证据不足。RFC 9610 无权替系统声称“这个人已从所有地方被擦除”。

现实层分离不是文字游戏。视图、关系、对象和外部世界分别控制不同的决策与风险。只有让每一层使用自己的凭证,删除报告才不会在重新出现的联系人面前崩塌。

来源