非官方中文译本声明:本页为 IETF RFC 8620《The JSON Meta Application Protocol (JMAP)(JSON 元数据应用协议)》中文译本,由 ztpop.net 整理翻译,仅供学习参考。RFC 文档由 IETF 发布,受 BCP 78 与 IETF 信托法律条款约束;本译本保留原文编号与结构,权威性以英文原文为准。英文原文见 rfc-editor.org/rfc/rfc8620

RFC 8620:JSON 元数据应用协议(JMAP)

摘要

本文档规定了一个协议,供客户端高效地查询、获取和修改基于 JSON 的数据对象,并支持变更推送通知、快速重新同步,以及带外(out-of-band)二进制数据的上传与下载。

本备忘录的状态

本文是一份互联网标准跟踪(Standards Track)文档。

本文是互联网工程任务组(IETF)的成果,代表 IETF 社区的共识,已经过公开评审,并由互联网工程指导组(IESG)批准发布。有关互联网标准的更多信息见 RFC 7841 第 2 节。

关于本文当前状态、任何勘误以及如何提供反馈的信息,可访问 https://www.rfc-editor.org/info/rfc8620

Copyright (c) 2019 IETF 信托及被列为文档作者的个人。保留所有权利。

本文受 BCP 78 以及 IETF 信托的《IETF 文档相关法律规定》(https://trustee.ietf.org/license-info)约束,以本文档发布之日生效的版本为准。请仔细审阅这些文档,因为它们描述了您就本文档所享有的权利与限制。从本文档中提取的代码组件必须包含《简化 BSD 许可证》文本(见信托法律条款第 4.e 节),并按该许可证的描述"不提供任何担保"。

目录

1. 引言

JSON 元数据应用协议(JMAP)用于在客户端与服务器之间同步数据,例如邮件、日历或联系人。它针对移动与 Web 环境进行了优化,旨在为不同的数据类型提供一致的接口。

本规范描述的是数据同步的通用机制。其他规范定义了可通过 JMAP 同步的不同数据类型的模型。

JMAP 的设计旨在高效利用有限的网络资源。多个 API 调用可以在单个请求中批处理,从而减少往返次数并改善移动设备的电池续航。推送连接消除了轮询的需要,而一种高效的增量更新机制确保传输的数据量最小。

JMAP 设计为可水平扩展到非常多的用户。这得益于登录后面向用户的独立端点、二进制数据与结构化数据的分离,以及一种不允许账户之间存在数据依赖的共享数据模型。

1.1. 符号约定

本文档中的关键词"MUST(必须)"、"MUST NOT(不得)"、"REQUIRED(要求)"、"SHALL(应)"、"SHALL NOT(不应)"、"SHOULD(应该)"、"SHOULD NOT(不应该)"、"RECOMMENDED(推荐)"、"NOT RECOMMENDED(不建议)"、"MAY(可以)"和"OPTIONAL(可选)",当且仅当它们以如这里所示的全大写形式出现时,才应按照 BCP 14 [RFC2119] [RFC8174] 中的描述进行解释。

本规范使用的基础格式是 JSON。因此,"object(对象)"和"array(数组)"这两个术语,以及四种基本类型(字符串、数字、布尔值和 null),应按照 [RFC8259] 第 1 节的描述进行解释。除非另有说明,所有属性名和属性值都是大小写敏感的。

本文档中的部分示例包含用于说明目的的"局部"JSON 文档。在这些示例中,使用三个句点"..."表示为了简洁而被删除的文档部分。

为了符合出版要求,在长 JSON 字符串内部插入了换行,后续行缩进显示。要构成有效的 JSON 示例,字符串内部的任何换行都必须替换为空格,并去除换行之后的任何其他空白。

除非另有说明,API 交换的示例仅显示 Request 对象的 methodCalls 数组或 Response 对象的 methodResponses 数组。为简洁起见,Request/Response 对象的其余部分被省略。

本文档为所有 JSON 值给出了类型签名。使用以下约定:

其他类型也可能被给出,其表示形式在本文档的其他位置定义。

对象属性除类型签名外,还可能带有一组属性。它们的含义如下:

1.2. Id 数据类型

所有记录 id 都由服务器分配且不可变。

当给出"Id"作为数据类型时,它表示一个"String(字符串)",大小至少为 1 个八位组、最多为 255 个八位组,并且只能包含 [RFC4648] 第 5 节定义的"URL 及文件名安全"base64 字母表(不包括填充字符"=")中的字符。这意味着允许的字符是 ASCII 字母数字字符("A-Za-z0-9")、连字符("-")和下划线("_")。

这些字符在几乎所有上下文中使用都是安全的(例如文件系统、URI 和 IMAP atom)。出于最大安全性考虑,服务器还应遵循防御性的分配策略,以避免在可能存在 glob 补全或数据类型检测的地方产生风险(例如在文件系统或电子表格中)。尤其应当明智地避免:

解决这些问题的一个好办法是为每个 id 加上单个字母前缀。

1.3. Int 与 UnsignedInt 数据类型

当给出"Int"作为数据类型时,它表示一个范围在 -2^53+1 <= value <= 2^53-1 内的整数,即存储在浮点双精度数中的整数的安全范围,表示为 JSON 的"Number(数字)"。

当给出"UnsignedInt"作为数据类型时,它表示一个"Int",其取值必须在 0 <= value <= 2^53-1 范围内。

1.4. Date 与 UTCDate 数据类型

当给出"Date"作为类型时,它表示一个采用"date-time"格式 [RFC3339] 的字符串。为确保规范化形式,"time-secfrac" 如果为零则必须省略,字符串中的任何字母(例如"T"和"Z")必须为大写。例如 "2014-10-30T14:12:00+08:00"。

当给出"UTCDate"作为类型时,它表示一个"Date",其"time-offset"部分必须为"Z"(即必须采用 UTC 时间)。例如 "2014-10-30T06:12:00Z"。

1.5. JSON 作为数据编码格式

JSON 是 [RFC8259] 中规定的基于文本的数据交换格式。[RFC7493] 中定义的互联网 JSON(I-JSON)格式是其严格子集,增加了一些限制以避免可能产生混淆的场景(例如,它要求对象不得有两个同名的成员)。

从客户端发往服务器、或从服务器发往客户端的所有数据(二进制文件的上传/下载除外)都必须是符合该 RFC 的有效 I-JSON,因此是大小写敏感的,并以 UTF-8 [RFC3629] 编码。

1.6. 术语

1.6.1. 用户

用户是通过 JMAP 访问数据的个人。用户拥有一组权限,决定了其能够看到的数据。

1.6.2. 账户

账户是一组数据的集合。单个账户可以包含任意一组数据类型,例如一组邮件、联系人和日历。大多数 JMAP 方法都带有一个必需的"accountId"参数,用于指定操作在哪个账户上进行。

账户与用户并不相同,尽管通常一个主要账户直接属于该用户。例如,你可能拥有一个包含某个群组或企业数据的账户,多个用户可以访问它。

一组凭证可以提供对多个账户的访问,例如,如果另一位用户正在与经过认证的用户共享其工作日历,或者存在一个用于支持台收件箱的群组邮箱。

在发生严重内部错误时,服务器可能不得不重新分配 id,或做出其他违反某账户标准 JMAP 数据约束的操作。在这种情况下,服务器上的数据不再与客户端此前可能缓存的数据兼容。服务器必须将此视为该账户已被删除、随后以新的账户 id 重新创建。客户端随后将被迫丢弃带有旧账户 id 的任何数据,并从头重新获取所有数据。

1.6.3. 数据类型与记录

JMAP 为创建、检索、更新和删除各类对象提供了统一的接口。一个"数据类型"是一组具名、带类型的属性的集合,就像数据库表的模式一样。数据类型的一个实例称为一条"记录"。

记录的 id 不可变,并由服务器分配。该 id 在同一账户内对同类型的所有记录必须是唯一的。id 可能在跨账户时冲突,或在同账户内对两种不同类型的记录冲突。

1.7. JMAP API 模型

JMAP 使用 HTTP [RFC7230] 来暴露 API、推送、上传和下载资源。所有 HTTP 请求都必须使用"https://"方案(基于 TLS 的 HTTP [RFC2818])。所有 HTTP 请求都必须经过认证。

经过认证的客户端可以获取用户的 Session(会话)对象,其中包含有关服务器可提供的数据与能力的详细信息,如第 2 节所示。客户端随后可以通过以下方式与服务器交换数据:

  1. 客户端可以向服务器发起 API 请求以获取或设置结构化数据。该请求由一组有序的方法调用组成。这些方法由服务器处理,随后服务器返回一组有序的响应。这在第 3、4、5 节中描述。
  2. 客户端可以从服务器下载或向服务器上传二进制文件。这在第 6 节详述。
  3. 客户端可以连接到服务器上的推送通道,以在数据发生变化时收到通知。这在第 7 节解释。

1.8. 厂商特定扩展

各个服务商都会有一些希望通过 JMAP 暴露的自定义特性。这可能表现为规范之外的额外数据类型和/或方法、JMAP 方法的额外参数,或现有数据类型上的额外属性(这些属性也可能出现在以属性名为参数的方法中)。

服务器可以通过将标识符包含在 capabilities(能力)对象中来通告其支持的自定义扩展。厂商扩展的标识符必须是属于该厂商拥有的域的 URL,以避免冲突。该 URL 应当能解析到描述该扩展所做变更的文档。

客户端必须通过在该 Request 对象的"using"数组中传入相应的能力标识符来选择启用(opt in)某个扩展,如第 3.3 节所述。服务器必须只遵循被选择启用的规范,并在处理请求时表现得好像它没有实现任何其他东西一样。这是为了确保与不知道某个特定自定义扩展的客户端之间的兼容性,以及与未来版本 JMAP 的兼容性。

2. JMAP 会话资源

连接到 JMAP 服务器需要两样东西:

  1. JMAP 会话资源的 URL。这可以直接向用户索取,也可以根据用户名域自动发现(见下文第 2.2 节)。
  2. 用于认证的凭证。如何获取凭证不在本文档范围内。

对 JMAP 会话资源发起的成功且经过认证的 GET 请求必须返回一个 JSON 编码的 *Session* 对象,给出在给定这些凭证的情况下,服务器可向客户端提供的数据与能力详情。它具有以下属性:

capabilities 对象必须包含一个名为"urn:ietf:params:jmap:core"的属性。该属性的值是一个对象,必须包含以下关于服务器能力的信息(给出了建议的最小值限制,以使客户端能高效利用网络):

未来能力的规范将在 capabilities 对象上定义它们自己的属性。

如第 1.8 节所述,服务器可以通告厂商特定的 JMAP 扩展。为避免冲突,厂商特定扩展的标识符必须是厂商拥有的域下的 URL。客户端必须选择启用它希望使用的任何能力(见第 3.3 节)。

服务器在上述 capabilities 对象中通告其支持的完整能力列表。如果某一能力定义了新方法,那么当用户可以使用这些方法操作本账户时,服务器必须将其包含在 accountCapabilities 对象中;当用户无法使用这些方法操作本账户时,服务器不得将其包含在 accountCapabilities 对象中。

例如,你可能可以访问自己的包含邮件、日历和联系人数据的账户,以及一个仅有联系人数据(例如企业通讯录)的共享账户。在这种情况下,第一个账户的 accountCapabilities 属性将包含类似 "urn:ietf:params:jmap:mail"、"urn:ietf:params:jmap:calendars" 和 "urn:ietf:params:jmap:contacts" 的内容,而第二个账户将仅包含最后这一项。

试图在某一不支持该能力的账户上使用该方法所定义的方法,将以"accountNotSupportedByMethod"错误被拒绝(见第 3.6.2 节"方法级错误")。

为确保未来的兼容性,Session 对象上可能包含其他属性。客户端必须忽略任何它未预期的性质。

实现者必须注意避免在 HTTP 层不当缓存 Session 对象。由于客户端只应在检测到发生更改时(通过 API 响应的 sessionState 属性)才重新获取,建议完全禁用 HTTP 缓存,例如通过在响应上设置 "Cache-Control: no-cache, no-store, must-revalidate"。

2.1. 示例

在下面的 Session 对象示例中,用户可通过 JMAP 访问自己的邮件和联系人,以及对另一用户共享邮件的只读访问。服务器正在通告自定义的 "https://example.com/apis/foobar" 能力。

{
  "capabilities": {
    "urn:ietf:params:jmap:core": {
      "maxSizeUpload": 50000000,
      "maxConcurrentUpload": 8,
      "maxSizeRequest": 10000000,
      "maxConcurrentRequest": 8,
      "maxCallsInRequest": 32,
      "maxObjectsInGet": 256,
      "maxObjectsInSet": 128,
      "collationAlgorithms": [
        "i;ascii-numeric",
        "i;ascii-casemap",
        "i;unicode-casemap"
      ]
    },
    "urn:ietf:params:jmap:mail": {}
    "urn:ietf:params:jmap:contacts": {},
    "https://example.com/apis/foobar": {
      "maxFoosFinangled": 42
    }
  },
  "accounts": {
    "A13824": {
      "name": "john@example.com",
      "isPersonal": true,
      "isReadOnly": false,
      "accountCapabilities": {
        "urn:ietf:params:jmap:mail": {
          "maxMailboxesPerEmail": null,
          "maxMailboxDepth": 10,
          ...
        },
        "urn:ietf:params:jmap:contacts": {
          ...
        }
      }
    },
    "A97813": {
      "name": "jane@example.com",
      "isPersonal": false,
      "isReadOnly": true,
      "accountCapabilities": {
        "urn:ietf:params:jmap:mail": {
          "maxMailboxesPerEmail": 1,
          "maxMailboxDepth": 10,
          ...
        }
      }
    }
  },
  "primaryAccounts": {
    "urn:ietf:params:jmap:mail": "A13824",
    "urn:ietf:params:jmap:contacts": "A13824"
  },
  "username": "john@example.com",
  "apiUrl": "https://jmap.example.com/api/",
  "downloadUrl": "https://jmap.example.com
    /download/{accountId}/{blobId}/{name}?accept={type}",
  "uploadUrl": "https://jmap.example.com/upload/{accountId}/",
  "eventSourceUrl": "https://jmap.example.com
    /eventsource/?types={types}&closeafter={closeafter}&ping={ping}",
  "state": "75128aab4b1b"
}

2.2. 服务自动发现

互联网协议目前有两种标准化的自动发现方法:

对于域 "example.com" 支持 JMAP 的主机应当发布一条 SRV 记录 "_jmap._tcp.example.com",给出主机名和端口(通常为端口 "443")。JMAP 会话资源随后即为 "https://${hostname}[:${port}]/.well-known/jmap"(遵循任何重定向)。

如果客户端拥有形式为电子邮件地址的用户名,它可以使用该用户名的域部分来尝试自动发现 JMAP 服务器。

3. 结构化数据交换

客户端可以向服务器发起 API 请求以获取或设置结构化数据。该请求由一组有序的方法调用组成。这些方法由服务器处理,随后服务器返回一组有序的响应。

3.1. 发起 API 请求

要发起 API 请求,客户端向 API 资源发起一个经过认证的 POST 请求,该资源由 Session 对象(见第 2 节)中的 "apiUrl" 属性定义。

该请求必须为 "application/json" 类型,并包含一个由第 3.3 节定义的、单个 JSON 编码的 "Request" 对象。如果成功,响应也必须为 "application/json" 类型,并包含由第 3.4 节定义的单个 "Response" 对象。

3.2. Invocation 数据类型

方法调用与响应由 *Invocation* 数据类型表示。这是一个元组,表示为一个包含三个元素的 JSON 数组:

  1. 一个方法名或响应名的 "String(字符串)" *name*。
  2. 一个包含该方法或响应的具名 *arguments(参数)* 的 "String[*]" 对象。
  3. 一个 *method call id(方法调用 id)*,类型为 "String":客户端提供的任意字符串,将随该方法调用发出的响应一同回显(一个方法可能返回 1 个或多个响应,因为它可能隐式调用其他方法;所有由该方法调用发起的响应在响应中都获得相同的方法调用 id)。

3.3. Request 对象

一个 *Request* 对象具有以下属性:

未来的规范可能会向 Request 对象添加更多属性以扩展其语义。为确保前向兼容性,服务器必须忽略 JMAP Request 对象上任何它不理解的其他属性。

3.3.1. 请求示例

{
  "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
  "methodCalls": [
    [ "method1", {
      "arg1": "arg1data",
      "arg2": "arg2data"
    }, "c1" ],
    [ "method2", {
      "arg1": "arg1data"
    }, "c2" ],
    [ "method3", {}, "c3" ]
  ]
}

3.4. Response 对象

一个 *Response* 对象具有以下属性:

除非另有说明,如果方法调用成功完成,其响应名与请求中的方法名相同。

3.4.1. 响应示例

                  {
                    "methodResponses": [
                      [ "method1", {
                        "arg1": 3,
                        "arg2": "foo"
                      }, "c1" ],
                      [ "method2", {
                        "isBlah": true
                      }, "c2" ],
                      [ "anotherResponseFromMethod2", {
                        "data": 10,
                        "yetmoredata": "Hello"
                      }, "c2"],
                      [ "error", {
                        "type":"unknownMethod"
                      }, "c3" ]
                    ],
                    "sessionState": "75128aab4b1b"
                  }

3.5. 省略参数

方法的某个参数可能被指定为具有默认值。如果客户端省略了它,服务器必须像指定了默认值一样处理该方法调用。类似地,服务器可以省略响应中任何具有默认值的参数。

除非在方法描述中另有说明,在请求或响应中(且类型签名允许的情况下)null 是任何参数的默认值。其他参数只有在方法描述中定义了显式默认值时才能被省略。

3.6. 错误

在 JMAP 中,错误可以在三个不同粒度级别上返回。

当发起一个 API 请求时,整个请求可能由于速率限制、格式错误的 JSON、对未知能力的请求等原因被拒绝。在这种情况下,整个请求被拒绝,并返回适当的 HTTP 错误响应码,以及为客户端提供详情的额外 JSON 主体。

只要请求本身在语法上有效(JSON 有效,且解码后匹配 Request 对象的类型签名),其中的方法就会由服务器按顺序执行。每个方法都可能单独失败,例如,如果给出了无效参数,或调用了未知的方法名。

最后,那些改变服务器状态的方法通常在单次调用中对多个不同的记录进行操作。每个记录的变更都可能被单独以 SetError 拒绝,如第 5.3 节所述。

3.6.1. 请求级错误

当向客户端返回 HTTP 错误响应时,服务器应当返回一个 JSON "problem details(问题详情)"对象作为响应主体,如 [RFC7807] 所述。

定义了以下问题类型:

3.6.1.1. 示例

       {
         "type": "urn:ietf:params:jmap:error:unknownCapability",
         "status": 400,
         "detail": "The Request object used capability
           'https://example.com/apis/foobar', which is not supported
           by this server."
       }

另一个示例:

     {
       "type": "urn:ietf:params:jmap:error:limit",
       "limit": "maxSizeRequest",
       "status": 400,
       "detail": "The request is larger than the server is willing to
                  process."
     }

3.6.2. 方法级错误

如果一个方法遇到错误,必须在 "methodResponses" 数组中的当前位置插入相应的 "error" 响应,并且除非另有说明,该方法调用内不得进行进一步处理。

请求中的任何其他方法调用随后必须照常处理。方法级的错误不得产生 HTTP 级别的错误。

一个 "error" 响应如下所示:

                         [ "error", {
                           "type": "unknownMethod"
                         }, "call-id" ]

响应名为 "error",并且必须具有一个 type 属性。可能存在其他属性以提供进一步信息;这些在相应的错误类型描述中详述。

除非返回的是 "serverPartialFail" 错误,否则如果返回了方法级错误,服务器对外可见的状态不得发生更改。

定义了以下错误类型,它们可能在对任何方法调用的适当情况下返回:

特定方法的进一步可能错误在方法描述中给出。

未来的 RFC 中可能定义更多的一般性错误。如果客户端收到一个它不理解的错误类型,它必须将其与 "serverFail" 类型同等对待。

3.7. 对先前方法结果的引用

为使客户端更高效地利用网络并避免往返,一个方法的某个参数可以取自同一请求中先前方法调用的结果。

为此,客户端在该参数名前加上 "#"(octothorpe,井号)。其值为如下所述的 ResultReference 对象。在处理方法调用时,服务器必须首先检查 arguments 对象中是否有任何以 "#" 开头的名称。如果找到,应先解析该结果引用,并将该值用作"真实"参数。随后方法照常处理。如果任何结果引用解析失败,整个方法必须以 "invalidResultReference" 错误被拒绝。如果 arguments 对象同时包含同名参数的普通形式与引用形式(例如 "foo" 和 "#foo"),该方法必须返回 "invalidArguments" 错误。

一个 *ResultReference* 对象具有以下属性:

解析步骤:

  1. 在 "methodResponses" 数组中,寻找方法调用 id 与 ResultReference 的 "resultOf" 属性相同的第一个响应(来自同一请求中此前已处理的方法调用)。如果没有,则求值失败。
  2. 如果响应名与 ResultReference 的 "name" 属性不一致,则求值失败。
  3. 对响应的参数对象(响应数组中的第二项)应用 "path",遵循 JSON Pointer 算法 [RFC6901],但有以下对"求值"(见第 4 节)的补充:
    • 如果当前被引用的值是一个 JSON 数组,引用令牌可以恰好是单个字符 "*",使新的被引用值成为将该 JSON Pointer 剩余令牌应用于数组中每一项、并以相同顺序在一个新数组中返回结果的结果。如果将剩余指针令牌应用于每一项的结果是其本身也是一个数组,则该数组的内容会被加入输出,而非数组本身(即结果从一个数组的数组被展平为单个数组)。

作为一个简单示例,假设我们有以下 API 请求的 "methodCalls":

                      [[ "Foo/changes", {
                          "accountId": "A1",
                          "sinceState": "abcdef"
                      }, "t0" ],
                      [ "Foo/get", {
                          "accountId": "A1",
                          "#ids": {
                              "resultOf": "t0",
                              "name": "Foo/changes",
                              "path": "/created"
                          }
                      }, "t1" ]]

执行第一个方法调用后,"methodResponses" 数组为:

                      [[ "Foo/changes", {
                          "accountId": "A1",
                          "oldState": "abcdef",
                          "newState": "123456",
                          "hasMoreChanges": false,
                          "created": [ "f1", "f4" ],
                          "updated": [],
                          "destroyed": []
                      }, "t0" ]]

要执行 "Foo/get" 调用,我们遍历参数,发现有一个带 "#" 前缀的参数。要解析它,我们应用上述算法:

  1. 寻找方法调用 id 为 "t0" 的第一个响应。"Foo/changes" 响应满足该标准。
  2. 检查响应名是否与结果引用中的相同。相同,因此没问题。
  3. 将 "path" 作为 JSON Pointer 应用于参数对象。这只需选择 "created" 属性,因此求值结果为:[ "f1", "f4" ]。

JMAP 服务器现在继续处理 "Foo/get" 调用,就好像参数是:

                         {
                             "accountId": "A1",
                             "ids": [ "f1", "f4" ]
                         }

现在,一个使用 JMAP Mail 数据模型的更复杂示例:获取收件箱中前 10 个会话(按最新优先排序)中每封邮件的 "from"/"date"/"subject":

      [[ "Email/query", {
        "accountId": "A1",
        "filter": { "inMailbox": "id_of_inbox" },
        "sort": [{ "property": "receivedAt", "isAscending": false }],
        "collapseThreads": true,
        "position": 0,
        "limit": 10,
        "calculateTotal": true
      }, "t0" ],
      [ "Email/get", {
        "accountId": "A1",
        "#ids": {
          "resultOf": "t0",
          "name": "Email/query",
          "path": "/ids"
        },
        "properties": [ "threadId" ]
      }, "t1" ],
      [ "Thread/get", {
        "accountId": "A1",
        "#ids": {
          "resultOf": "t1",
          "name": "Email/get",
          "path": "/list/*/threadId"
        }
      }, "t2" ],
      [ "Email/get", {
        "accountId": "A1",
        "#ids": {
          "resultOf": "t2",
          "name": "Thread/get",
          "path": "/list/*/emailIds"
        },
        "properties": [ "from", "receivedAt", "subject" ]
      }, "t3" ]]

执行前 3 个方法调用后,"methodResponses" 数组可能是:

       [[ "Email/query", {
           "accountId": "A1",
           "queryState": "abcdefg",
           "canCalculateChanges": true,
           "position": 0,
           "total": 101,
           "ids": [ "msg1023", "msg223", "msg110", "msg93", "msg91",
               "msg38", "msg36", "msg33", "msg11", "msg1" ]
       }, "t0" ],
       [ "Email/get", {
           "accountId": "A1",
           "state": "123456",
           "list": [{
               "id": "msg1023",
               "threadId": "trd194"
           }, {
               "id": "msg223",
               "threadId": "trd114"
           },
           ...
           ],
           "notFound": []
       }, "t1" ],
       [ "Thread/get", {
           "accountId": "A1",
           "state": "123456",
           "list": [{
               "id": "trd194",
               "emailIds": [ "msg1020", "msg1021", "msg1023" ]
           }, {
               "id": "trd114",
               "emailIds": [ "msg201", "msg223" ]
           },
           ...
           ],
           "notFound": []
       }, "t2" ]]

要执行最后的 "Email/get" 调用,我们遍历参数,发现有一个带 "#" 前缀的参数。要解析它,我们应用该算法:

  1. 寻找方法调用 id 为 "t2" 的第一个响应。"Thread/get" 响应满足该标准。
  2. "Thread/get" 是结果引用中指定的名称,因此没问题。
  3. 将 "path" 作为 JSON Pointer 应用于参数对象。逐令牌:
    1. "list":获取会话对象数组。
    2. "*":对数组中的每一项:
      1. "emailIds":获取邮件 id 数组。
      2. 将这些连接成结果中所有 id 的单个数组。

JMAP 服务器现在继续处理 "Email/get" 调用,就好像参数是:

{
    "accountId": "A1",
    "ids": [ "msg1020", "msg1021", "msg1023", "msg201", "msg223", ... ],
    "properties": [ "from", "receivedAt", "subject" ]
}

ResultReference 与创建 id 扮演类似的角色,因为它允许链式的方法调用引用在生成请求时尚不可用的信息。然而,它们是不同的东西,不可互换;唯一的共同点是都使用了用于标识它们的井号(octothorpe)。

3.8. 用户可见字符串的本地化

如果要返回展示给用户的自定义字符串(例如错误消息),服务器应当使用请求的 Accept-Language 头(如 [RFC7231] 第 5.3.5 节所定义)中的信息来选择最佳的可用本地化。响应的 Content-Language 头(见 [RFC7231] 第 3.1.3.2 节)应当指明用于用户可见字符串的语言。

例如,假设发起了带有以下头的请求:

       Accept-Language: fr-CH, fr;q=0.9, de;q=0.8, en;q=0.7, *;q=0.5

某个方法生成了一个要展示给用户的错误。服务器拥有该错误消息的英文和德文译文。查看 Accept-Language 头,用户的首选语言是法语。由于我们没有法语译文,我们查看下一个最受偏好的语言,即德语。我们有德语译文,因此服务器返回该译文,并在 Content-Language 头中指明所选语言,如下所示:

                           Content-Language: de

3.9. 安全

一如既往,服务器必须严格对待从客户端接收的数据。需要检查参数的有效性;恶意用户可能试图通过 API 找到漏洞。在参数无效(数据未知/不足/类型错误等)的情况下,该方法必须返回 "invalidArguments" 错误并终止。

3.10. 并发

单个请求内的方法调用必须按顺序执行。然而,来自不同并发 API 请求的方法调用可能交错进行。这意味着在单个 API 请求内的两个方法调用之间,服务器上的数据可能发生变化。

4. Core/echo 方法

"Core/echo" 方法原样返回它所接收到的参数。它对于测试你是否拥有一个到 JMAP API 端点的有效、经过认证的连接有用。

4.1. 示例

请求:

                             [[ "Core/echo", {
                               "hello": true,
                               "high": 5
                             }, "b3ff" ]]

响应:

                             [[ "Core/echo", {
                               "hello": true,
                               "high": 5
                             }, "b3ff" ]]

5. 标准方法与命名约定

JMAP 为创建、检索、更新和删除特定类型的对象提供了统一的接口。对于 "Foo" 数据类型,该类型的记录将通过 "Foo/get" 调用获取,并通过 "Foo/set" 调用修改。增量更新可以通过 "Foo/changes" 调用获取。这些方法都遵循如下所述的标准格式。

某些类型可能没有所有这些方法。定义类型的规范必须指定该类型可用哪些方法。

5.1. /get

Foo 类型的对象通过调用 "Foo/get" 获取。

它接受以下参数:

响应具有以下参数:

可能返回以下额外错误以代替 "Foo/get" 响应:

5.2. /changes

当账户中一组 Foo 记录的状态在服务器上发生变化时(无论是由于创建、更新还是删除),"Foo/get" 响应的 "state" 属性会变化。"Foo/changes" 方法允许客户端高效地将其 Foo 缓存状态更新为与服务器上的新状态匹配。它接受以下参数:

响应具有以下参数:

如果一条记录自旧状态以来既被创建又被更新,服务器应当只将该 id 返回在 "created" 列表中,但也可以将其返回在 "updated" 列表中。

如果一条记录自旧状态以来既被更新又被销毁,服务器应当只将该 id 返回在 "destroyed" 列表中,但也可以将其返回在 "updated" 列表中。

如果一条记录自旧状态以来既被创建又被销毁,服务器应当从响应中完全移除该 id。不过,它可能只将其包含在 "destroyed" 列表中,或同时包含在 "destroyed" 和 "created" 列表中。

如果提供了 "maxChanges" 或由服务器自动设置,服务器必须确保跨 "created"、"updated" 和 "destroyed" 返回的 id 数量不超过此限制。如果客户端状态与当前服务器状态之间的变更数量超过此限制,服务器应当生成一个将客户端带到中间状态的更新,客户端可继续调用 "Foo/changes" 直到完全更新。如果它无法计算中间状态,则必须返回一个 "cannotCalculateChanges" 错误响应。

在生成中间状态时,服务器可以选择如何划分变更。对许多类型而言,先返回较新的变更能提供更好的用户体验,因为这更可能是用户最感兴趣的。随后客户端可以在用户查看较新数据的同时继续分页载入较旧的变更。例如,假设服务器经历了以下状态:

                           A -> B -> C -> D -> E

客户端请求自状态 "B" 起的变更。服务器可能首先获取状态 D 与 E 之间创建、更新或销毁的记录 id,并返回:

                           state: "B-D-E"
                           hasMoreChanges: true

客户端随后将请求自 "B-D-E" 起的变更,服务器可以返回状态 C 与 D 之间的变更,返回:

                           state: "B-C-E"
                           hasMoreChanges: true

最后,客户端将请求自 "B-C-E" 起的变更,服务器可以返回状态 B 与 C 之间的变更,返回:

                           state: "E"
                           hasMoreChanges: false

如果在此过程中服务器状态被修改(变为 "F"),服务器仍执行相同操作,但现在当返回到状态 "E" 的更新时,它会指示仍有更多变更需要客户端获取。

当对一条记录的多次变更被拆分到不同的中间状态时,服务器不得在某个将其视为已更新或已销毁的响应之后,又将该记录作为已创建返回;并且不得在某个将其视为已创建或已更新的响应之前,将该记录作为已销毁返回。服务器可能需要合并对某条记录的多次变更以满足此要求。

可能返回以下额外错误以代替 "Foo/changes" 响应:

维持状态以便计算 "Foo/changes" 对服务器而言可能代价高昂,但总是返回 "cannotCalculateChanges" 会严重增加客户端的网络流量和资源使用。为了实现高效同步,服务器应当能够从过去 30 天内提供给客户端的任何状态字符串计算变更(当然也可以支持从更旧的状态计算更新)。

5.3. /set

修改服务器上 Foo 对象的状态通过 "Foo/set" 方法完成。它涵盖创建、更新和销毁 Foo 记录。这使得服务器能够理清一次性执行多个操作(例如,确保某种记录类型始终具有最小数量)时可能存在的顺序与依赖关系。

"Foo/set" 方法接受以下参数:

每个对象的创建、修改或销毁都被视为一个原子单元。允许服务器将某些对象的变更提交而其他对象不提交;但是,它不得将单个记录更新的一部分(例如,如果更新对象中同时提供了 "name" 和 "count" 属性,却只更新了 "name" 而未更新 "count")仅提交一部分。

"Foo/set" 完成后的最终状态必须是有效的;不过,服务器在处理单个 create/update/destroy 请求时,可能必须经历无效的(不对客户端暴露的)中间状态。例如,假设有一个必须唯一的 "name" 属性。单个方法调用可以将对象 A 重命名为 B,同时将两个对象 B 重命名为 A。如果最终状态有效,则允许这样做。否则,每个对象的创建、修改或销毁应当按顺序处理,并根据当前服务器状态接受/拒绝。

如果某个创建、更新或销毁被拒绝,必须将相应的错误添加到响应的 notCreated/notUpdated/notDestroyed 属性中,并且服务器必须继续下一个 create/update/destroy。它不会终止该方法。

如果找不到给定的 id,必须以 "notFound" set 错误拒绝该更新或销毁。

如果某个对象在同一 /set 请求中被销毁,服务器可以跳过该更新(以 "willDestroy" SetError 拒绝它)。

某些记录可能持有对其他记录的引用(外键)。该引用可以在同一请求中与被引用记录的创建同时设置(通过 create 或 update)。为此,客户端使用其创建 id(加上 "#" 前缀)来引用新记录。客户端在请求中方法调用的顺序必须使得被引用的记录在相同或更早的调用中被创建。因此,服务器永远无需向前查看。相反,在处理请求时,服务器必须在请求的持续期间为每种新创建的记录维护一个从创建 id 到记录 id 的简单映射,以便在后续方法调用中必要时能够替换进正确的值。对于引用同类型的记录,服务器必须在单个方法调用内对 create 和 update 排序,使得 create 在其创建 id 被同一调用中的另一个 create/update/destroy 引用之前发生。

创建 id 不是按类型划分作用域的,而是所有类型的单一映射。客户端不应在同一 API 请求中的任何地方复用创建 id。如果复用了创建 id,服务器必须将该创建 id 映射到最近用该 id 创建的项。为便于代理 API 请求,可以将一组初始的创建 id 到真实 id 的值随请求传入(见第 3.3 节"Request 对象"),并将映射的最终状态随响应传出(见第 3.4 节"Response 对象")。

响应具有以下参数:

一个 *SetError* 对象具有以下属性:

定义了以下 SetError 类型,并可能在适当情况下针对任何记录类型的 set 操作返回:

SetError 对象还应当具有一个名为 "properties" 的 "String[]" 类型属性,列出所有无效的属性。

各个方法可能为否则会导致 invalidProperties 错误的某些条件指定更具体的错误。如果满足了其中某一条件的情形,则必须返回该错误而非 invalidProperties 错误。

其他可能的 SetError 类型可能在特定的方法描述中给出。SetError 对象上也可能存在其他属性,如相关方法中所述。

可能返回以下额外错误以代替 "Foo/set" 响应:

5.4. /copy

在两个不同账户之间移动 Foo 记录的唯一方式,是使用 "Foo/copy" 方法复制它们;复制成功后,再删除原始记录。"onSuccessDestroyOriginal" 参数允许你尝试在单个方法调用中完成此操作;但请注意,这两个不同的动作不是原子的,因此有可能复制成功但由于某种原因原始记录未被销毁。

复制在概念上分为三个阶段:

  1. 从 "from" 账户读取当前值。
  2. 将新副本写入另一个账户。
  3. 如果请求,销毁 "from" 账户中的原始记录。

由于并发请求,阶段之间数据可能发生变化。

"Foo/copy" 方法接受以下参数:

每个记录副本被视为一个原子单元,可单独成功或失败。

响应具有以下参数:

SetError 可以是为 create 或 update 返回的任何标准 set 错误。此外,定义了以下 SetError:

可能返回以下额外错误以代替 "Foo/copy" 响应:

5.5. /query

对于预期数据总量非常小的数据集,客户端可以只获取完整数据集,然后在本地进行任何排序/过滤。然而,对于大型数据集(例如数 GB 的邮箱),客户端需要能够在服务器上对数据类型进行搜索/排序/窗口分页。

对账户中一组 Foo 的查询通过调用 "Foo/query" 完成。它接受若干参数以确定包含哪些记录、如何排序,以及返回结果的哪一部分(完整列表可能*非常*长)。结果以 Foo id 列表的形式返回。

对 "Foo/query" 的调用接受以下参数:

如果给定了 "anchor" 参数,在过滤和排序后会在结果中查找该锚点。如果找到,则将 "anchorOffset" 加到其索引上。如果得到的索引现在为负,则钳制为 0。该索引现在被完全当作如同作为 "position" 参数提供一样使用。如果找不到锚点,则以 "anchorNotFound" 错误拒绝该调用。

如果指定了 "anchor",客户端提供的任何 position 参数必须被忽略。如果未提供 "anchor",任何 "anchorOffset" 参数必须被忽略。

客户端可以使用 "anchor" 而非 "position" 来在大型结果集中查找某个 id 的索引。

响应具有以下参数:

可能返回以下额外错误以代替 "Foo/query" 响应:

5.6. /queryChanges

"Foo/queryChanges" 方法允许客户端高效地将其缓存的查询状态更新为与服务器上的新状态匹配。它接受以下参数:

响应具有以下参数:

其结果是:如果客户端有一个对应于旧状态结果的、稀疏的 Foo id 缓存数组,那么:

   fooIds = [ "id1", "id2", null, null, "id3", "id4", null, null, null ]

如果它剪除掉其缓存结果中所有在 removed 数组中的 id,那么:

      removed = [ "id2", "id31", ... ];
      fooIds => [ "id1", null, null, "id3", "id4", null, null, null ]

插入(按索引顺序逐个,从最低索引开始)added 数组中的所有 id:

  added = [{ id: "id5", index: 0, ... }];
  fooIds => [ "id5", "id1", null, null, "id3", "id4", null, null, null ]

截断扩展到新的总长度,结果现在就处于新状态。

注意:插入(splicing in)是在给定索引处添加该项,使该索引或更高索引处所有项的索引加一。剪除(splicing out)是其逆操作,移除该项并使数组中其后每一项的索引减一。

可能返回以下额外错误以代替 "Foo/queryChanges" 响应:

5.7. 示例

假设我们有一个 *Todo* 类型,具有以下属性:

还假设该类型的所有标准方法都已定义,且 FilterCondition 对象支持一个 "hasKeyword" 属性来匹配具有给定关键字的 Todo。

客户端可能希望显示一个带有 "music" 关键字或 "video" 关键字的 Todo 列表,因此它发起以下方法调用:

                   [[ "Todo/query", {
                     "accountId": "x",
                     "filter": {
                       "operator": "OR",
                       "conditions": [
                         { "hasKeyword": "music" },
                         { "hasKeyword": "video" }
                       ]
                     },
                     "sort": [{ "property": "title" }],
                     "position": 0,
                     "limit": 10
                   }, "0" ],
                   [ "Todo/get", {
                     "accountId": "x",
                     "#ids": {
                       "resultOf": "0",
                       "name": "Todo/query",
                       "path": "/ids"
                     }
                   }, "1" ]]

这会向服务器查询带有 "music" 或 "video" 关键字的 Todo 集合,按 title 排序,并限制为前 10 个结果。它使用反向引用(back-references)来引用查询结果,获取每个 Todo 的完整对象。响应可能类似如下:

       [[ "Todo/query", {
         "accountId": "x",
         "queryState": "y13213",
         "canCalculateChanges": true,
         "position": 0,
         "ids": [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j" ]
       }, "0" ],
       [ "Todo/get", {
         "accountId": "x",
         "state": "10324",
         "list": [{
           "id": "a",
           "title": "Practise Piano",
           "keywords": {
             "music": true,
             "beethoven": true,
             "mozart": true,
             "liszt": true,
             "rachmaninov": true
           },
           "neuralNetworkTimeEstimation": 3600
         }, {
           "id": "b",
           "title": "Watch Daft Punk music video",
           "keywords": {
             "music": true,
             "video": true,
             "trance": true
           },
           "neuralNetworkTimeEstimation": 18000
         },
         ...
         ]
       }, "1" ]]

现在,假设用户为 "Practise Piano" 任务添加关键字 "chopin" 并移除关键字 "mozart"。客户端可以将整个对象发送给服务器,因为这是一个有效的 PatchObject:

                 [[ "Todo/set", {
                   "accountId": "x",
                   "ifInState": "10324",
                   "update": {
                     "a": {
                       "id": "a",
                       "title": "Practise Piano",
                       "keywords": {
                         "music": true,
                         "beethoven": true,
                         "chopin": true,
                         "liszt": true,
                         "rachmaninov": true
                       },
                       "neuralNetworkTimeEstimation": 360
                     }
                   }
                 }, "0" ]]

或者它可以发送一个最小的 patch:

                      [[ "Todo/set", {
                        "accountId": "x",
                        "ifInState": "10324",
                        "update": {
                          "a": {
                            "keywords/chopin": true,
                            "keywords/mozart": null
                          }
                        }
                      }, "0" ]]

在任一种情况下,服务器上的效果完全相同,并且假设服务器仍处于状态 "10324",它很可能返回成功:

                 [[ "Todo/set", {
                   "accountId": "x",
                   "oldState": "10324",
                   "newState": "10329",
                   "updated": {
                     "a": {
                       "neuralNetworkTimeEstimation": 5400
                     }
                   }
                 }, "0" ]]

服务器在此变更过程中更改了对象上的 "neuralNetworkTimeEstimation" 属性;由于该更改的方式*不是*由发给服务器的 PatchObject 显式请求的,因此它随 "updated" 确认一同返回。

现在让我们向新的 "Practise Piano" Todo 添加一个子 Todo。在此示例中,我们可以看到对创建 id 的引用的使用,使我们能够设置对同一请求中创建的记录的外键引用:

                   [[ "Todo/set", {
                     "accountId": "x",
                     "create": {
                       "k15": {
                         "title": "Warm up with scales"
                       }
                     },
                     "update": {
                       "a": {
                         "subTodoIds": [ "#k15" ]
                       }
                     }
                   }, "0" ]]

现在,假设另一位用户删除了 "Listen to Daft Punk" Todo。第一位用户将收到一个推送通知(见第 7 节),带有 Todo 类型已更改的状态字符串。由于新字符串与其当前状态不匹配,它知道自己需要检查更新。它可能发起类似如下的请求:

                   [[ "Todo/changes", {
                     "accountId": "x",
                     "sinceState": "10324",
                     "maxChanges": 50
                   }, "0" ],
                   [ "Todo/queryChanges", {
                     "accountId": "x",
                     "filter": {
                       "operator": "OR",
                       "conditions": [
                         { "hasKeyword": "music" },
                         { "hasKeyword": "video" }
                       ]
                     },
                     "sort": [{ "property": "title" }],
                     "sinceQueryState": "y13213",
                     "maxChanges": 50
                   }, "1" ]]

并收到如下响应:

                       [[ "Todo/changes", {
                         "accountId": "x",
                         "oldState": "10324",
                         "newState": "871903",
                         "hasMoreChanges": false,
                         "created": [],
                         "updated": [],
                         "destroyed": ["b"]
                       }, "0" ],
                       [ "Todo/queryChanges", {
                         "accountId": "x",
                         "oldQueryState": "y13213",
                         "newQueryState": "y13218",
                         "removed": ["b"],
                         "added": null
                       }, "1" ]]

假设用户可以访问另一个账户 "y",例如多个用户之间共享的团队账户。要将现有 Todo 从账户 "x" 移出,客户端将调用:

                    [[ "Todo/copy", {
                      "fromAccountId": "x",
                      "accountId": "y",
                      "create": {
                        "k5122": {
                          "id": "a"
                        }
                      },
                      "onSuccessDestroyOriginal": true
                    }, "0" ]]

服务器成功将 Todo 复制到新账户(在那里它获得一个新 id)并删除原记录。由于隐式调用了 "Todo/set",对单个方法调用有两个响应,两者具有相同的方法调用 id:

                       [[ "Todo/copy", {
                         "fromAccountId": "x",
                         "accountId": "y",
                         "created": {
                           "k5122": {
                             "id": "DAf97"
                           }
                         },
                         "oldState": "c1d64ecb038c",
                         "newState": "33844835152b"
                       }, "0" ],
                       [ "Todo/set", {
                         "accountId": "x",
                         "oldState": "871903",
                         "newState": "871909",
                         "destroyed": [ "a" ],
                         ...
                       }, "0" ]]

5.8. 代理考量

JMAP 的设计使 API 端点能够轻松地代理到一个或多个 JMAP 服务器。这对于负载均衡、扩充能力,或向托管在不同 JMAP 服务器上的账户呈现单一端点(基于每个方法的 "accountId" 参数拆分请求)可能很有用。代理只需理解 JMAP Request 对象的一般结构;它无需知道它将传递给其他服务器的任何方法或参数的具体细节。

如果将请求中的方法拆分以在不同的后端服务器上调用,代理必须做两件事,以确保反向引用和创建 id 引用以与在整个请求在单一服务器上处理时相同的方式解析:

  1. 它必须随每个子请求传入一个 "createdIds" 属性。如果客户端未提供,则第一个子请求应使用空对象。每个子响应的 "createdIds" 属性应传入下一个子请求。
  2. 它必须解析在不同服务器上处理的、对先前方法结果的反向引用。这是一个相对简单的语法替换,在第 3.7 节中描述。

当基于 accountId 拆分请求时,代理实现者确实需要意识到在账户之间复制的 "/copy" 方法。如果这些账户位于不同服务器上,代理将不得不直接实现此功能。

6. 二进制数据

在 JMAP 中,二进制数据由 *blobId* 引用,并独立于核心 API 上传/下载。blobId 仅代表数据的原始字节,而非任何关联的元数据,例如文件名或内容类型。此类元数据与引用它的对象中的 blobId 一同存储。blobId 所代表的数据是不可变的。

账户内存在的任何 blobId 都可以在该账户中创建/更新另一个对象时使用。例如,Email 类型可能有一个代表 Internet Message Format [RFC5322] 中对象的 blobId。客户端可以创建一个带有附件的新 Email 对象并使用该 blobId,实际上是将旧消息附加到新消息上。类似地,它可以在不重新下载和上传的情况下,附加旧消息的任何现有附件。

当客户端在 create/update 中使用 blobId 时,服务器可以为该新/更新对象中的相同二进制数据分配一个新的 blobId。如果这样做,它必须在 created/updated 响应中返回任何包含已更改 blobId 的属性,以便客户端获得新的 id。

未被 JMAP 对象(例如作为消息附件)引用的 blob 可能被服务器删除以释放资源。上传(见下文)最初是未引用的 blob。为确保互操作性:

6.1. 上传二进制数据

存在一个处理账户所有文件上传的单一端点,无论它们将用于什么。Session 对象(见第 2 节)具有一个 "uploadUrl" 属性,采用 URI 模板(level 1)格式 [RFC6570],其中必须包含一个名为 "accountId" 的变量。客户端可以将此模板与 "accountId" 结合使用,以获取文件上传资源的 URL。

要上传文件,客户端向文件上传资源发起一个经过认证的 POST 请求。

成功的请求必须返回单个 JSON 对象,作为响应,具有以下属性:

如果上传的内容与账户中现有 blob 的二进制内容相同,可能返回现有的 blobId。

客户端应及时使用返回的 blobId。在极少数情况下,在客户端使用之前,服务器可能已删除了该 blob;客户端应保留对本地文件的引用,以便在这种情况下可以再次上传。

当向客户端返回 HTTP 错误响应时,服务器应当返回一个 JSON "problem details" 对象作为响应主体,如 [RFC7807] 所述。

由于访问控制通常由持有对 blob 引用的对象决定,即使在共享账户中,未引用的 blob 也只能由上传者访问。

6.2. 下载二进制数据

Session 对象(见第 2 节)具有一个 "downloadUrl" 属性,采用 URI 模板(level 1)格式 [RFC6570]。该 URL 必须包含名为 "accountId"、"blobId"、"type" 和 "name" 的变量。

要下载文件,客户端向下载 URL 发起一个经过认证的 GET 请求,代入相应的变量:

由于特定 blobId 的数据是不可变的,因此在生成的下载 URL 中的响应也是不可变的,建议实现者为成功的响应设置较长的缓存时间,并使用 "immutable" Cache-Control 扩展 [RFC8246],例如 "Cache-Control: private, immutable, max-age=31536000"。

当向客户端返回 HTTP 错误响应时,服务器应当返回一个 JSON "problem details" 对象作为响应主体,如 [RFC7807] 所述。

6.3. Blob/copy

二进制数据可以使用 "Blob/copy" 方法在两个不同账户之间复制,而无需在客户端下载后重新上传。

"Blob/copy" 方法接受以下参数:

响应具有以下参数:

SetError 可以是第 5.3 节中定义的、可能为 create 返回的任何标准 set 错误。此外,如果找不到要复制的 blobId,可能返回 "notFound" SetError 错误。

可能返回以下额外方法级错误以代替 "Blob/copy" 响应:

7. 推送

推送通知允许客户端高效地更新(几乎)即时地与服务器上的数据变更保持同步。推送的一般模型很简单,并在推送通道上发送最小的数据:刚好足够让客户端知道是否需要重新同步。该格式允许多个变更合并为单个推送更新,并允许服务器对推送频率进行速率限制。即使某些推送事件在到达客户端之前被丢弃也没关系;下次它获取/设置任何已变更类型的记录时,它会发现数据已变,并仍然会同步所有变更。

客户端可以通过两种不同的机制接收推送通知,以适应客户端可能存在其中的不同环境。事件源资源(见第 7.3 节)允许能够保持传输连接打开的客户端直接从 JMAP 服务器接收推送通知。这很简单且避免了第三方,但在受限平台(例如移动设备)上往往不可行。或者,客户端可以利用其环境支持的任何推送服务。将推送服务的 URL 注册到 JMAP 服务器(见第 7.2 节);随后服务器将每个通知 POST 到该 URL。该推送服务随后负责将这些通知路由到客户端。

7.1. StateChange 对象

当服务器上发生某些变更时,服务器向客户端推送一个 StateChange 对象。一个 *StateChange* 对象具有以下属性:

7.1.1. 示例

在此示例中,服务器在推送到客户端的以下 StateChange 对象之前,已跨用户有权访问的两个不同账户合并了若干变更:

                  {
                    "@type": "StateChange",
                    "changed": {
                      "a3123": {
                        "Email": "d35ecb040aab",
                        "EmailDelivery": "428d565f2440",
                        "CalendarEvent": "87accfac587a"
                      },
                      "a43461d": {
                        "Mailbox": "0af7a512ce70",
                        "CalendarEvent": "7a4297cecd76"
                      }
                    }
                  }

客户端可以将状态字符串与相应账户中 Email、CalendarEvent 等对象类型的当前状态进行比较,以查看它是否需要获取变更。

如果客户端自身正在做出变更,它可能在 /set API 调用还在进行中时收到一个 StateChange 对象。它可以等到调用完成,然后比较 /set 之后的新状态字符串是否与 StateChange 对象中推送的相同;如果相同,且 /set 响应的旧状态与客户端的先前状态匹配,它就不必浪费一个请求去询问它已经知道的变更。

7.2. PushSubscription

客户端可以创建一个 PushSubscription,向 JMAP 服务器注册一个 URL。JMAP 服务器随后会针对它希望发送给客户端的每个推送通知,向该 URL 发起一个 HTTP POST 请求。

由于推送订阅会导致 JMAP 服务器向先前未知的端点发起若干请求,它可被用作发起拒绝服务攻击的载体。为防止这种情况,当创建订阅时,JMAP 服务器立即向该 URL 发送一个 PushVerification 对象(见第 7.2.2 节)。在客户端收到推送并用正确的验证码更新订阅之前,JMAP 服务器不得向该 URL 发起任何进一步请求。

一个 *PushSubscription* 对象具有以下属性:

POST 请求必须具有 "application/json" 的内容类型,并将 UTF-8 JSON 编码的对象作为主体。请求必须具有 "TTL" 头,并可以具有 "Urgency" 和/或 "Topic" 头,如 [RFC8030] 第 5 节所规定。JMAP 服务器应以合理的方式理解并处理 HTTP 状态响应。必须让 "429"(Too Many Requests)响应导致 JMAP 服务器降低推送频率;JMAP 推送结构允许多个变更合并为单个最小的 StateChange 对象。有关连接到未知服务器的风险的讨论,见第 8.6 节的安全考量。

JMAP 服务器充当 [RFC8030] 中定义的应用服务器。客户端可以结合使用 [RFC8030] 的其余部分与其自身的推送服务,以形成完整的端到端解决方案,或者也可以依赖替代机制来确保推送数据在离开 JMAP 服务器后送达。

推送订阅与用于认证创建它的 API 请求的凭证绑定。如果这些凭证过期或撤销,JMAP 服务器必须销毁该推送订阅。客户端获取现有订阅时,只返回由这些凭证创建的订阅。

当这些凭证自身有过期时间(即带有超时的会话)时,服务器不应设置或限定客户端给出的推送订阅过期时间,而必须在会话过期时使其过期。

当这些凭证没有时间限制时(例如 Basic 认证 [RFC7617]),如果客户端未给出,服务器应为推送订阅设置一个过期时间,并在设置得过于久远时限其过期时间。该最大过期时间必须至少在未来 48 小时,并应至少在未来 7 天。在移动设备上运行的应用可能只能在位于前台时刷新推送订阅生存期,因此这提供了一个合理的时间窗口来允许这种情况发生。

在单独的访问凭证与刷新凭证的情况下(如 OAuth 2.0 [RFC6749]),服务器应将推送订阅绑定到刷新 token(而非访问 token)的有效性,并根据其是否时间受限来表现。

当推送订阅被销毁时,服务器必须尽快从内存和存储中安全擦除 URL 和加密密钥。

7.2.1. PushSubscription/get

如第 5.1 节所述的标准 /get 方法,区别是它接受或返回 "accountId" 参数,因为推送订阅不绑定到特定账户。它也返回 "state" 参数。"ids" 参数可以为 null 以一次性获取所有。

服务器必须只返回使用与本次 "PushSubscription/get" 请求相同的认证凭证创建的推送订阅。

由于 "url" 和 "keys" 属性可能包含特定设备私有数据,这些属性的值不得返回。如果 "properties" 参数为 null 或被省略,服务器必须默认为除这两个之外的所有属性。如果显式请求了其中之一,必须以 "forbidden" 错误拒绝该方法调用。

7.2.2. PushSubscription/set

如第 5.3 节所述的标准 /set 方法,区别是它接受或返回 "accountId" 参数,因为推送订阅不绑定到特定账户。它也接受 "ifInState" 参数,也不返回 "oldState" 或 "newState" 参数。

"url" 和 "keys" 属性是不可变的;如果客户端希望更改这些,它必须销毁当前的推送订阅并创建一个新的。

当创建 PushSubscription 时,服务器必须立即向该 URL 推送一个 *PushVerification* 对象。它具有以下属性:

在服务器向该订阅的 URL 发起任何进一步请求之前,客户端必须用正确的验证码更新该推送订阅。尝试用无效验证码更新订阅必须被服务器以 "invalidProperties" SetError 拒绝。

客户端可以更新 "expires" 属性以延长(或较少见地缩短)推送订阅的生存期。服务器可以修改建议的新过期时间以实施服务器定义的限制。延长生存期不需要再次验证订阅。

客户端不应更新或销毁不是它创建的推送订阅(即具有它不认识的 "deviceClientId" 的订阅)。

7.2.3. 示例

在 "2018-07-06T02:14:29Z",一个 deviceClientId 为 "a889-ffea-910" 的客户端获取服务器上当前的推送订阅集合,发起 API 请求:

                       [[ "PushSubscription/get", {
                         "ids": null
                       }, "0" ]]

返回:

       [[ "PushSubscription/get", {
         "list": [{
             "id": "e50b2c1d-9553-41a3-b0a7-a7d26b599ee1",
             "deviceClientId": "b37ff8001ca0",
             "verificationCode": "b210ef734fe5f439c1ca386421359f7b",
             "expires": "2018-07-31T00:13:21Z",
             "types": [ "Todo" ]
         }, {
             "id": "f2d0aab5-e976-4e8b-ad4b-b380a5b987e4",
             "deviceClientId": "X8980fc",
             "verificationCode": "f3d4618a9ae15c8b7f5582533786d531",
             "expires": "2018-07-12T05:55:00Z",
             "types": [ "Mailbox", "Email", "EmailDelivery" ]
         }],
         "notFound": []
       }, "0" ]]

由于返回的两个推送订阅对象都没有该客户端的 deviceClientId,它知道自己服务器上当前没有活跃的推送订阅。因此它创建一个,发送此请求:

[[ "PushSubscription/set", {
  "create": {
    "4f29": {
      "deviceClientId": "a889-ffea-910",
      "url": "https://example.com/push/?device=X8980fc&client=12c6d086",
      "types": null
    }
  }
}, "0" ]]

服务器创建推送订阅,但将过期时间限制为未来 7 天,返回此响应:

            [[ "PushSubscription/set", {
              "created": {
                "4f29": {
                  "id": "P43dcfa4-1dd4-41ef-9156-2c89b3b19c60",
                  "keys": null,
                  "expires": "2018-07-13T02:14:29Z"
                }
              }
            }, "0" ]]

服务器还立即向 "https://example.com/push/?device=X8980fc&client=12c6d086" 发起一个 POST 请求,数据为:

      {
        "@type": "PushVerification",
        "pushSubscriptionId": "P43dcfa4-1dd4-41ef-9156-2c89b3b19c60",
        "verificationCode": "da1f097b11ca17f06424e30bf02bfa67"
      }

客户端收到此数据,并用验证码更新订阅(注意这里存在潜在的竞态条件;客户端必须能够处理在创建订阅的请求仍在进行中时就收到推送的情况):

       [[ "PushSubscription/set", {
         "update": {
           "P43dcfa4-1dd4-41ef-9156-2c89b3b19c60": {
             "verificationCode": "da1f097b11ca17f06424e30bf02bfa67"
           }
         }
       }, "0" ]]

服务器确认更新成功,现在将在状态变化时向注册的 URL 发起请求。

两天后,客户端更新订阅以延长其生存期,发送此请求:

               [[ "PushSubscription/set", {
                 "update": {
                   "P43dcfa4-1dd4-41ef-9156-2c89b3b19c60": {
                     "expires": "2018-08-13T00:00:00Z"
                   }
                 }
               }, "0" ]]

服务器延长过期时间,但再次只到其未来 7 天的最大限制,返回此响应:

               [[ "PushSubscription/set", {
                 "updated": {
                   "P43dcfa4-1dd4-41ef-9156-2c89b3b19c60": {
                     "expires": "2018-07-15T02:22:50Z"
                   }
                 }
               }, "0" ]]

7.3. 事件源

能够保持传输连接打开的客户端可以直接连接到 JMAP 服务器,通过 "text/event-stream" 资源接收推送通知,如 [EventSource] 所述。这是一个长时间运行的 HTTP 请求,服务器可以通过追加数据而不结束响应,从而将数据推送给客户端。

当服务器上的数据发生变化时,它会向任何已连接的客户端推送一个名为 "state" 的事件,数据为一个 StateChange 对象。

服务器还应发送一个新的事件 id,编码在发送 "state" 事件后立即对用户可见的整个服务器状态。当与事件源端点建立新连接时,遵循服务器发送事件(server-sent events)规范的客户端会发送一个 Last-Event-ID HTTP 头字段,带其看到的最后一个 id,服务器可用它来判断客户端是否错过了某些变更。如果是,它应在连接时立即发送这些变更。

Session 对象(见第 2 节)具有一个 "eventSourceUrl" 属性,采用 URI 模板(level 1)格式 [RFC6570]。该 URL 必须包含名为 "types"、"closeafter" 和 "ping" 的变量。

要连接到该资源,客户端向事件源 URL 发起一个经过认证的 GET 请求,代入相应的变量:

客户端可以保持与事件源资源的多个连接打开,尽管出于效率考虑它应尝试使用单一连接。

8. 安全考量

8.1. 传输机密性

为确保通过 JMAP 收发数据的机密性与完整性,所有请求必须使用 TLS 1.2 [RFC5246] [RFC8446] 或更高版本,遵循 [RFC7525] 中的建议。服务器应支持 TLS 1.3 [RFC8446] 或更高版本。

客户端必须校验 TLS 证书链,以防范中间人攻击 [RFC5280]。

8.2. 认证方案

已经标准化了若干 HTTP 认证方案(见 <https://www.iana.org/assignments/http-authschemes/>)。服务器在决定实现哪些方案时,应仔细评估不同方案相对于其需求的安全特性。

不建议使用 Basic 认证方案。选择使用它的服务强烈建议为每个希望连接的客户端,通过某种外部机制生成唯一的"应用密码(app password)"。这使得服务器能够区分来自不同设备的连接,并单独撤销访问。

8.3. 服务自动发现

除非有类似 DNSSEC 的保护,否则使用 SRV DNS 记录的服务器详情自动发现容易受到 DNS 投毒攻击,这可能导致客户端与攻击者的服务器而非真实的 JMAP 服务器通信。攻击者随后可能拦截请求以实施中间人攻击,并根据认证方案窃取凭证来生成其自己的请求。

不支持 SRV 查找的客户端可能会尝试直接对用户名域通过 HTTPS 使用 "/.well-known/jmap" 路径。服务器应确保该路径能解析或重定向到正确的 JMAP 会话资源,以允许这种情况工作。如果这不可行,服务器必须确保该路径不能被攻击者控制,因为同样它可能用于窃取凭证。

8.4. JSON 解析

[RFC8259] 的安全考量适用于将 JSON 用作数据交换格式的情形。

与任何序列化格式一样,解析器需要彻底检查所提供数据的语法。JSON 对若干类型与结构使用开始和结束标记,在扫描匹配结束标记时可能到达所提供数据的末尾;这是一个错误条件,实现需要在所提供数据末尾停止扫描。

JSON 还使用带某些转义序列的字符串编码来表示特殊字符。处理这些转义序列时需要小心,以确保它们在触发特殊处理之前是完整的,当转义序列出现在其他(非转义)特殊字符附近或紧邻数据末尾时(如前一段所述)要特别小心。

如果将 JSON 解析为非文本的结构化数据格式,实现可能需要分配存储来保存 JSON 字符串元素。由于 JSON 不使用显式的字符串长度,因资源耗尽而导致拒绝服务的风险较小,但实现仍可能希望对它们在任何给定上下文中愿意进行的分配大小设定限制,以避免不受信任的数据导致过度的内存分配。

8.5. 拒绝服务

如果不实施资源限制,一个小的请求可能导致非常大的响应,并在服务器上需要大量工作。JMAP 提供了通告和实施多种限制以缓解此威胁的机制,包括单次方法调用中获取的对象数量、单个请求中的方法数量、并发请求数量等的限制。

JMAP 服务器必须实施合理的限制,以缓解资源耗尽攻击。

8.6. 连接到未知推送服务器

当注册推送订阅时,应用服务器将向给定 URL 发起 POST 请求。实现这一点时必须考虑若干安全考量。

服务器必须确保该 URL 在外部可解析,以避免服务器端请求伪造(SSRF),即服务器向其内部网络上的资源发起请求。

恶意客户端可能利用推送订阅尝试向第三方服务器洪水般发送请求,制造拒绝服务攻击并掩盖攻击者的真实身份。无法保证提供给 JMAP 服务器的 URL 确实是有效的推送服务器。在创建推送订阅时,JMAP 服务器向该 URL 发送一个 PushVerification 对象,并且在客户端验证其已收到初始推送之前,不得发送任何进一步请求。验证码必须包含足够的熵,以防止客户端能够通过暴力验证该订阅。

验证码并不能保证该 URL 是有效的推送服务器,只能保证客户端能够访问提交给它的数据。虽然验证步骤显著减少了潜在目标集合,但仍然存在该服务器与客户无关、并被作为拒绝服务攻击目标的风险。

服务器必须限制任何单一用户可拥有的推送订阅数量,以确保该用户无法使服务器一次性发送大量推送通知,而这同样可能被用作拒绝服务攻击的一部分。创建速率也必须受限,以尽量降低将验证请求作为攻击载体滥用的能力。

8.7. 推送加密

当数据发生变化时,会推送一个包含已变更类型新状态字符串的小对象。虽然这里的数据最少,但被动的中间人攻击者可能获得有用的信息。为确保机密性与完整性,如果推送是通过客户端和 JMAP 服务器控制范围之外的第三方发送的,客户端必须在建立 PushSubscription 时指定加密密钥,并忽略任何未用这些密钥加密的推送通知。

[RFC8030] 和 [RFC8291] 的隐私与安全考量也适用于 PushSubscription 机制的使用。

由于 Web Push Encryption [RFC8291] 中没有加密算法敏捷性,如果未来需要新算法,将需要一个新的规范来提供。

8.8. 流量分析

虽然数据是加密的,但具有监控网络流量能力的被动观察者可能能够从 API 请求和推送通知的时序中获得信息。例如,假设一封电子邮件或日历邀请从用户 A(托管在服务器 X 上)发送给用户 B(托管在服务器 Y 上)。如果服务器 X 为许多用户托管数据,被动观察者可以看到两台服务器建立了连接,但不知道数据是为谁准备的。然而,如果立即向用户 B 发送了推送通知,且攻击者也能观察到这一点,他们可以合理地推断服务器 X 上有人正在连接到用户 B。

9. IANA 考量

9.1. 分配 jmap 服务名

IANA 已在"服务名称与传输协议端口号注册表"[RFC6335] 中分配了 'jmap' 服务名。

9.2. 注册 JMAP 的 Well-Known URI 后缀

如 [RFC8615] 所述,IANA 已在"Well-Known URIs"注册表中为 JMAP 注册了以下后缀:

9.3. 注册 jmap URN 子命名空间

如 [RFC3553] 所述,IANA 已在"Uniform Resource Name (URN) Namespace for IETF Use"注册表中的"IETF URN Sub-namespace for Registered Protocol Parameter Identifiers"注册表里,注册了以下 URN 子命名空间:

9.4. 创建"JMAP Capabilities"注册表

IANA 已按第 2 节所述创建了"JMAP Capabilities"注册表。JMAP 能力在 JMAP 会话资源的 "capabilities" 属性中通告。它们用于扩展 JMAP 服务器的功能。能力通过 URI 引用。JMAP 能力 URI 可以是一个以 "urn:ietf:params:jmap:" 开头、加上作为 jmap URN 子命名空间中索引值的唯一后缀的 URN。以其他形式 URI 注册 JMAP 能力对 jmap URN 子命名空间没有影响。

除非"intended use(预期用途)"字段为 "common" 或 "placeholder",否则该注册表遵循专家评审流程;在这两种情况下,注册遵循规范要求的流程。

JMAP 能力注册的预期用途可以是 "common"、"placeholder"、"limited" 或 "obsolete"。IANA 将把 common-use 注册与其他预期用途值的注册分开、突出列出。

JMAP 能力注册流程不是正式的 standards 流程,而是一种行政流程,旨在允许社区评论和健全性检查,而不会有过多的时间延迟。

"placeholder"注册为 jmap URN 命名空间的另一用途保留了一部分,但通常不包含在 JMAP 会话资源的 "capabilities" 属性中。

9.4.1. 初步社区评审

潜在 JMAP common-use 注册的通知应发送到 JMAP 邮件列表 <jmap@ietf.org> 进行评审。该邮件列表适合就提议的 JMAP 能力征集社区反馈。不打算用于 common-use 的注册也可以发送到该列表进行评审;这样做完全可选,但受到鼓励。

公开发布到该列表的目的是就能力名称的选择、规范文档的明确性,以及任何互操作性或安全考量的审查征集评论和反馈。提交者可以在任何时候提交修订的注册提案,或完全放弃注册。

9.4.2. 向 IANA 提交请求

注册请求可发送到 <iana@iana.org>。

9.4.3. 指定专家评审

对于 limited-use 注册,指定专家(DE)主要关注防止名称冲突,并鼓励提交者记录安全与隐私考量;不要求已发布的规范。对于 common-use 注册,DE 应确认存在 [RFC8126] 第 4.6 节所述的合适文档。DE 还应核实该能力不与 IETF 内活跃或已发布的工作冲突。

在 30 天期限过去之前,DE 将批准或拒绝注册请求,并向 JMAP WG 邮件列表或其继任者发布决定通知,同时通知 IANA。拒绝通知必须有解释作为理由,并且在可能的情况下,应提供关于如何修改请求以使其可接受的切实建议。

如果 DE 在 30 天内未响应,注册者可以请求 IESG 采取行动以及时处理该请求。

9.4.4. 变更流程

一旦 JMAP 能力已由 IANA 发布,变更控制者可以请求对其定义进行变更。处理变更请求时使用与原始注册请求适当的相同流程。

JMAP 能力注册不得删除;不再被认为适合使用的能力可以通过将其 "intended use" 字段变更为 "obsolete" 来声明废弃;此类能力将在 IANA 发布的列表中明确标记。

只有当已发布规范存在严重遗漏或错误时,才应请求对能力定义进行重大变更。当需要评审时,如果变更请求会使在先前定义下有效的实体在新定义下无效,则可能被拒绝。

JMAP 能力的所有者可以通过通知 IANA,将责任移交给另一个人或机构;这可以在没有讨论或评审的情况下完成。

IESG 可以重新分配 JMAP 能力的责任。最常见的情况是将责任分配给那些注册作者已去世、失去联系,或以其他方式无法做出对社区重要的变更的能力。

9.4.5. JMAP Capabilities 注册表模板

9.4.6. JMAP Core 的初始注册

9.4.7. 在 JMAP Capabilities 注册表中为 JMAP 错误占位符注册

9.5. 创建"JMAP Error Codes"注册表

IANA 已创建"JMAP Error Codes"注册表。JMAP 错误码出现在 JSON problem details 对象(如第 3.6.1 节所述)的 "type" 成员中、JMAP 错误对象(如第 3.6.2 节所述)的 "type" 成员中,或 JMAP 方法特定错误对象(例如第 5.3 节的 SetError)的 "type" 成员中。当用于 problem details 对象时,始终包含前缀 "urn:ietf:params:jmap:error:";当用于 JMAP 对象时,始终省略该前缀。

该注册表遵循专家评审流程。该注册表的初步社区评审遵循与"JMAP Capabilities"注册表相同的流程,但是可选的。该注册表的变更流程与"JMAP Capabilities"注册表的变更流程相同。

9.5.1. 专家评审

指定专家应评审注册的以下方面:

  1. 核实错误码不与现有名称冲突。
  2. 核实错误码遵循语法限制(不需要 URI 编码)。
  3. 鼓励提交者遵循先前注册错误的命名约定。
  4. 鼓励提交者描述建议针对该错误码做出的客户端行为。这些可能将该错误码与其他错误码区分开来。
  5. 鼓励提交者描述服务器应在何时发出该错误码,而非某些其他错误码。
  6. 鼓励提交者注明与该错误相关的任何安全考量(如果有)(例如,可能泄露经过认证用户无权知晓的数据是否存在的错误码)。

第 3-6 步旨在促进更高质量的注册表。然而,鼓励专家批准任何不会主动损害 JMAP 互操作性的注册,以使这成为一个相对轻量的流程。

9.5.2. JMAP Error Codes 注册表模板

9.5.3. JMAP Error Codes 注册表的初始内容

错误码预期用途变更控制者参考描述
accountNotFoundCommonIETFRFC 8620,第 3.6.2 节accountId 不与有效的账户相对应。
accountNotSupportedByMethodCommonIETFRFC 8620,第 3.6.2 节给定的 accountId 对应于一个有效的账户,但该账户不支持此方法或数据类型。
accountReadOnlyCommonIETFRFC 8620,第 3.6.2 节此方法修改状态,但账户是只读的(如 JMAP 会话资源的相应 Account 对象所返回)。
anchorNotFoundCommonIETFRFC 8620,第 5.5 节提供了 anchor 参数,但在查询结果中找不到它。
alreadyExistsCommonIETFRFC 8620,第 5.4 节服务器禁止重复,且记录已存在于目标账户中。SetError 对象上必须包含一个类型为 Id 的 existingId 属性,带有现有记录的 id。
cannotCalculateChangesCommonIETFRFC 8620,第 5.2 与 5.6 节服务器无法根据客户端给出的状态字符串计算变更。
forbiddenCommonIETFRFC 8620,第 3.6.2、5.3 与 7.2.1 节该操作会违反 ACL 或其他权限策略。
fromAccountNotFoundCommonIETFRFC 8620,第 5.4 与 6.3 节fromAccountId 不与有效的账户相对应。
fromAccountNotSupportedByMethodCommonIETFRFC 8620,第 5.4 节给定的 fromAccountId 对应于一个有效的账户,但该账户不支持此数据类型。
invalidArgumentsCommonIETFRFC 8620,第 3.6.2 节某个参数的类型错误或以其他方式无效,或者缺少必需的参数。
invalidPatchCommonIETFRFC 8620,第 5.3 节用于更新记录的 PatchObject 不是有效的 patch。
invalidPropertiesCommonIETFRFC 8620,第 5.3 节给定的记录无效。
notFoundCommonIETFRFC 8620,第 5.3 节给定的 id 找不到。
notJSONCommonIETFRFC 8620,第 3.6.1 节请求的内容类型不是 application/json,或请求未能解析为 I-JSON。
notRequestCommonIETFRFC 8620,第 3.6.1 节请求解析为 JSON,但与 Request 对象的类型签名不匹配。
overQuotaCommonIETFRFC 8620,第 5.3 节创建会超出服务器定义的、该类型对象数量或总大小的限制。
rateLimitCommonIETFRFC 8620,第 5.3 节最近创建了过多该类型的对象,达到了服务器定义的速率限制。稍后重试可能成功。
requestTooLargeCommonIETFRFC 8620,第 5.1 与 5.3 节动作总数超过了服务器愿意在单个方法调用中处理的最大数量。
invalidResultReferenceCommonIETFRFC 8620,第 3.6.2 节该方法为其某个参数使用了结果引用,但解析失败。
serverFailCommonIETFRFC 8620,第 3.6.2 节在处理调用期间发生了意外或未知的错误。该方法调用未对服务器状态做出任何更改。
serverPartialFailLimitedIETFRFC 8620,第 3.6.2 节描述的预期变更中有一部分(而非全部)发生了。客户端必须重新同步受影响的数据以确定服务器状态。强烈不建议使用此错误。
serverUnavailableCommonIETFRFC 8620,第 3.6.2 节某个内部服务器资源临时不可用。稍后(也许在带随机退避因子后)重试同一操作可能会成功。
singletonCommonIETFRFC 8620,第 5.3 节这是一个单例类型,因此你不能创建另一个,也不能销毁现有的这一个。
stateMismatchCommonIETFRFC 8620,第 5.3 节提供了 ifInState 参数,但它与当前状态不匹配。
tooLargeCommonIETFRFC 8620,第 5.3 节该操作会导致一个超出服务器定义的、该类型单个对象最大大小限制的对象。
tooManyChangesCommonIETFRFC 8620,第 5.6 节变更数量超过了客户端的 maxChanges 参数。
unknownCapabilityCommonIETFRFC 8620,第 3.6.1 节客户端在请求的 "using" 属性中包含了一个服务器不支持的能力。
unknownMethodCommonIETFRFC 8620,第 3.6.2 节服务器无法识别该方法名。
unsupportedFilterCommonIETFRFC 8620,第 5.5 节过滤器在语法上有效,但服务器无法处理。
unsupportedSortCommonIETFRFC 8620,第 5.5 节排序在语法上有效,但包含服务器不支持排序的属性,或它不认识的 collation 方法。
willDestroyCommonIETFRFC 8620,第 5.3 节客户端请求在同一 /set 请求中既更新又销毁某个对象,服务器因此决定忽略该更新。

10. 参考文献

10.1. 规范性参考文献

10.2. 资料性参考文献

作者地址

Neil Jenkins
Fastmail
PO Box 234, Collins St. West
Melbourne, VIC 8007
Australia
Email: neilj@fastmailteam.com

Chris Newman
Oracle
Email: chris.newman@oracle.com