JMAP 协议:替代 IMAP 的现代邮件同步协议
协议定位与设计背景
JMAP 全称 JSON Meta Application Protocol,是一套面向客户端与服务端数据同步的通用协议,邮件只是其定义的第一个应用类型。基础协议由 RFC 8620 规定,邮件部分由 RFC 8621 规定,两份规范于 2019 年发布,前者在七月、后者在八月。协议起源于 FastMail 的工程实践,先在自有邮件系统中运行多年,再提交 IETF 完成标准化,规范中的取舍带有明显的实现反馈痕迹。从设计意图看,JMAP 面向移动与网页环境优化,目标是在有限网络资源下提供一致、可批量执行的数据访问接口。
它要解决的核心问题,是传统邮件同步协议在长连接、会话状态与命令体系上累积的复杂度。IMAP 面向早期网络条件设计,客户端需要维持有状态连接并逐条执行命令;JMAP 将同步建模为数据版本的比较,把状态管理从命令交互中剥离出来。这一方向上的差异贯穿后续的数据模型、增量机制与推送设计。
会话发现与请求模型
客户端接入 JMAP 的第一步是会话发现。客户端通过 .well-known/jmap 端点获取一个 Session 资源,其中包含 apiUrl、uploadUrl、downloadUrl、eventSourceUrl、可用能力列表与账号列表。这些地址与能力信息确定了后续请求的去向与可用方法。取得 Session 之后,客户端即可按能力与账号发起业务请求。
之后的每个请求都是自包含的独立 HTTPS 请求,携带认证信息、accountId 与目标方法。服务端不为客户端维持会话,请求之间没有先后依赖,前一个请求的上下文不会隐式保留到后一个请求。这一特征是传输层无状态的具体含义:服务端不保存连接期的交互状态,每个请求自行说明身份、账号与操作目标。
两层状态的区分
传输层无状态不等于协议整体无状态,两层状态需要分开理解。传输层不维持会话,数据层则维护版本:每种数据类型对应一个 state 字符串,客户端保存上次同步到的 state,服务端在响应中返回新的 state。state 可以理解为该类型数据的版本号,客户端据此判断本地副本是否落后于服务端。
因此,将 JMAP 称为无状态协议是错误的。准确的表述是:传输层无状态,数据层为强状态设计。数据层的版本机制是增量同步与推送通知的基础,缺少这一层状态,客户端无法判断哪些对象发生了变化。
统一方法族
每种数据类型共享同一套方法命名,这是 JMAP 在命令层面的结构性安排。Foo/get 用于取对象,Foo/changes 用于取变更,Foo/set 用于增删改,Foo/query 用于条件查询,Foo/queryChanges 用于查询结果增量,Foo/copy 用于跨账号复制。同一套命名覆盖读、写、查询与复制四类操作,新数据类型接入时沿用既有方法族。
统一命名降低了客户端的学习与实现成本。客户端只需掌握一套方法语义,即可处理 Mailbox、Email 等不同类型,而不必像 IMAP 那样记忆各有差异的命令与参数组合。从工程实现看,方法族的一致性也使通用的批量调度、错误处理与缓存逻辑可以跨类型复用。
邮件数据模型
RFC 8621 定义的邮件相关数据类型包括 Mailbox、Thread、Email、EmailSubmission、Identity 与 VacationResponse。另有 EmailDelivery 类型仅作为推送状态存在,不提供任何操作方法;SieveScript 属于独立的 JMAP for Sieve Scripts 规范,即 RFC 9661,2024 年 9 月发布,不在 RFC 8621 的定义范围内。各类型的方法支持程度并不一致,需要按类型区分。
Thread 由服务端自动维护,客户端只读,仅提供 Thread/get 与 Thread/changes 两个方法,没有 query,也没有 set。线程分组由服务端依据消息头与主题等信息完成,客户端只能读取线程内的邮件列表,不能直接创建、删除或重组线程。这一限制与 Mailbox、Email 支持完整方法族的情况不同。
SearchSnippet 不属于数据类型。它是搜索结果附带的高亮片段对象,仅提供 SearchSnippet/get 一个方法,自身没有 id 属性,也没有对应的 state。片段内容由消息内容派生,算法可能随版本变化,重复获取得到的结果允许不同,因此规范不为其维护版本与变更机制。
对象可变性边界
Email 对象在内容层面基本不可变。正文、附件、收发件人、主题等属性在创建后不可修改,可修改的只有 keywords 与 mailboxIds 两类属性。keywords 承载已读、已回复等标记,mailboxIds 记录邮件所属文件夹;其余属性一经写入即固定。
对已发送邮件修改内容,语义上等价于创建新对象并销毁旧对象。规范示例中,草稿内容的修订同样采用新建对象加销毁旧对象的方式完成,草稿状态由 keywords 中的 $draft 关键字标记。需要说明的是,RFC 8621 未定义独立的 isDraft 属性,草稿身份通过关键字表达。
不可变设计使客户端缓存可以按 id 直接复用,不必处理同一 id 下的多版本冲突。这一取舍的代价转移到服务端:服务端需要维护对象的版本与变更记录,支撑增量比对与历史回溯。在工程上,这一安排更接近缓存失效策略中以稳定标识定位对象的做法,版本冲突由服务端侧的变更记录承担。
两级增量同步
增量同步采用两级拉取结构,这是 JMAP 同步模型的核心。客户端持有上次同步到的 state,调用 Foo/changes 得到 created、updated、destroyed 三个 id 数组以及新的 state。changes 响应只返回 id,不返回对象内容;随后客户端再调用 Foo/get 获取实际数据,并可通过 updatedProperties 参数限定只取变更过的字段。
变更列表与对象内容分离,使变更列表可以被缓存、合并与批量处理。多个来源的变更通知可以先汇总为一份待处理 id 集合,再统一拉取内容。这一设计的代价是最坏情况下需要多一次往返:先取变更列表,再按 id 取对象,弱网环境下两次请求都需成功才能完成同步。
增量机制存在明确的历史边界。若客户端持有的 state 早于服务端保留的变更历史,服务端返回 cannotCalculateChanges,无法推算中间的变更序列。此时客户端只能退回全量同步,重新拉取目标类型的完整数据。
批量调用与引用
单个 HTTP 请求可以包含多个方法调用,按数组顺序依次执行。后续调用通过 resultOf、name、path 三个字段反向引用前序调用的结果,前序调用返回的数据可以被后序调用直接取用。创建对象时,客户端可生成 creationId 作为临时标识,被同请求内的后续调用引用;服务端在响应中将 creationId 映射为真实 id。
批量执行以调用为粒度隔离失败。单个方法调用失败不影响同一请求内的其他调用,错误按调用粒度返回,客户端可以根据各调用的结果分别处理。这一机制使建文件夹、移动邮件、标记已读这类原本需要多轮往返的复合操作,可以在一次往返内完成。
推送机制
推送存在两条独立路径,分别对应不同的在线状态。EventSource 基于 Server-Sent Events,由服务端维持长连接下发通知,适用于持续在线的桌面与网页客户端。PushSubscription 由客户端注册外部回调地址,服务端在数据变更时向该地址推送,配合 APNs、FCM 等系统级推送服务,客户端无需维持长连接。
两条路径下发的都是 StateChange 对象,只携带各数据类型的 state,不携带邮件或文件夹内容。客户端收到通知后,仍需自行调用 changes 方法与本地 state 对比,再决定拉取哪些对象。推送通道因此只承担唤醒与版本通知,不承担数据传输;数据本身始终经由 get 类方法获取。
EmailDelivery 类型在推送中承担细分职责。该类型的 state 仅在新邮件进入存储时变更,标记已读、删除等操作不会触发其变化;受限环境中的客户端可单独订阅该类型,优先拉取新到达的邮件。
附件传输与多账号
二进制附件通过独立的上传与下载通道传输,不进入结构化的方法调用体。上传内容经由 uploadUrl 提交,服务端返回 blobId,后续方法调用以 blobId 引用该二进制对象;下载则经由 downloadUrl 按 blobId 取回。这一安排避免在 JSON 方法体中嵌入二进制数据,结构化字段与二进制内容保持分离。
一个会话可以包含多个账号,每个方法调用显式携带 accountId。共享邮箱、多域名场景下,客户端无需像 IMAP 那样为不同账号建立多条连接,而是在同一会话内按调用指定目标账号。能力与账号列表在 Session 资源中一并给出,客户端据此判断各账号可访问的数据类型。
与 IMAP 的机制对比
IMAP 是有状态的长连接协议,状态与编号都绑定在会话之上。sequence number 是会话内的临时编号,任何 EXPUNGE 都会导致其后的编号整体重排,客户端实际需要依赖 UID 才能稳定定位邮件。编号错位问题迫使客户端在缓存与变更处理中额外维护编号到 UID 的映射。
IMAP 的增量同步依赖 CONDSTORE、QRESYNC 等扩展,服务端实现程度不一;推送依赖 IDLE 维持长连接,断线之后需要重新对齐状态。移动网络下连接频繁中断,每次重连都要重新协商并核对本地缓存与服务端的差异。
IMAP 的设计在其历史条件下是合理的。早期带宽稀缺、客户端存储有限,把状态集中在服务端、按需取用,是节省带宽与本地资源的有效选择。移动网络改变了这一前提,往返成本与连接维持成本上升,长连接与会话状态带来的负担才逐步显现。
协议自身代价
JMAP 的取舍并非没有代价。JSON 表达比 IMAP 的紧凑文本响应占用更多字节,同样一封邮件的元数据,序列化后的体积更大。请求独立意味着每次业务请求都伴随连接建立与 TLS 握手开销,这部分成本需要依靠 HTTP/2 的连接复用缓解。
搜索条件以 JSON 结构表达,复杂查询的表达力不如 IMAP 的 SEARCH 语法,部分条件组合需要服务端扩展支持。服务端还需要为每个账号保留变更历史,以支撑 changes 推算,存储与计算成本随账号规模上升。这些成本由服务端与连接层分担,换取的是客户端侧状态模型的简化。
采纳现状与成因
JMAP 发布至今,主流邮件服务商未提供原生支持,实际部署集中在少数服务端实现上,包括 FastMail 自家系统、Cyrus 与 Stalwart。扩展规范推进缓慢,JMAP for Contacts 长期停留在草案阶段,直到 2024 年 12 月才以 RFC 9610 形成正式标准。邮件主链路之外的应用类型,标准化节奏明显落后于基础协议。
采纳缓慢的原因可以落到具体机制与投入结构。客户端生态长期稳定,迁移收益需要服务端与客户端双方同时投入才能兑现;服务端重构同步链路成本较高,且缺少直接的业务回报;多数终端用户感知不到 IMAP 的同步缺陷,来自使用侧的需求压力不足。三方因素叠加,使既有链路的维护优先级高于协议替换。
结论
协议能否被采纳,取决于迁移成本的分担方式与既有生态的锁定程度,技术层面的优劣只是其中一个变量。JMAP 在状态模型、批量能力与推送结构上的改进,属于服务端与客户端双侧改造才能兑现的收益;在缺少明确业务压力的情况下,规范层面的设计优势不足以单独推动生态迁移。评估一类新协议时,需要同时考察技术机制、部署激励与生态锁定三个维度,而不仅比较命令集合与数据表达。

浙公网安备 33010602011771号