非官方中文译本声明:本页为 IETF RFC 8621《The JSON Meta Application Protocol (JMAP) for Mail(JMAP 邮件)》 的中文译本,由 ztpop.net 整理翻译,仅供学习参考。RFC 文档由 IETF 发布,受 BCP 78 与 IETF 信托法律条款约束;本译本保留原文编号与结构,权威性以英文原文为准。英文原文见 rfc-editor.org/rfc/rfc8621。
RFC 8621:JMAP Mail(邮件)
摘要
本文档规定了一种使用 JSON 元数据应用协议(JMAP)与服务器同步邮件数据的数据模型。客户端可借此高效地搜索、访问、组织并发送消息,并在新消息送达或其他客户端做出变更时获取推送通知,从而实现快速重新同步。
本备忘录的状态
本文是一份互联网标准跟踪(Standards Track)文档。
本文是互联网工程任务组(IETF)的成果,代表 IETF 社区的共识,已经过公开评审,并由互联网工程指导组(IESG)批准发布。有关互联网标准的更多信息见 RFC 7841 第 2 节。
关于本文当前状态、任何勘误以及如何提供反馈的信息,可访问 https://www.rfc-editor.org/info/rfc8621。
版权声明
Copyright (c) 2019 IETF 信托及被列为文档作者的个人。保留所有权利。
本文受 BCP 78 以及 IETF 信托的《IETF 文档相关法律规定》(https://trustee.ietf.org/license-info)约束,以本文档发布之日生效的版本为准。请仔细审阅这些文档,因为它们描述了您就本文档所享有的权利与限制。从本文档中提取的代码组件必须包含《简化 BSD 许可证》文本(见信托法律条款第 4.e 节),并按该许可证的描述"不提供任何担保"。
目录
- 1. 引言
- 1.1. 符号约定
- 1.2. 术语
- 1.3. 对 Capabilities 对象的增补
- 1.3.1. urn:ietf:params:jmap:mail
- 1.3.2. urn:ietf:params:jmap:submission
- 1.3.3. urn:ietf:params:jmap:vacationresponse
- 1.4. 不同账户中的数据类型支持
- 1.5. 推送
- 1.5.1. 示例
- 1.6. Ids
- 2. 邮箱(Mailboxes)
- 2.1. Mailbox/get
- 2.2. Mailbox/changes
- 2.3. Mailbox/query
- 2.4. Mailbox/queryChanges
- 2.5. Mailbox/set
- 2.6. 示例
- 3. 会话线索(Threads)
- 3.1. Thread/get
- 3.1.1. 示例
- 3.2. Thread/changes
- 3.1. Thread/get
- 4. 电子邮件(Emails)
- 4.1. Email 对象的属性
- 4.1.1. 元数据
- 4.1.2. 头字段解析形式
- 4.1.2.1. 原始(Raw)
- 4.1.2.2. 文本(Text)
- 4.1.2.3. 地址(Addresses)
- 4.1.2.4. 分组地址(GroupedAddresses)
- 4.1.2.5. 消息标识(MessageIds)
- 4.1.2.6. 日期(Date)
- 4.1.2.7. URL
- 4.1.3. 头字段属性
- 4.1.4. 正文部分(Body Parts)
- 4.2. Email/get
- 4.2.1. 示例
- 4.3. Email/changes
- 4.4. Email/query
- 4.4.1. 过滤
- 4.4.2. 排序
- 4.4.3. 会话线索折叠
- 4.5. Email/queryChanges
- 4.6. Email/set
- 4.7. Email/copy
- 4.8. Email/import
- 4.9. Email/parse
- 4.10. 示例
- 4.1. Email 对象的属性
- 5. 搜索摘要(Search Snippets)
- 5.1. SearchSnippet/get
- 5.2. 示例
- 6. 身份(Identities)
- 6.1. Identity/get
- 6.2. Identity/changes
- 6.3. Identity/set
- 6.4. 示例
- 7. 邮件提交(Email Submission)
- 7.1. EmailSubmission/get
- 7.2. EmailSubmission/changes
- 7.3. EmailSubmission/query
- 7.4. EmailSubmission/queryChanges
- 7.5. EmailSubmission/set
- 7.5.1. 示例
- 8. 休假回复(Vacation Response)
- 8.1. VacationResponse/get
- 8.2. VacationResponse/set
- 9. 安全考量
- 9.1. EmailBodyPart 值
- 9.2. HTML 邮件显示
- 9.3. 多部分显示
- 9.4. 邮件提交
- 9.5. 部分账户访问
- 9.6. 从某地址发送的权限
- 10. IANA 考量
- 10.1. "mail" 的 JMAP 能力注册
- 10.2. "submission" 的 JMAP 能力注册
- 10.3. "vacationresponse" 的 JMAP 能力注册
- 10.4. IMAP 与 JMAP 关键字注册表
- 10.4.1. JMAP 关键字 "$draft" 的注册
- 10.4.2. JMAP 关键字 "$seen" 的注册
- 10.4.3. JMAP 关键字 "$flagged" 的注册
- 10.4.4. JMAP 关键字 "$answered" 的注册
- 10.4.5. "$recent" 关键字的注册
- 10.5. IMAP 邮箱名称属性注册表
- 10.5.1. "inbox" 角色的注册
- 10.6. JMAP 错误代码注册表
- 10.6.1. mailboxHasChild
- 10.6.2. mailboxHasEmail
- 10.6.3. blobNotFound
- 10.6.4. tooManyKeywords
- 10.6.5. tooManyMailboxes
- 10.6.6. invalidEmail
- 10.6.7. tooManyRecipients
- 10.6.8. noRecipients
- 10.6.9. invalidRecipients
- 10.6.10. forbiddenMailFrom
- 10.6.11. forbiddenFrom
- 10.6.12. forbiddenToSend
- 11. 参考文献
- 11.1. 规范性参考文献
- 11.2. 资料性参考文献
- 作者地址
1. 引言
JSON 元数据应用协议(JMAP)[RFC8620] 是一种通用协议,用于在客户端与服务器之间同步诸如邮件、日历或联系人之类的数据。它针对移动与 Web 环境进行了优化,旨在为不同数据类型提供一致的接口。
本规范定义了通过 JMAP 访问邮件存储的数据模型,允许你查询、读取、组织邮件并提交以发送。
该数据模型的设计使得服务器能够通过 IMAP [RFC3501] 以及 JMAP 提供对同一数据的一致访问。与 IMAP 中一样,一封消息必须属于某个邮箱;但在 JMAP 中,若将其在邮箱间移动,其 id 不会改变,并且服务器可允许它同时属于多个邮箱(在用户代理中通常表现为标签而非文件夹)。
与 IMAP 中一样,消息可被赋予零个或多个关键字:即简短的任意字符串。这些主要用于存储告知客户端显示的元数据,例如未读状态,或消息是否已被回复。一个 IANA 注册表使得通用语义能够在客户端之间共享,并便于未来扩展。
一条消息及其回复在服务器上通过共同的 Thread(会话线索)id 关联起来。客户端可获取具有特定 Thread id 的消息列表,以便更轻松地呈现线程化或会话式界面。
消息访问的权限基于每个邮箱进行。服务器可对用户授予受限的邮箱权限,例如,若另一用户的收件箱以只读方式与其共享。
1.1. 符号约定
本文件中的关键词"MUST(必须)"、"MUST NOT(不得)"、"REQUIRED(要求)"、"SHALL(应)"、"SHALL NOT(不应)"、"SHOULD(应该)"、"SHOULD NOT(不应该)"、"RECOMMENDED(推荐)"、"NOT RECOMMENDED(不推荐)"、"MAY(可以)"和"OPTIONAL(可选)",当且仅当它们以如这里所示的全部大写字母出现时,应按照 BCP 14 [RFC2119] [RFC8174] 中的描述进行解释。
本文档中的类型签名、示例与属性描述遵循 [RFC8620] 第 1.1 节所确立的约定。核心规范中定义的数据类型在本文档中同样使用。
服务器必须支持本文档为新数据类型规定的所有属性。
1.2. 术语
本文档使用与核心 JMAP 规范相同的术语。
Mailbox、Thread、Email、SearchSnippet、EmailSubmission 与 VacationResponse(采用该特定大写形式)等术语用于指代本文档中定义的数据类型以及这些数据类型的实例。
术语"message(消息)"指 internet message format(互联网报文格式,见 [RFC5322])中的文档。Email 数据类型表示邮件存储中的消息及关联元数据。
1.3. 对 Capabilities 对象的增补
capabilities 对象作为 JMAP 会话对象的一部分返回,见 [RFC8620] 第 2 节。
本文档定义了三个额外的能力 URI。
1.3.1. urn:ietf:params:jmap:mail
它表示对 Mailbox、Thread、Email 与 SearchSnippet 数据类型及相关 API 方法的支持。在 JMAP 会话的 "capabilities" 属性中,该属性的值为一个空对象。
在某个账户的 "accountCapabilities" 属性中,该属性的值是一个对象,必须包含关于该账户的服务器能力与权限的以下信息:
- maxMailboxesPerEmail: "UnsignedInt|null"
- 可分配给单个 Email 对象(见第 4 节)的 Mailbox(见第 2 节)的最大数量。该值必须是一个整数 >= 1,或为 null 表示无限制(更准确地说,限制始终为该账户中 Mailbox 的数量)。
- maxMailboxDepth: "UnsignedInt|null"
- Mailbox 层次结构的最大深度(即一个 Mailbox 可能拥有的祖先数量加一),或为 null 表示无限制。
- maxSizeMailboxName: "UnsignedInt"
- Mailbox 名称所允许的最大长度(以 UTF-8 八位组计)。该值必须至少为 100,不过建议服务器允许更大值。
- maxSizeAttachmentsPerEmail: "UnsignedInt"
- 单个 Email 对象所允许的附件总大小(以八位组计)。服务器仍可能拒绝导入或创建附件总大小更低(例如,若正文包含若干兆字节文本,使得编码后 MIME 结构的大小超过服务器定义的某个限制)的 Email。
注意,此限制针对的是未编码附件大小的总和。用户通常不了解编码开销等细节,也不应需要了解,因此营销与帮助材料通常告诉他们的是"最大附件大小"。这是他们在硬盘上看到的未编码大小,因此该能力与之一致,并允许客户端一致地强制执行为用户所理解的限制。
服务器可能另行对消息 [RFC5322] 的总大小(由附件(通常为 base64 编码)与消息头及正文组合而成)设限。例如,假设服务器通告 "maxSizeAttachmentsPerEmail: 50000000"(50 MB)。强制执行的服务器限制可能是针对 70000000 八位组的消息大小。即便有 base64 编码与 2 MB 的 HTML 正文,50 MB 附件也能落在该限制之下。
- emailQuerySortOptions: "String[]"
- 服务器在 "Email/query" 排序的 Comparator 对象的 "property" 字段所支持的全部取值列表(见第 4.4.2 节)。其中可能包含客户端无法识别的属性(例如厂商扩展中规定的自定义属性)。客户端必须忽略列表中的任何未知属性。
- mayCreateTopLevelMailbox: "Boolean"
- 若为 true,用户可在本账户中创建 parentId 为 null 的 Mailbox(见第 2 节)。(创建现有 Mailbox 子级的权限由该 Mailbox 的 "myRights" 属性给出。)
1.3.2. urn:ietf:params:jmap:submission
它表示对 Identity 与 EmailSubmission 数据类型及相关 API 方法的支持。在 JMAP 会话的 "capabilities" 属性中,该属性的值为一个空对象。
在某个账户的 "accountCapabilities" 属性中,该属性的值是一个对象,必须包含关于该账户的服务器能力与权限的以下信息:
- maxDelayedSend: "UnsignedInt"
- 服务器在发送(见 EmailSubmission 对象描述)时所支持的最大延迟秒数。若服务器不支持延迟发送,则该值为 0。
- submissionExtensions: "String[String[]]"
- 服务器支持的 SMTP 提交扩展集合,客户端在创建 EmailSubmission 对象(见第 7 节)时可使用。对象中的每个键都是 "ehlo-name",值为 "ehlo-args" 列表。
与提交服务器 [RFC6409] 通信的 JMAP 实现应当具有一个配置项,允许管理员修改其可在此属性上暴露的提交 EHLO 能力集合。这使得 JMAP 服务器无需修改代码即可轻松添加对新提交扩展的访问。默认情况下,JMAP 服务器应隐藏与传输机制相关、因而仅与 JMAP 服务器有关的 EHLO 能力(例如 PIPELINING、CHUNKING 或 STARTTLS)。
可包含的提交扩展示例:
- FUTURERELEASE [RFC4865]
- SIZE [RFC1870]
- DSN [RFC3461]
- DELIVERYBY [RFC2852]
- MT-PRIORITY [RFC6710]
即便 JMAP 所使用的提交服务器未实现某扩展,JMAP 服务器也可通告该扩展并在 JMAP 服务器本地实现其语义。
提交扩展的完整 IANA 注册表可在 <https://www.iana.org/assignments/mail-parameters> 找到。
1.3.3. urn:ietf:params:jmap:vacationresponse
它表示对 VacationResponse 数据类型及相关 API 方法的支持。在 JMAP 会话的 "capabilities" 属性与账户的 "accountCapabilities" 属性中,该属性的值均为空对象。
1.4. 不同账户中的数据类型支持
对于任何用户可使用由该 URI 所代表的数据类型的账户,服务器必须在该账户的 "accountCapabilities" 属性中包含相应的能力字符串作为键。受支持的数据类型在用户可访问的不同账户之间可能不同。例如,在用户个人账户中,他们可能可以访问全部三组数据,但在一个共享账户中,他们可能只能访问 "urn:ietf:params:jmap:mail" 的数据。这意味着他们可以在共享账户中访问 Mailbox/Thread/Email 数据,但不被允许以该账户身份发送(因此没有 Identity/EmailSubmission 对象的访问权限),也无法查看/设置其 VacationResponse。
1.5. 推送
服务器必须支持 JMAP 推送机制,如 [RFC8620] 第 7 节所规定,以便在本规范定义的任何类型状态发生变化时接收通知。
此外,实现了 "urn:ietf:params:jmap:mail" 能力的服务器必须支持为名为 "EmailDelivery" 的类型推送状态变化。没有可作用于该类型的方法;它仅作为推送机制的一部分存在。该类型的状态字符串必须在有新 Email 加入存储时改变,但不应在 Email 对象发生任何其他变更时改变,例如当它被标记为已读或被删除时。
处于电量受限环境中的客户端可能希望延迟获取由用户发起的变更,但立即获取新 Email 以便通知用户。为此,它们可以为 EmailDelivery 类型(而非第 4 节定义的 Email 类型)注册推送。
1.5.1. 示例
客户端已(见 [RFC8620])仅针对 EmailDelivery 类型注册了推送通知。用户在另一台设备上将某封 Email 标记为已读,导致 Email 类型的状态字符串改变;但由于存储中未新增任何内容,EmailDelivery 状态未改变,也没有任何内容被推送到客户端。一封新消息到达用户的收件箱,再次导致 Email 状态改变。这一次,EmailDelivery 状态也发生改变,一个 StateChange 对象被推送到客户端,并带有新的状态字符串。客户端随后可立即重新同步以获取新 Email。
1.6. Ids
若某个 JMAP 邮件服务器同时提供数据的 IMAP 接口,并且支持 IMAP 对象标识符扩展 [RFC8474],则 Mailbox、Thread 与 Email 对象在 JMAP 中的 id 应当相同。
2. 邮箱(Mailboxes)
一个 Mailbox 表示一组具名的 Email 对象。这是在账户内组织消息的主要机制,类似于其他系统中的文件夹或标签。一个 Mailbox 可能在系统中扮演某种角色;详见下文。
为与 IMAP 兼容,一封 Email 必须属于一个或多个 Mailbox。若 Email 更改所属 Mailbox,其 Email id 不变。
一个 *Mailbox* 对象具有以下属性:
- id: "Id"(不可变;服务器设定)
- Mailbox 的 id。
- name: "String"
- 用户可见的 Mailbox 名称,例如 "Inbox"。这必须是长度至少为 1 个字符的 Net-Unicode 字符串 [RFC5198],并受能力对象中给出的最大大小限制。不得存在两个具有相同父级与相同名称的兄弟 Mailbox。服务器可能拒绝违反服务器策略的名称(例如包含斜杠(/)或控制字符的名称)。
- parentId: "Id|null"(默认:null)
- 本 Mailbox 的父级 Mailbox 的 id,若本 Mailbox 位于顶层则为 null。Mailbox 构成由子到父关系导向的无环图(森林)。不得存在环。
- role: "String|null"(默认:null)
- 标识具有特定通用用途(例如 "inbox")的 Mailbox,而不论 "name" 属性(其可能被本地化)如何。
该值与 IMAP 共享(通过 SPECIAL-USE 扩展 [RFC6154] 在 IMAP 中暴露)。但与 IMAP 不同的是,一个 Mailbox 必须具有单一角色,并且同一账户中不得有两个具有相同角色的 Mailbox。提供 IMAP 访问同一数据的服务器被鼓励在 IMAP 中也强制实施这些额外限制。否则,当通过 JMAP 暴露数据时为确保合规而修改 IMAP 属性,取决于具体实现。
该值必须是 [RFC8457] 确立的 IANA "IMAP Mailbox Name Attributes" 注册表(位于 <https://www.iana.org/assignments/imap-mailbox-name-attributes/>)中列出的某个 Mailbox 属性名称,并转换为小写。未来可能在此处确立新角色。
不要求账户拥有任何具有特定角色的 Mailbox。
- sortOrder: "UnsignedInt"(默认:0)
- 定义客户端 UI 中呈现 Mailbox 时的排序顺序,以便在设备之间保持一致。该数字必须是在 0 <= sortOrder < 2^31 范围内的整数。
排序顺序较小的 Mailbox 应当显示在排序顺序较大(且具有相同父级)的 Mailbox 之前,于客户端 UI 的任何 Mailbox 列表中。排序顺序相等的 Mailbox 应当按名称的字母顺序排序。排序应考虑区域特定的字符顺序约定。
- totalEmails: "UnsignedInt"(服务器设定)
- 本 Mailbox 中 Email 的数量。
- unreadEmails: "UnsignedInt"(服务器设定)
- 本 Mailbox 中既没有 "$seen" 关键字也没有 "$draft" 关键字的 Email 数量。
- totalThreads: "UnsignedInt"(服务器设定)
- 至少有一个 Email 位于本 Mailbox 中的 Thread 的数量。
- unreadThreads: "UnsignedInt"(服务器设定)
- 对 Mailbox 中"未读"Thread 数量的指示。
为与现有实现兼容,"未读 Thread"的判定方式本文档不作强制。最简单的实现方案就是:至少有一个 Email 既在本 Mailbox 中、又不带 "$seen" 与 "$draft" 关键字的 Thread 的数量。
然而,一个高质量的实作会返回用户若打开该 Mailbox 将看到的未读项数量。若某个 Thread 包含任何在打开该 Thread 时将被显示的未读 Email,则该 Thread 显示为未读。因此,"unreadThreads" 应为:至少有一个 Email 既在本 Mailbox 中、又不带 "$seen" 与 "$draft" 关键字,并且该 Thread 中至少有一个 Email 位于本 Mailbox 中的 Thread 数量。注意未读 Email 不必是处于本 Mailbox 中的那一个。此外,回收站 Mailbox(即 "role" 为 "trash" 的 Mailbox)需要特殊处理:
- 仅为计算其他 Mailbox 的 "unreadThreads" 计数时,那些*仅*在回收站(且不在其他 Mailbox)中的 Email 被忽略。
- 仅为计算回收站 Mailbox 的 "unreadThreads" 计数时,不在回收站中的 Email 被忽略。
其结果是,就未读计数而言,回收站中的 Email 被视为处于一个独立的 Thread 中。预期客户端在查看另一 Mailbox 中的某个 Thread 时会隐藏回收站中的 Email,反之亦然。这允许你从某个 Thread 中删除单封 Email 到回收站。
例如,假设你的账户的全部内容是一个包含 2 封 Email 的 Thread:一封在回收站中的未读 Email 与一封在收件箱中的已读 Email。则回收站的 "unreadThreads" 计数为 1,收件箱的计数为 0。
- myRights: "MailboxRights"(服务器设定)
- 用户相对于本 Mailbox 所拥有的权利集合(访问控制列表,ACL)。这些与 [RFC4314] 定义的 IMAP ACL 向后兼容。*MailboxRights* 对象具有以下属性:
- mayReadItems: "Boolean"
- 若为 true,用户可在 "Email/query" 调用中将该 Mailbox 用作过滤器的一部分,并且该 Mailbox 可被包含在 Email 对象的 "mailboxIds" 属性中。若 Email 位于*至少*一个具有此权限的 Mailbox 中,则可被获取。若子 Mailbox 被共享而父 Mailbox 未被共享,此值可能为 false。对应 IMAP ACL 的 "lr"(若从 IMAP 映射,二者皆需为真才使此为真)。
- mayAddItems: "Boolean"
- 用户可向本 Mailbox 添加邮件(通过创建新 Email 或移动已有 Email)。对应 IMAP ACL 的 "i"。
- mayRemoveItems: "Boolean"
- 用户可从本 Mailbox 移除邮件(通过更改 Email 的 Mailbox 或销毁该 Email)。对应 IMAP ACL 的 "te"(若从 IMAP 映射,二者皆需为真才使此为真)。
- maySetSeen: "Boolean"
- 用户可向 Email 添加或移除 "$seen" 关键字。若某 Email 属于多个 Mailbox,用户仅在对*所有*这些 Mailbox 都拥有此权限时才能修改 "$seen"。对应 IMAP ACL 的 "s"。
- maySetKeywords: "Boolean"
- 用户可向 Email 添加或移除除 "$seen" 之外的任何关键字。若某 Email 属于多个 Mailbox,用户仅在对*所有*这些 Mailbox 都拥有此权限时才能修改关键字。对应 IMAP ACL 的 "w"。
- mayCreateChild: "Boolean"
- 用户可创建一个以本 Mailbox 为父级的 Mailbox。对应 IMAP ACL 的 "k"。
- mayRename: "Boolean"
- 用户可重命名该 Mailbox 或使其成为另一 Mailbox 的子级。对应 IMAP ACL 的 "x"(尽管此覆盖重命名与删除权限二者)。
- mayDelete: "Boolean"
- 用户可删除 Mailbox 本身。对应 IMAP ACL 的 "x"(尽管此覆盖重命名与删除权限二者)。
- maySubmit: "Boolean"
- 消息可直接提交到本 Mailbox。对应 IMAP ACL 的 "p"。
- mayReadItems: "Boolean"
- 用户相对于本 Mailbox 所拥有的权利集合(访问控制列表,ACL)。这些与 [RFC4314] 定义的 IMAP ACL 向后兼容。*MailboxRights* 对象具有以下属性:
- isSubscribed: "Boolean"
- 用户是否表明希望在客户端中看到此 Mailbox?对于用户可访问的共享账户中的 Mailbox,此值应默认为 false;对于用户自己创建的任何新 Mailbox,应默认为 true。在多个用户可访问共享 Mailbox 的情况下,此值必须按用户分别存储。
用户可能拥有访问大量共享账户的权限,或拥有一个包含极多 Mailbox 的共享账户,但只对其中少数内容感兴趣。客户端可选择仅显示 "isSubscribed" 属性为 true 的 Mailbox,并提供单独的 UI 让用户查看全部 Mailbox 集合并进行订阅/取消订阅。然而,客户端也可选择忽略此属性,要么为便于实现而整体忽略,要么仅对 "isPersonal" 为 true 的账户忽略(表明这是用户自己的账户而非共享账户)。
此属性对应 IMAP [RFC3501] 的邮箱订阅。
为与 IMAP 兼容,同时属于回收站与另一 Mailbox 的 Email,客户端应将其视为同时存在于两处(即清空回收站时,客户端应仅将其从回收站 Mailbox 中移除,而保留在另一 Mailbox 中)。
支持以下 JMAP 方法。
2.1. Mailbox/get
这是 [RFC8620] 第 5.1 节所述的标准 "/get" 方法。"ids" 参数可以为 "null" 以一次性获取全部。
2.2. Mailbox/changes
这是 [RFC8620] 第 5.2 节所述的标准 "/changes" 方法,但响应中带有一个额外参数:
- updatedProperties: "String[]|null"
- 若自旧状态以来仅 "totalEmails"、"unreadEmails"、"totalThreads" 和/或 "unreadThreads" Mailbox 属性发生了变化,则这将为可能已变化的属性列表。若服务器无法判断仅有计数发生变化,则必须为 null。
由于计数频繁变化而其它属性通常很少更改,服务器可通过将 Email/Thread 计数变更与其它状态变更分开记录,来帮助客户端优化数据传输。"updatedProperties" 数组可通过同一请求中的反向引用直接在后续的 "Mailbox/get" 调用中使用,因此若其它内容未变,则仅返回这些属性。
2.3. Mailbox/query
这是 [RFC8620] 第 5.5 节所述的标准 "/query" 方法,但带有以下额外请求参数:
- sortAsTree: "Boolean"(默认:false)
- 若为 true,在对查询结果排序并比较 Mailbox A 与 B 时:
- 若 A 是 B 的祖先,则不论排序比较器如何,A 始终排在前面。类似地,若 A 是 B 的后代,则 B 始终排在前面。
- 否则,若 A 与 B 不共享 "parentId",则分别找到二者中具有相同 "parentId" 的最近祖先,并改为比较那些 Mailbox 上的排序属性。
- 若为 true,在对查询结果排序并比较 Mailbox A 与 B 时:
其结果是,Mailbox 按照 parentId 属性作为树排序,每个具有共同父级的子级集合按照标准排序比较器排序。
- filterAsTree: "Boolean"(默认:false)
- 若为 true,则只有当某 Mailbox 的所有祖先也按照过滤器被纳入查询结果时,该 Mailbox 才被包含在内。
一个 *FilterCondition* 对象具有以下属性,均可省略:
- parentId: "Id|null"
- Mailbox 的 "parentId" 属性必须与给定值精确匹配。
- name: "String"
- Mailbox 的 "name" 属性包含给定字符串。
- role: "String|null"
- Mailbox 的 "role" 属性必须与给定值精确匹配。
- hasAnyRole: "Boolean"
- 若为 true,则若 Mailbox 的 "role" 属性为任意非 null 值即匹配。
- isSubscribed: "Boolean"
- Mailbox 的 "isSubscribed" 属性必须与给定值完全相同才匹配该条件。
当且仅当所有给定条件都匹配时,一个 Mailbox 对象才匹配该 FilterCondition。若未指定任何属性,则对所有对象自动为真。
以下 Mailbox 属性必须支持排序:
- "sortOrder"
- "name"
2.4. Mailbox/queryChanges
这是 [RFC8620] 第 5.6 节所述的标准 "/queryChanges" 方法。
2.5. Mailbox/set
这是 [RFC8620] 第 5.3 节所述的标准 "/set" 方法,但带有以下额外请求参数:
- onDestroyRemoveEmails: "Boolean"(默认:false)
- 若为 false,任何试图销毁仍含有 Email 的 Mailbox 的操作都将被拒绝,并返回 "mailboxHasEmail" SetError。若为 true,则该 Mailbox 中原有的任何 Email 将被从中移除,并且若不在其他 Mailbox 中,则在该 Mailbox 被销毁时一并销毁。
定义了以下额外的 SetError 类型:
对于 "destroy":
- "mailboxHasChild":该 Mailbox 仍至少有一个子 Mailbox。客户端必须先移除这些子级才能删除父 Mailbox。
- "mailboxHasEmail":该 Mailbox 至少分配了一封 Email,且 "onDestroyRemoveEmails" 参数为 false。
2.6. 示例
获取账户中的所有 Mailbox:
[[ "Mailbox/get", {
"accountId": "u33084183",
"ids": null
}, "0" ]]
以及响应:
[[ "Mailbox/get", {
"accountId": "u33084183",
"state": "78540",
"list": [{
"id": "MB23cfa8094c0f41e6",
"name": "Inbox",
"parentId": null,
"role": "inbox",
"sortOrder": 10,
"totalEmails": 16307,
"unreadEmails": 13905,
"totalThreads": 5833,
"unreadThreads": 5128,
"myRights": {
"mayAddItems": true,
"mayRename": false,
"maySubmit": true,
"mayDelete": false,
"maySetKeywords": true,
"mayRemoveItems": true,
"mayCreateChild": true,
"maySetSeen": true,
"mayReadItems": true
},
"isSubscribed": true
}, {
"id": "MB674cc24095db49ce",
"name": "Important mail",
...
}, ... ],
"notFound": []
}, "0" ]]
现在假设一封 Email 被标记为已读,并且我们收到 Mailbox 状态已变化的推送更新。你可以这样获取更新:
[[ "Mailbox/changes", {
"accountId": "u33084183",
"sinceState": "78540"
}, "0" ],
[ "Mailbox/get", {
"accountId": "u33084183",
"#ids": {
"resultOf": "0",
"name": "Mailbox/changes",
"path": "/created"
}
}, "1" ],
[ "Mailbox/get", {
"accountId": "u33084183",
"#ids": {
"resultOf": "0",
"name": "Mailbox/changes",
"path": "/updated"
},
"#properties": {
"resultOf": "0",
"name": "Mailbox/changes",
"path": "/updatedProperties"
}
}, "2" ]]
这先获取已创建/更新/销毁的 Mailbox 的 id 列表,然后利用反向引用,在同一请求中获取仅新建/更新的 Mailbox 的数据。响应可能类似如下:
[[ "Mailbox/changes", {
"accountId": "u33084183",
"oldState": "78541",
"newState": "78542",
"hasMoreChanges": false,
"updatedProperties": [
"totalEmails", "unreadEmails",
"totalThreads", "unreadThreads"
],
"created": [],
"updated": ["MB23cfa8094c0f41e6"],
"destroyed": []
}, "0" ],
[ "Mailbox/get", {
"accountId": "u33084183",
"state": "78542",
"list": [],
"notFound": []
}, "1" ],
[ "Mailbox/get", {
"accountId": "u33084183",
"state": "78542",
"list": [{
"id": "MB23cfa8094c0f41e6",
"totalEmails": 16307,
"unreadEmails": 13903,
"totalThreads": 5833,
"unreadThreads": 5127
}],
"notFound": []
}, "2" ]]
下面是一个尝试重命名一个 Mailbox 并销毁另一个的示例:
[[ "Mailbox/set", {
"accountId": "u33084183",
"ifInState": "78542",
"update": {
"MB674cc24095db49ce": {
"name": "Maybe important mail"
}
},
"destroy": [ "MB23cfa8094c0f41e6" ]
}, "0" ]]
假设重命名成功,但我们没有权限销毁试图销毁的那个 Mailbox;我们可能收到:
[[ "Mailbox/set", {
"accountId": "u33084183",
"oldState": "78542",
"newState": "78549",
"updated": {
"MB674cc24095db49ce": null
},
"notDestroyed": {
"MB23cfa8094c0f41e6": {
"type": "forbidden"
}
}
}, "0" ]]
3. 会话线索(Threads)
回复与原消息被归并到一起构成一个 Thread。在 JMAP 中,一个 Thread 就是一个按日期排序的 Email 扁平列表。每封 Email 都必须属于某个 Thread,即便它是该 Thread 中唯一的 Email。
判定两封 Email 是否属于同一 Thread 的确切算法,本规范不作强制,以便与不同现有系统兼容。对于新的实现,建议当以下两个条件均满足时,两封消息属于同一 Thread:
- 在 Message-Id、In-Reply-To 与 References 头字段中的任意一个里,两封消息出现了相同的 message id [RFC5322]。
- 在去除自动添加的前缀(如 "Fwd:"、"Re:"、"[List-Tag]" 等)并忽略空白后,主题相同。这避免了某人回复一封旧消息以方便地找到正确的收件人来发送、却更改主题并开始新对话的情况。
若消息因某种原因乱序投递,用户可能在同一 Thread 中有两封 Email,却缺乏将它们彼此关联的头部。第三封 Email 的到来可能提供缺失的引用,将它们全部合并到单一 Thread 中。由于 Email 的 "threadId" 是不可变的,若服务器希望合并 Thread,它必须通过删除并重新插入(使用新的 Email id)那些改变了 "threadId" 的 Email 来处理。
一个 *Thread* 对象具有以下属性:
- id: "Id"(不可变;服务器设定)
- Thread 的 id。
- emailIds: "Id[]"(服务器设定)
- Thread 中 Email 的 id,按 Email 的 "receivedAt" 日期排序,最旧的在前。若两封 Email 日期相同,排序取决于服务器但必须稳定(建议按 id 排序)。
支持以下 JMAP 方法。
3.1. Thread/get
这是 [RFC8620] 第 5.1 节所述的标准 "/get" 方法。
3.1.1. 示例
请求:
[[ "Thread/get", {
"accountId": "acme",
"ids": ["f123u4", "f41u44"]
}, "#1" ]]
响应:
[[ "Thread/get", {
"accountId": "acme",
"state": "f6a7e214",
"list": [
{
"id": "f123u4",
"emailIds": [ "eaa623", "f782cbb"]
},
{
"id": "f41u44",
"emailIds": [ "82cf7bb" ]
}
],
"notFound": []
}, "#1" ]]
3.2. Thread/changes
这是 [RFC8620] 第 5.2 节所述的标准 "/changes" 方法。
4. 电子邮件(Emails)
一个 *Email* 对象是对消息 [RFC5322] 的表示,使客户端能够避免 MIME 解析、传输编码与字符编码的复杂性。
4.1. Email 对象的属性
大体上,一个消息由两部分组成:一列头字段,随后是正文。Email 数据类型提供了一种访问完整结构的方式,或在使用简化属性以避免某些复杂性(若对客户端应用已足够)的方式。
虽然可以获取和设置原始头,但绝大多数客户端应为其想要处理的每个头字段使用适当的解析形式,因为这样可使客户端避免有效消息(依 RFC 5322)所需的各种编码的复杂性。
消息的正文通常是一组以树状结构组织的、经过 MIME 编码的文档。它可能是任意嵌套的,但绝大多数邮件客户端呈现的是消息正文的扁平模型(通常为纯文本或 HTML)外加一组附件。展平 MIME 结构以构建此模型可能很困难,并会导致客户端之间的不一致。因此,除了给出完整树的 "bodyStructure" 属性外,Email 对象还包含 3 个带有扁平正文部分列表的替代属性:
- "textBody"/"htmlBody":这些提供应被顺序渲染为消息"正文"的部分列表。这是一个列表而非单个部分,因为消息在传输时可能作为独立部分附加/前置了头与/或脚注,并且某些客户端将文本与图像(甚至是视频与声Clip)作为多个部分而非带有引用图像的单个 HTML 部分发送,以便内联显示在正文中。
- "attachments":这提供应作为消息"附件"呈现的部分列表。某些图像可能仅用于嵌入 HTML 正文部分中;若客户端直接以嵌入图像显示 HTML,则可以在 UI 中不将这些呈现为附件。某些部分也可能出现在 htmlBody/textBody 中;类似地,若作为正文的一部分被渲染,客户端也可选择不在 UI 中将其呈现为附件。
"bodyValues" 属性允许客户端直接获取文本部分的值,而无需对 blob 发起第二次请求,并让服务器负责将字符集解码为 unicode。该数据位于独立属性中而非 EmailBodyPart 对象上,是为了避免大量数据的重复,因为若客户端获取了 bodyStructure、textBody 与 htmlBody 中的多个,同一部分可能被包含两次。
在以下小节中,针对内容类型采用了通配符的通用符号约定,因此 "foo/*" 表示任何以 "foo/" 开头的内容类型。
由于涉及的属性众多,Email 属性集在以下四个小节中规定。这纯粹是为了可读性;所有属性都是顶层平级关系。
4.1.1. 元数据
这些属性表示邮件存储中关于消息的元数据,而非通过解析消息本身得到。
- id: "Id"(不可变;服务器设定)
- Email 对象的 id。注意这是 JMAP 对象 id,而非消息 [RFC5322] 的 Message-ID 头字段值。
- blobId: "Id"(不可变;服务器设定)
- 代表本 Email 所对应的消息 [RFC5322] 原始八位组的 id。这可用于下载原始消息,或直接将其附加到另一封 Email 等。
- threadId: "Id"(不可变;服务器设定)
- 本 Email 所属 Thread 的 id。
- mailboxIds: "Id[Boolean]"
- 本 Email 所属 Mailbox 的 id 集合。邮件存储中的 Email 在任何时候都必须属于一个或多个 Mailbox(在被销毁之前)。该集合表示为一个对象,每个键为一个 Mailbox id。对象中每个键的值必须为 true。
- keywords: "String[Boolean]"(默认:{})
- 适用于本 Email 的关键字集合。该集合表示为一个对象,键为关键字。对象中每个键的值必须为 true。
关键字与 IMAP 共享。IMAP 的六个系统关键字受到特殊处理。以下四个关键字在 IMAP 中的首字符 "\" 在 JMAP 中改为 "$",并具有特定语义含义:
- "$draft":该 Email 是用户正在撰写的草稿。
- "$seen":该 Email 已被阅读。
- "$flagged":该 Email 已被标记以引起紧急/特别关注。
- "$answered":该 Email 已被回复。
IMAP 的 "\Recent" 关键字不通过 JMAP 暴露。IMAP 的 "\Deleted" 关键字也不存在:IMAP 使用删除+expunge 模型,而 JMAP 不使用。任何带有 "\Deleted" 关键字的消息都不得通过 JMAP 可见(因此不计入 "totalEmails"、"unreadEmails"、"totalThreads" 与 "unreadThreads" Mailbox 属性)。
用户可向 Email 添加任意关键字。为与 IMAP 兼容,关键字是长度为 1–255 个字符、属于 ASCII 子集 %x21–%x7e(不包括控制字符与空格)的区分大小写字符串,且不得包含以下任何字符:
( ) { ] % * " \
由于 JSON 区分大小写,服务器必须以小写形式返回关键字。
由 [RFC5788] 确立的 IANA "IMAP and JMAP Keywords" 注册表(位于 <https://www.iana.org/assignments/imap-jmap-keywords/>)为一些其他常用关键字赋予语义含义。未来可能在此处确立新关键字。特别需注意:
- "$forwarded":该 Email 已被转发。
- "$phishing":该 Email 极有可能是钓鱼邮件。客户端在显示此 Email 时应警告用户小心,并禁用链接与附件。
- "$junk":该 Email 肯定是垃圾邮件。当用户报告垃圾邮件时,客户端应设置此标记,以帮助训练自动垃圾检测系统。
- "$notjunk":该 Email 肯定不是垃圾邮件。当用户表明某 Email 是合法邮件时,客户端应设置此标记,以帮助训练自动垃圾检测系统。
- size: "UnsignedInt"(不可变;服务器设定)
- 消息 [RFC5322] 原始数据的大小(以八位组计)(如 "blobId" 所引用,即用户将下载的文件中的八位组数)。
- receivedAt: "UTCDate"(不可变;默认:服务器上的创建时间)
- Email 被消息存储接收的日期。这是 IMAP [RFC3501] 中的"内部日期"。
4.1.2. 头字段解析形式
头字段属性派生自消息头字段 [RFC5322] [RFC6532]。所有头字段都可以原始形式获取。某些头字段也可以解析形式获取。可获取的解析结构取决于具体头字段。这些形式在随后的小节中定义。
4.1.2.1. 原始(Raw)
类型:"String"
头字段值从头字段名结尾冒号之后的第一个八位组开始,直到但不包括头字段终止 CRLF 为止的原始八位组。任何符合标准的消息必须是 ASCII(RFC 5322)或 UTF-8(RFC 6532);但现实中存在其他编码。服务器应将其违反 UTF-8 语法、带有高位设置的所有八位组或八位组串,替换为 Unicode 替换字符(U+FFFD)。任何 NUL 八位组必须被丢弃。
此形式通常带有前导空格,因为大多数生成的消息在终止头字段名的冒号之后插入了一个空格。
4.1.2.2. 文本(Text)
类型:"String"
头字段值经过以下处理:
- 空白被展开(如 [RFC5322] 第 2.2.3 节所定义)。
- 值末尾终止的 CRLF 被移除。
- 值开头的任何 SP 字符被移除。
- 任何具有已知字符集、句法正确的编码段 [RFC2047] 被解码。任何按 [RFC2047] 编码的 NUL 八位组或控制字符从解码值中丢弃。任何看似符合 [RFC2047] 语法但违反其放置或空白规则的文本不得被解码。
- 所得 unicode 转换为标准等价组合范式 C(NFC)形式。
若任何解码失败,解析器应插入一个 Unicode 替换字符(U+FFFD)并尽可能继续。
为防止明显无意义的、可能导致互操作性问题的行为,此形式只能针对以下头字段获取或设置:
- Subject
- Comments
- Keywords
- List-Id
- 任何未在 [RFC5322] 或 [RFC2369] 中定义的头字段
4.1.2.3. 地址(Addresses)
类型:"EmailAddress[]"
头字段被解析为 "address-list" 值(如 [RFC5322] 第 3.4 节所规定),成为 "EmailAddress[]" 类型。从 "address-list" 解析出的每个 "mailbox" 对应一个 EmailAddress 项。组与注释信息被丢弃。
一个 *EmailAddress* 对象具有以下属性:
- name: "String|null"
- "mailbox" 的 "display-name" [RFC5322]。若这是 "quoted-string":
- 去掉外围的 DQUOTE 字符。
- 任何 "quoted-pair" 被解码。
- 空白被展开,然后去掉任何前导与尾随空白。
- "mailbox" 的 "display-name" [RFC5322]。若这是 "quoted-string":
若没有 "display-name" 但紧随 "addr-spec" 之后有一个 "comment",则应使用该 "comment" 的值。否则,此属性为 null。
- email: "String"
- "mailbox" 的 "addr-spec" [RFC5322]。
任何句法正确的、具有已知编码的编码段 [RFC2047] 必须被解码,遵循与 Text 形式相同的规则(见第 4.1.2.2 节)。
面对无效结构时,解析应尽力而为,以容纳无效消息与半成品草稿。EmailAddress 对象的 "email" 属性可能不符合 "addr-spec" 形式(例如可能不含 @ 符号)。
例如,以下 "address-list" 字符串:
" James Smythe" <james@example.com>, Friends:
jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?=
<john@example.com>;
将被解析为:
[
{ "name": "James Smythe", "email": "james@example.com" },
{ "name": null, "email": "jane@example.com" },
{ "name": "John Smith", "email": "john@example.com" }
]
为防止明显无意义的、可能导致互操作性问题的行为,此形式只能针对以下头字段获取或设置:
- From
- Sender
- Reply-To
- To
- Cc
- Bcc
- Resent-From
- Resent-Sender
- Resent-Reply-To
- Resent-To
- Resent-Cc
- Resent-Bcc
- 任何未在 [RFC5322] 或 [RFC2369] 中定义的头字段
4.1.2.4. 分组地址(GroupedAddresses)
类型:"EmailAddressGroup[]"
这与 Addresses 形式类似,但保留组信息。头字段被解析为 "address-list" 值(如 [RFC5322] 第 3.4 节所规定),成为 "GroupedAddresses[]" 类型。不属于某个组的连续 "mailbox" 值仍被收集到一个 EmailAddressGroup 对象之下,以提供统一类型。
一个 *EmailAddressGroup* 对象具有以下属性:
- name: "String|null"
- "group" 的 "display-name" [RFC5322],或若地址不属于某个组则为 null。若这是 "quoted-string",则按与 EmailAddress 类型中的 "name" 相同的方式处理。
- addresses: "EmailAddress[]"
- 属于该组的 "mailbox" 值,表示为 EmailAddress 对象。
任何句法正确的、具有已知编码的编码段 [RFC2047] 必须被解码,遵循与 Text 形式相同的规则(见第 4.1.2.2 节)。
面对无效结构时,解析应尽力而为,以容纳无效消息与半成品草稿。
例如,以下 "address-list" 字符串:
" James Smythe" <james@example.com>, Friends:
jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?=
<john@example.com>;
将被解析为:
[
{ "name": null, "addresses": [
{ "name": "James Smythe", "email": "james@example.com" }
]},
{ "name": "Friends", "addresses": [
{ "name": null, "email": "jane@example.com" },
{ "name": "John Smith", "email": "john@example.com" }
]}
]
为防止明显无意义的、可能导致互操作性问题的行为,此形式只能针对与 Addresses 形式相同的头字段获取或设置(见第 4.1.2.3 节)。
4.1.2.5. 消息标识(MessageIds)
类型:"String[]|null"
头字段被解析为 "msg-id" 值列表(如 [RFC5322] 第 3.6.4 节所规定),成为 "String[]" 类型。注释和/或折叠空白(CFWS)以及外围尖括号("<>")被移除。若解析失败,值为 null。
为防止明显无意义的、可能导致互操作性问题的行为,此形式只能针对以下头字段获取或设置:
- Message-ID
- In-Reply-To
- References
- Resent-Message-ID
- 任何未在 [RFC5322] 或 [RFC2369] 中定义的头字段
4.1.2.6. 日期(Date)
类型:"Date|null"
头字段被解析为 "date-time" 值(如 [RFC5322] 第 3.3 节所规定),成为 "Date" 类型。若解析失败,值为 null。
为防止明显无意义的、可能导致互操作性问题的行为,此形式只能针对以下头字段获取或设置:
- Date
- Resent-Date
- 任何未在 [RFC5322] 或 [RFC2369] 中定义的头字段
4.1.2.7. URL
类型:"String[]|null"
头字段被解析为 URL 列表(如 [RFC2369] 所描述),成为 "String[]" 类型。值不包含外围尖括号或头字段中附带的任何注释。若解析失败,值为 null。
为防止明显无意义的、可能导致互操作性问题的行为,此形式只能针对以下头字段获取或设置:
- List-Help
- List-Unsubscribe
- List-Subscribe
- List-Post
- List-Owner
- List-Archive
- 任何未在 [RFC5322] 或 [RFC2369] 中定义的头字段
4.1.3. 头字段属性
以下底层 Email 属性为完整访问消息的头数据而规定:
- headers: "EmailHeader[]"(不可变)
- 这是所有头字段 [RFC5322] 的列表,顺序与它们在消息中出现的顺序相同。一个 *EmailHeader* 对象具有以下属性:
- name: "String"
- 如 [RFC5322] 所定义的头 "field name",大小写与消息中保持一致。
- value: "String"
- 如 [RFC5322] 所定义的头 "field value",为原始(Raw)形式。
- name: "String"
- 这是所有头字段 [RFC5322] 的列表,顺序与它们在消息中出现的顺序相同。一个 *EmailHeader* 对象具有以下属性:
此外,客户端可以请求/发送代表以下形式的各个头字段的属性:
header:{header-field-name}
其中 "{header-field-name}" 指任意一个或多个可打印 ASCII 字符的序列(即取值在 33 到 126 之间,含端点)的组合,冒号(:)除外。该属性还可带有以下后缀:
- :as{header-form}
- 表示值处于解析形式,其中 "{header-form}" 是上面规定的某个解析形式名称。若未给出,则值为原始(Raw)形式。
- :all
- 表示值是一个数组,各项对应头字段的每个实例,按其在消息中出现的顺序排列。若未使用该后缀,结果为头字段*最后*一个实例的值(即若使用 :all,则等同于数组中的最后一项),若不存在则为 null。
若两个后缀都使用,则必须以上述顺序指定。头字段名以不区分大小写的方式匹配。值的类型取决于所请求的形式,若使用了 :all 则为该类型的数组。若消息中不存在具有所请求名称的头字段,则获取单个实例时值为 null,请求 :all 时为空数组。
作为一个简单示例,若客户端请求名为 "header:subject" 的属性,这意味着找到消息中名为 "subject" 的*最后*一个头字段(不区分大小写匹配),并以原始形式返回值,若未找到该名称的头字段则返回 null。
作为一个更复杂的示例,考虑客户端请求名为 "header:Resent-To:asAddresses:all" 的属性。这意味着:
- 找到*所有*名为 Resent-To 的头字段(不区分大小写匹配)。
- 对每个实例,以 Addresses 形式解析头字段值。
- 结果为 "EmailAddress[][]" 类型——数组中的每一项对应一个 Resent-To 头字段实例的解析值(其本身也是一个数组)。
还为 Email 对象规定了以下便捷属性:
- messageId: "String[]|null"(不可变)
- 该值与 "header:Message-ID:asMessageIds" 的值相同。对于符合 RFC 5322 的消息,这将是一个仅含单项的数组。
- inReplyTo: "String[]|null"(不可变)
- 该值与 "header:In-Reply-To:asMessageIds" 的值相同。
- references: "String[]|null"(不可变)
- 该值与 "header:References:asMessageIds" 的值相同。
- sender: "EmailAddress[]|null"(不可变)
- 该值与 "header:Sender:asAddresses" 的值相同。
- from: "EmailAddress[]|null"(不可变)
- 该值与 "header:From:asAddresses" 的值相同。
- to: "EmailAddress[]|null"(不可变)
- 该值与 "header:To:asAddresses" 的值相同。
- cc: "EmailAddress[]|null"(不可变)
- 该值与 "header:Cc:asAddresses" 的值相同。
- bcc: "EmailAddress[]|null"(不可变)
- 该值与 "header:Bcc:asAddresses" 的值相同。
- replyTo: "EmailAddress[]|null"(不可变)
- 该值与 "header:Reply-To:asAddresses" 的值相同。
- subject: "String|null"(不可变)
- 该值与 "header:Subject:asText" 的值相同。
- sentAt: "Date|null"(不可变;创建时默认:服务器当前时间)
- 该值与 "header:Date:asDate" 的值相同。
4.1.4. 正文部分(Body Parts)
这些属性派生自消息正文 [RFC5322] 及其 MIME 实体 [RFC2045]。
一个 *EmailBodyPart* 对象具有以下属性:
- partId: "String|null"
- 在 Email 内唯一标识此部分。其作用域限于 "emailId",在 JMAP Email 对象表示之外无意义。当且仅当该部分类型为 "multipart/*" 时,此值为 null。
- blobId: "Id|null"
- 代表该部分内容的原始八位组,在解码任何已知的 Content-Transfer-Encoding(如 [RFC2045] 所定义)之后;当且仅当该部分类型为 "multipart/*" 时,此值为 null。注意,若两个部分的已解码八位组相同且服务器使用了数据的加密安全散列作为 blob id,则它们可能以不同方式传输编码却具有相同的 blob id。若传输编码未知,则视为无传输编码。
- size: "UnsignedInt"
- 内容传输解码后的原始数据大小(以八位组计)(如 "blobId" 所引用,即用户将下载的文件中的八位组数)。
- headers: "EmailHeader[]"
- 该部分中所有头字段的列表,按其在消息中出现的顺序排列。值为原始形式。
- name: "String|null"
- 这是依 [RFC2231] 的 Content-Disposition 头字段中已解码的 "filename" 参数;或(为与现有系统兼容)若不存在,则是依 [RFC2047] 的 Content-Type 头字段中已解码的 "name" 参数。
- type: "String"
- 该部分 Content-Type 头字段的值(若存在);否则为依 MIME 标准的隐式类型("text/plain",或若在 "multipart/digest" 内部则为 "message/rfc822")。CFWS 被移除,任何参数被剥离。
- charset: "String|null"
- 若存在,则为 Content-Type 头字段的 charset 参数值;若头字段存在但类型非 "text/*",则为 null。若不存在 Content-Type 头字段,或它存在且类型为 "text/*" 但没有 charset 参数,则为依 MIME 标准的隐式 charset:"us-ascii"。
- disposition: "String|null"
- 若存在,则为该部分 Content-Disposition 头字段的值;否则为 null。CFWS 被移除,任何参数被剥离。
- cid: "String|null"
- 若存在,则为该部分 Content-Id 头字段的值;否则为 null。CFWS 与外围尖括号("<>")被移除。这可用于从 "text/html" 正文部分 [HTML] 之内通过 "cid:" 协议(如 [RFC2392] 所定义)引用该内容。
- language: "String[]|null"
- 该部分 Content-Language 头字段中的语言标签列表(如 [RFC3282] 所定义),若存在)。
- location: "String|null"
- 该部分 Content-Location 头字段中的 URI(如 [RFC2557] 所定义),若存在。
- subParts: "EmailBodyPart[]|null"
- 若类型为 "multipart/*",则这包含每个子级的正文部分。
此外,客户端可以遵循与 Email 对象相同的语法与语义,请求/发送代表各个头字段的 EmailBodyPart 属性,例如 "header:Content-Type"。
为访问消息的正文数据,规定了以下 Email 属性:
- bodyStructure: "EmailBodyPart"(不可变)
- 这是消息正文的完整 MIME 结构,不递归进入 "message/rfc822" 或 "message/global" 部分。注意 EmailBodyPart 若为类型 "multipart/*" 则可能带有 subParts。
- bodyValues: "String[EmailBodyValue]"(不可变)
- 这是从 "partId" 到 EmailBodyValue 对象的映射,涵盖零个、部分或所有 "text/*" 部分。包含哪些部分以及值是否被截断,由 "Email/get" 与 "Email/parse" 的各种参数决定。一个 *EmailBodyValue* 对象具有以下属性:
- value: "String"
- 在解码 Content-Transfer-Encoding 与 Content-Type 字符集(若服务器二者皆已知)之后、并将任何 CRLF 替换为单个 LF 后的正文部分值。若字符集未知、未给定字符集,或服务器认为给定字符集有误,服务器可使用启发式方法确定用于解码的字符集。解码是尽力而为的;当遇到畸形段时,服务器应插入 Unicode 替换字符(U+FFFD)并继续。
- isEncodingProblem: "Boolean"(默认:false)
- 若在解码字符集时发现畸形段、字符集未知,或内容传输编码未知,则为 true。
- isTruncated: "Boolean"(默认:false)
- 若 "value" 已被截断,则为 true。
- value: "String"
- 这是从 "partId" 到 EmailBodyValue 对象的映射,涵盖零个、部分或所有 "text/*" 部分。包含哪些部分以及值是否被截断,由 "Email/get" 与 "Email/parse" 的各种参数决定。一个 *EmailBodyValue* 对象具有以下属性:
有关截断以及启发式确定内容类型与字符集的问题,见安全考量一节。
- textBody: "EmailBodyPart[]"(不可变)
- 应(按顺序)显示为消息正文的 "text/plain"、"text/html"、"image/*"、"audio/*" 和/或 "video/*" 部分列表,在存在替代版本时偏好 "text/plain"。
- htmlBody: "EmailBodyPart[]"(不可变)
- 应(按顺序)显示为消息正文的 "text/plain"、"text/html"、"image/*"、"audio/*" 和/或 "video/*" 部分列表,在存在替代版本时偏好 "text/html"。
- attachments: "EmailBodyPart[]"(不可变)
- 以深度优先遍历 "bodyStructure" 得到的、满足以下任一条件的所有部分的列表:
- 类型非 "multipart/*" 且未包含在 "textBody" 或 "htmlBody" 中;
- 类型为 "image/*"、"audio/*" 或 "video/*" 且未在 "textBody" 与 "htmlBody" 中同时出现。
- 以深度优先遍历 "bodyStructure" 得到的、满足以下任一条件的所有部分的列表:
这些部分都不包含 subParts,包括 "message/*" 类型。附加的消息可使用 "Email/parse" 方法与 "blobId" 获取。
注意,"text/html" 正文部分 [HTML] 可能通过使用 "cid:" 链接引用 Content-Id(如 [RFC2392] 所定义),或通过引用 Content-Location,来引用 attachments 中的图像部分。
- hasAttachment: "Boolean"(不可变;服务器设定)
- 若存在一个或多个客户端 UI 应作为可下载项提供的部分,则为 true。若 "attachments" 列表包含至少一个不具有 "Content-Disposition: inline" 的项,服务器应将 hasAttachment 设为 true。服务器可忽略以某种方式被自动处理、或被消息某个 "text/html" 部分引用为嵌入图像的部分。
- preview: "String"(不可变;服务器设定)
- 消息正文的一段纯文本片段。旨在作为在邮件存储中列出消息时的预览行显示,显示时可能被截断。服务器可选择将消息的哪一部分包含在预览中;跳过引用段与称呼语并折叠空白,可得到更有用的预览。
该值长度不得超过 256 个字符。
由于这是由服务器从消息内容导出,且导出算法可能随时间改变,第二次获取某 Email 的此值可能返回不同结果。然而,先前的值不被视为错误,且该变化不应导致服务器认为 Email 对象发生了变化。
将 bodyStructure 分解为 textBody、htmlBody 与 attachments 部分列表的确切算法不作强制,因为这是一个服务质量实现问题,且很可能需要在长期内针对发现的畸形内容采用变通方法。然而,基于实际经验,建议以下算法(此处以 JavaScript 表达)作为起点:
function isInlineMediaType ( type ) {
return type.startsWith( 'image/' ) ||
type.startsWith( 'audio/' ) ||
type.startsWith( 'video/' );
}
function parseStructure ( parts, multipartType, inAlternative,
htmlBody, textBody, attachments ) {
// For multipartType == alternative
let textLength = textBody ? textBody.length : -1;
let htmlLength = htmlBody ? htmlBody.length : -1;
for ( let i = 0; i < parts.length; i += 1 ) {
let part = parts[i];
let isMultipart = part.type.startsWith( 'multipart/' );
// Is this a body part rather than an attachment
let isInline = part.disposition != "attachment" &&
// Must be one of the allowed body types
( part.type == "text/plain" ||
part.type == "text/html" ||
isInlineMediaType( part.type ) ) &&
// If multipart/related, only the first part can be inline
// If a text part with a filename, and not the first item
// in the multipart, assume it is an attachment
( i === 0 ||
( multipartType != "related" &&
( isInlineMediaType( part.type ) || !part.name ) ) );
if ( isMultipart ) {
let subMultiType = part.type.split( '/' )[1];
parseStructure( part.subParts, subMultiType,
inAlternative || ( subMultiType == 'alternative' ),
htmlBody, textBody, attachments );
} else if ( isInline ) {
if ( multipartType == 'alternative' ) {
switch ( part.type ) {
case 'text/plain':
textBody.push( part );
break;
case 'text/html':
htmlBody.push( part );
break;
default:
attachments.push( part );
break;
}
continue;
} else if ( inAlternative ) {
if ( part.type == 'text/plain' ) {
htmlBody = null;
}
if ( part.type == 'text/html' ) {
textBody = null;
}
}
if ( textBody ) {
textBody.push( part );
}
if ( htmlBody ) {
htmlBody.push( part );
}
if ( ( !textBody || !htmlBody ) &&
isInlineMediaType( part.type ) ) {
attachments.push( part );
}
} else {
attachments.push( part );
}
}
if ( multipartType == 'alternative' && textBody && htmlBody ) {
// Found HTML part only
if ( textLength == textBody.length &&
htmlLength != htmlBody.length ) {
for ( let i = htmlLength; i < htmlBody.length; i += 1 ) {
textBody.push( htmlBody[i] );
}
}
// Found plaintext part only
if ( htmlLength == htmlBody.length &&
textLength != textBody.length ) {
for ( let i = textLength; i < textBody.length; i += 1 ) {
htmlBody.push( textBody[i] );
}
}
}
}
// Usage:
let htmlBody = [];
let textBody = [];
let attachments = [];
parseStructure( [ bodyStructure ], 'mixed', false,
htmlBody, textBody, attachments );
例如,考虑一封同时具有文本与 HTML 版本、且经过列表软件管理器附加了页眉与页脚的消息。它的 MIME 结构可能类似于:
multipart/mixed
text/plain, content-disposition=inline - A
multipart/mixed
multipart/alternative
multipart/mixed
text/plain, content-disposition=inline - B
image/jpeg, content-disposition=inline - C
text/plain, content-disposition=inline - D
multipart/related
text/html - E
image/jpeg - F
image/jpeg, content-disposition=attachment - G
application/x-excel - H
message/rfc822 - J
text/plain, content-disposition=inline - K
在这种情况下,上述算法会将此分解为:
textBody => [ A, B, C, D, K ]
htmlBody => [ A, E, K ]
attachments => [ C, F, G, H, J ]
4.2. Email/get
这是 [RFC8620] 第 5.1 节所述的标准 "/get" 方法,带有以下额外请求参数:
- bodyProperties: "String[]"
- 为每个返回的 EmailBodyPart 获取的属性列表。若省略,则默认为:
[ "partId", "blobId", "size", "name", "type", "charset", "disposition", "cid", "language", "location" ]
- 为每个返回的 EmailBodyPart 获取的属性列表。若省略,则默认为:
- fetchTextBodyValues: "Boolean"(默认:false)
- 若为 true,则 "bodyValues" 属性包含 "textBody" 属性中的任何 "text/*" 部分。
- fetchHTMLBodyValues: "Boolean"(默认:false)
- 若为 true,则 "bodyValues" 属性包含 "htmlBody" 属性中的任何 "text/*" 部分。
- fetchAllBodyValues: "Boolean"(默认:false)
- 若为 true,则 "bodyValues" 属性包含 "bodyStructure" 属性中的任何 "text/*" 部分。
- maxBodyValueBytes: "UnsignedInt"(默认:0)
- 若大于零,则在 "bodyValues" 中返回的任何 EmailBodyValue 对象的 "value" 属性必要时必须被截断,使其大小不超过此八位组数。若为 0(默认),则不发生截断。
服务器必须确保截断产生有效的 UTF-8,且不会发生在码点中间。若部分是 "text/html" 类型,服务器不应在 HTML 标签内部截断,例如不应在 "<a href="https://example.com">" 的中间截断。并不要求截断后的形式是平衡的树或有效的 HTML(实际上原始源码很可能既非平衡树也非有效 HTML)。
若标准的 "properties" 参数被省略或为 null,则必须使用以下默认值而非 "all" 属性:
[ "id", "blobId", "threadId", "mailboxIds", "keywords", "size", "receivedAt", "messageId", "inReplyTo", "references", "sender", "from", "to", "cc", "bcc", "replyTo", "subject", "sentAt", "hasAttachment", "preview", "bodyValues", "textBody", "htmlBody", "attachments" ]
在高质量的实作中,以下属性预期可以快速获取:
- id
- blobId
- threadId
- mailboxIds
- keywords
- size
- receivedAt
- messageId
- inReplyTo
- sender
- from
- to
- cc
- bcc
- replyTo
- subject
- sentAt
- hasAttachment
- preview
客户端在获取任何其他属性时应小心,因为获取与返回这些数据可能存在明显更长的延迟。
如前所述,头的解析形式只能用于适当的头字段。试图获取被禁止的形式(例如 "header:From:asDate")必须导致方法调用被以 "invalidArguments" 错误拒绝。
当作为属性请求了特定头字段时,响应中属性名的大小写必须与请求中使用的大小写完全相同。
4.2.1. 示例
请求:
[[ "Email/get", {
"ids": [ "f123u456", "f123u457" ],
"properties": [ "threadId", "mailboxIds", "from", "subject",
"receivedAt", "header:List-POST:asURLs",
"htmlBody", "bodyValues" ],
"bodyProperties": [ "partId", "blobId", "size", "type" ],
"fetchHTMLBodyValues": true,
"maxBodyValueBytes": 256
}, "#1" ]]
以及响应:
[[ "Email/get", {
"accountId": "abc",
"state": "41234123231",
"list": [
{
"id": "f123u457",
"threadId": "ef1314a",
"mailboxIds": { "f123": true },
"from": [{ "name": "Joe Bloggs", "email": "joe@example.com" }],
"subject": "Dinner on Thursday?",
"receivedAt": "2013-10-13T14:12:00Z",
"header:List-POST:asURLs": [
"mailto:partytime@lists.example.com"
],
"htmlBody": [{
"partId": "1",
"blobId": "B841623871",
"size": 283331,
"type": "text/html"
}, {
"partId": "2",
"blobId": "B319437193",
"size": 10343,
"type": "text/plain"
}],
"bodyValues": {
"1": {
"isEncodingProblem": false,
"isTruncated": true,
"value": "<html><body><p>Hello ..."
},
"2": {
"isEncodingProblem": false,
"isTruncated": false,
"value": "-- Sent by your friendly mailing list ..."
}
}
}
],
"notFound": [ "f123u456" ]
}, "#1" ]]
4.3. Email/changes
这是 [RFC8620] 第 5.2 节所述的标准 "/changes" 方法。若为大量变更集合生成中间状态,建议先返回较新的变更,因为用户通常对它们更感兴趣。
4.4. Email/query
这是 [RFC8620] 第 5.5 节所述的标准 "/query" 方法,但带有以下额外请求参数:
- collapseThreads: "Boolean"(默认:false)
- 若为 true,则列表中位于前序 Email 同一 Thread 中的 Email(给定过滤器与排序顺序)将从列表中移除。这意味着对于任何给定 Thread,列表中最多只包含一封 Email。
在高质量的实作中,当过滤器仅由单个 "inMailbox" 属性组成时,查询的 "total" 属性预期可以快速计算,因为它等同于关联 Mailbox 对象的 totalEmails 或 totalThreads 属性(取决于 collapseThreads 是否为 true)。
4.4.1. 过滤
一个 *FilterCondition* 对象具有以下属性,均可省略:
- inMailbox: "Id"
- 一个 Mailbox id。Email 必须位于此 Mailbox 中才能匹配该条件。
- inMailboxOtherThan: "Id[]"
- 一组 Mailbox id 列表。Email 必须位于不在此列表中的至少一个 Mailbox 中才能匹配该条件。这是为了便于轻松地将仅存在于回收站/垃圾邮件中的消息排除出搜索。
- before: "UTCDate"
- Email 的 "receivedAt" 日期时间必须早于该日期时间才能匹配该条件。
- after: "UTCDate"
- Email 的 "receivedAt" 日期时间必须相同于或晚于该日期时间才能匹配该条件。
- minSize: "UnsignedInt"
- Email 的 "size" 属性必须大于或等于此数才能匹配该条件。
- maxSize: "UnsignedInt"
- Email 的 "size" 属性必须小于此数才能匹配该条件。
- allInThreadHaveKeyword: "String"
- 与本 Email 同一 Thread 中的所有 Email(包括本封)都必须具有给定关键字才能匹配该条件。
- someInThreadHaveKeyword: "String"
- 与本 Email 同一 Thread 中至少有一封 Email(可能是本封)具有给定关键字才能匹配该条件。
- noneInThreadHaveKeyword: "String"
- 与本 Email 同一 Thread 中的所有 Email(包括本封)都必须*不*具有给定关键字才能匹配该条件。
- hasKeyword: "String"
- 本 Email 必须具有给定关键字才能匹配该条件。
- notKeyword: "String"
- 本 Email 必须不具有给定关键字才能匹配该条件。
- hasAttachment: "Boolean"
- Email 的 "hasAttachment" 属性必须与给定值完全相同才能匹配该条件。
- text: "String"
- 在 Email 中查找文本。服务器必须查找消息 From、To、Cc、Bcc 与 Subject 头字段中的文本,并应查找可能被服务器转换为文本的任意 "text/*" 或其他正文部分内部。服务器可将搜索扩展到任何额外的文本属性。
- from: "String"
- 在消息的 From 头字段中查找文本。
- to: "String"
- 在消息的 To 头字段中查找文本。
- cc: "String"
- 在消息的 Cc 头字段中查找文本。
- bcc: "String"
- 在消息的 Bcc 头字段中查找文本。
- subject: "String"
- 在消息的 Subject 头字段中查找文本。
- body: "String"
- 在消息的某个正文部分中查找文本。服务器可将内容媒体类型非 "text/*" 与 "message/*" 的 MIME 正文部分排除在搜索匹配考虑之外。应注意基于该媒体类型实际呈现给最终用户的文本内容、或以其他方式被识别为适合搜索索引的文本进行匹配。匹配对最终用户无趣的文档元数据(例如标记标签与属性名)是不可取的。
- header: "String[]"
- 该数组必须包含一或二个元素。第一个元素是所要匹配的头字段名。第二个(可选)元素是要在该头字段值中查找的文本。若未提供,则只要消息具有给定名称的头字段即匹配。
若在 FilterCondition 上未指定任何属性,该条件必须始终求值为真。若指定了多个属性,则必须所有属性都满足该条件才为真(这等价于将对象拆分为单属性条件并使其成为 AND 过滤运算符的所有子项)。
匹配 "String" 字段的确切语义*刻意不予定义*,以便索引实现具有灵活性,但须满足以下约束:
- 任何句法正确的、具有已知编码的头字段编码段 [RFC2047] 在尝试匹配文本之前应被解码。
- 在 "text/html" 正文部分内部搜索时,任何被视为标记而非内容的文本都应忽略,包括 HTML 标签与大多数属性、位于 "<head>" 标签内的任何内容、层叠样式表(CSS)与 JavaScript。面向用户呈现的属性内容(如 "alt" 与 "title")应被纳入搜索考虑。
- 文本应以不区分大小写的方式匹配。
- 包含在单引号(')或双引号(")中的文本(在短语匹配中)应被视为*短语搜索*;即要求精确匹配该词或词序,不包括外围引号。
在短语内部,要匹配以下字符之一,必须在其前加反斜杠(\)进行转义:
' " \
- 在短语之外,空白应被视为划分可分别搜索的单独词元,但这些词元必须全部出现,Email 才能匹配过滤器。
- 词元(非短语的一部分)可基于整词、使用词干提取进行匹配(例如,文本搜索 "bus" 会匹配 "buses" 但不匹配 "business")。
4.4.2. 排序
Comparator 对象上 "property" 字段的以下值必须支持排序:
- "receivedAt" - Email 对象中返回的 "receivedAt" 日期。
Comparator 对象上 "property" 字段的以下值应支持排序。当指定 "hasKeyword"、"allInThreadHaveKeyword" 或 "someInThreadHaveKeyword" 排序时,Comparator 对象还必须具有 "keyword" 属性。
- "size" - Email 对象中返回的 "size"。
- "from" - 取 Email "from" 属性中*第一个* EmailAddress 对象的 "name" 属性,若为 null/空则取 "email" 属性。若仍无,则将值视为空字符串。
- "to" - 取 Email "to" 属性中*第一个* EmailAddress 对象的 "name" 属性,若为 null/空则取 "email" 属性。若仍无,则将值视为空字符串。
- "subject" - 取消息的基本主题,如 [RFC5256] 第 2.1 节所定义。
- "sentAt" - Email 对象上的 "sentAt" 属性。
- "hasKeyword" - 若 Email 具有作为 Comparator 对象上额外 "keyword" 属性给定的关键字,则此值必须视为 true,否则为 false。
- "allInThreadHaveKeyword" - 若同一 Thread 中*所有* Email 都具有作为 Comparator 对象上额外 "keyword" 属性给定的关键字,则 Email 的此值必须视为 true。
- "someInThreadHaveKeyword" - 若同一 Thread 中*任意* Email 具有作为 Comparator 对象上额外 "keyword" 属性给定的关键字,则 Email 的此值必须视为 true。
服务器也可支持基于其他属性的排序。客户端可通过检查账户的 "capabilities" 对象(见第 1.3 节)来发现支持哪些属性。
排序示例:
[{
"property": "someInThreadHaveKeyword",
"keyword": "$flagged",
"isAscending": false
}, {
"property": "subject",
"collation": "i;ascii-casemap"
}, {
"property": "receivedAt",
"isAscending": false
}]
这将首先对处于已标记 Thread 中的 Email 排序(若 Thread 内任何 Email 被标记,则该 Thread 被视为已标记),其次按主题顺序,然后对于具有相同主题的消息按从新到旧排序。若两封 Email 在这三个属性上的值完全相同,则顺序取决于服务器但必须稳定。
4.4.3. 会话线索折叠
当 "collapseThreads" 为 true 时,在过滤与排序 Email 列表之后,列表通过移除已经出现过的 Thread id 的 Email(当顺序遍历列表时)被进一步筛选。因此,一个 Thread 在结果中只会*出现一次*,位于属于该 Thread 的第一个 Email 在列表中的位置(给定当前排序/过滤)。
4.5. Email/queryChanges
这是 [RFC8620] 第 5.6 节所述的标准 "/queryChanges" 方法,带有以下额外请求参数:
- collapseThreads: "Boolean"(默认:false)
- 与 "Email/query" 一起使用的 "collapseThreads" 参数。
4.6. Email/set
这是 [RFC8620] 第 5.3 节所述的标准 "/set" 方法。"Email/set" 方法涵盖:
- 创建草稿
- 更改 Email 的关键字(例如未读/标记状态)
- 向 Mailbox 添加/从中移除 Email(移动消息)
- 删除 Email
"keywords"/"mailboxIds" 属性的格式意味着,在更新 Email 时,你既可以用属性的完整值替换整个关键字/Mailbox 集合,也可以使用 JMAP 补丁语法添加/移除单个项(规范见 [RFC8620] 第 5.3 节,示例见第 5.7 节)。
由于 Email 对象的格式,在创建 Email 时有多种方式来指定相同信息。为确保所要创建的消息 [RFC5322] 是无歧义的,以下约束适用于为创建而提交的 Email 对象:
- 无论顶层 Email 还是 EmailBodyPart,都不得给定 "headers" 属性——客户端必须将每个头字段作为单独属性设置。
- 在 Email 或特定 EmailBodyPart 内,不得存在代表同一头字段的两个属性(例如 "header:from" 与 "from")。
- 头字段不得以对该特定字段而言被禁止的解析形式指定。
- 以 "Content-" 开头的头字段不得在 Email 对象上指定,只能在 EmailBodyPart 对象上指定。
- 若给定了 "bodyStructure" 属性,则不得有 "textBody"、"htmlBody" 或 "attachments" 属性。
- 若给定,则 "bodyStructure" EmailBodyPart 不得包含已在顶层 Email 对象上定义的头字段对应的属性。
- 若给定,textBody 必须恰好包含一个正文部分,且必须为 "text/plain" 类型。
- 若给定,htmlBody 必须恰好包含一个正文部分,且必须为 "text/html" 类型。
- 在 EmailBodyPart 内部:
- 客户端可指定 partId 或 blobId,但不能同时指定二者。若给定了 partId,则该 partId 必须出现在 "bodyValues" 属性中。
- 若给定了 partId,则必须省略 "charset" 属性(该部分的内容包含在 bodyValues 中,服务器可选择任意适当的编码)。
- 若给定了 partId,则必须省略 "size" 属性。若给定了 blobId,它可被包含但会被服务器忽略(大小实际是从 blob 内容本身计算的)。
- 不得给定 Content-Transfer-Encoding 头字段。
- 在 EmailBodyValue 对象内部,isEncodingProblem 与 isTruncated 必须为 false 或省略。
违反其中任何一点的创建尝试应被以 "invalidProperties" 错误拒绝;然而,服务器也可选择修改 Email(例如,在冲突的头之间选择、使用不同的内容编码等)以符合其要求来代替。
服务器也可选择设置额外的头。若未包含,服务器必须生成并设置符合 [RFC5322] 第 3.6.4 节的 Message-ID 头字段,以及符合第 3.6.1 节的 Date 头字段。
最终生成的消息可能不符合 RFC 5322。例如,若它是未完成的草稿,To 头字段的值可能不符合该头字段要求的语法。该消息将在提交发送时接受严格一致性检查(见 EmailSubmission 对象描述)。
销毁一封 Email 会将其从其所属的的所有 Mailbox 中移除。要仅仅将 Email 删除到回收站,只需更改 "mailboxIds" 属性,使其现在位于 "role" 属性等于 "trash" 的 Mailbox 中,并移除所有其他 Mailbox id。
清空回收站时,客户端不应销毁同时也位于回收站之外某 Mailbox 中的 Email。对于那些 Email,它们应仅仅移除其回收站 Mailbox。
对于成功创建的 Email 对象,"created" 响应包含该对象的 "id"、"blobId"、"threadId" 与 "size" 属性。
定义了以下额外的 SetError 类型:
对于 "create":
- "blobNotFound":为某个 EmailBodyPart 给定的至少一个 blob id 不存在。SetError 对象中必须包含一个类型为 "Id[]" 的额外 "notFound" 属性,包含服务器上无法找到的、由 EmailBodyPart 引用的每个 "blobId"。
对于 "create" 与 "update":
- "tooManyKeywords":对 Email 关键字的更改将超过服务器定义的最大值。
- "tooManyMailboxes":对本 Email 所在 Mailbox 集合的更改将超过服务器定义的最大值。
4.7. Email/copy
这是 [RFC8620] 第 5.4 节所述的标准 "/copy" 方法,但仅在复制期间可设置 "mailboxIds"、"keywords" 与 "receivedAt" 属性。此方法不能修改 Email 所表示的消息。
服务器可禁止两个具有完全相同消息内容 [RFC5322]、甚至仅具有相同 Message-ID [RFC5322] 的 Email 对象共存于一个账户中;若目标账户已有该 Email,复制将以标准的 "alreadyExists" 错误被拒绝。
对于成功复制的 Email 对象,"created" 响应包含新对象的 "id"、"blobId"、"threadId" 与 "size" 属性。
4.8. Email/import
"Email/import" 方法将消息 [RFC5322] 添加到账户中的 Email 集合。服务器必须支持具有邮件地址国际化(EAI)头 [RFC6532] 的消息。消息必须首先使用标准上传机制作为 blob 上传。该方法接受以下参数:
- accountId: "Id"
- 要使用的账户 id。
- ifInState: "String|null"
- 这是由 "Email/get" 方法返回的状态字符串。若提供,该字符串必须与 accountId 所引用账户的当前状态匹配;否则,方法将中止并返回 "stateMismatch" 错误。若为 null,任何变更都将应用于当前状态。
- emails: "Id[EmailImport]"
- 从创建 id(客户端指定)到 EmailImport 对象的映射。
一个 *EmailImport* 对象具有以下属性:
- blobId: "Id"
- 包含原始消息 [RFC5322] 的 blob 的 id。
- mailboxIds: "Id[Boolean]"
- 要将本 Email 分配到的 Mailbox 的 id。必须给定至少一个 Mailbox。
- keywords: "String[Boolean]"(默认:{})
- 要应用到本 Email 的关键字。
- receivedAt: "UTCDate"(默认:最近的 Received 头的时间,若无则为服务器上的导入时间)
- 要设置到本 Email 上的 "receivedAt" 日期。
每个要导入的 Email 被视为一个原子单元,可能单独成功或失败。成功导入会从 blobId 所引用的数据创建一个新的 Email 对象,并应用给定的 Mailbox、关键字与 receivedAt 日期。
服务器可禁止两个具有完全相同内容 [RFC5322]、甚至仅具有相同 Message-ID [RFC5322] 的 Email 对象共存于一个账户中。在这种情况下,它必须以 "alreadyExists" SetError 拒绝导入被视为重复的 Email 的尝试。SetError 对象上必须包含一个类型为 "Id" 的 "existingId" 属性,带有现有 Email 的 id。若允许重复,则新创建的 Email 对象必须具有独立于现有对象的 id 以及独立的可变属性。
若 "blobId"、"mailboxIds" 或 "keywords" 属性无效(例如缺失、类型错误、id 未找到),服务器必须以 "invalidProperties" SetError 拒绝导入。
若 Email 因将使账户超出配额而无法导入,导入应以 "overQuota" SetError 被拒绝。
若所引用的 blob 不是有效的消息 [RFC5322],服务器可修改消息以修复错误(例如移除 NUL 八位组或修复无效头)。若如此,响应上的 "blobId" 必须代表新的表示,因此不同于 EmailImport 对象上的 "blobId"。或者,服务器可以以 "invalidEmail" SetError 拒绝导入。
响应具有以下参数:
- accountId: "Id"
- 本次调用所用账户的 id。
- oldState: "String|null"
- 在进行所请求的变更之前,本账户上 "Email/get" 本应返回的状态字符串;若服务器不知道先前的状态字符串,则为 null。
- newState: "String"
- 本账户上 "Email/get" 现在将返回的状态字符串。
- created: "Id[Email]|null"
- 从创建 id 到包含每个成功导入 Email 的 "id"、"blobId"、"threadId" 与 "size" 属性的对象的映射,若无则为 null。
- notCreated: "Id[SetError]|null"
- 从创建 id 到每个创建失败的 Email 的 SetError 对象的映射,若全部成功则为 null。可能的错误在上方定义。
可能返回以下额外错误以代替 "Email/import" 响应:
"stateMismatch":提供了 "ifInState" 参数,但它与当前状态不匹配。
4.9. Email/parse
此方法允许你将 blob 作为消息 [RFC5322] 解析以获取 Email 对象。服务器必须支持具有 EAI 头 [RFC6532] 的消息。这可用于解析并显示附加的消息,而无需将它们作为顶层 Email 对象导入邮件存储自身。
若被请求,Email 对象上的以下元数据属性将为 null:
- id
- mailboxIds
- keywords
- receivedAt
若服务器能计算出该 Email 若被导入将被分配到的 Thread,则 Email 的 "threadId" 属性可能呈现。否则,若被获取,此值也为 null。
"Email/parse" 方法接受以下参数:
- accountId: "Id"
- 要使用的账户 id。
- blobIds: "Id[]"
- 要解析的 blob 的 id。
- properties: "String[]"
- 若提供,则每个 Email 对象仅返回数组中列出的属性。若省略,则默认为:
[ "messageId", "inReplyTo", "references", "sender", "from", "to", "cc", "bcc", "replyTo", "subject", "sentAt", "hasAttachment", "preview", "bodyValues", "textBody", "htmlBody", "attachments" ]
- 若提供,则每个 Email 对象仅返回数组中列出的属性。若省略,则默认为:
- bodyProperties: "String[]"
- 为每个返回的 EmailBodyPart 获取的属性列表。若省略,则默认为与 "Email/get" 的 "bodyProperties" 默认参数相同的值。
- fetchTextBodyValues: "Boolean"(默认:false)
- 若为 true,则 "bodyValues" 属性包含 "textBody" 属性中的任何 "text/*" 部分。
- fetchHTMLBodyValues: "Boolean"(默认:false)
- 若为 true,则 "bodyValues" 属性包含 "htmlBody" 属性中的任何 "text/*" 部分。
- fetchAllBodyValues: "Boolean"(默认:false)
- 若为 true,则 "bodyValues" 属性包含 "bodyStructure" 属性中的任何 "text/*" 部分。
- maxBodyValueBytes: "UnsignedInt"(默认:0)
- 若大于零,则在 "bodyValues" 中返回的任何 EmailBodyValue 对象的 "value" 属性必要时必须被截断,使其大小不超过此八位组数。若为 0(默认),则不发生截断。
服务器必须确保截断产生有效的 UTF-8,且不会发生在码点中间。若部分是 "text/html" 类型,服务器不应在 HTML 标签内部截断,例如不应在 "<a href="https://example.com">" 的中间截断。并不要求截断后的形式是平衡的树或有效的 HTML(实际上原始源码很可能既非平衡树也非有效 HTML)。
响应具有以下参数:
- accountId: "Id"
- 调用所用账户的 id。
- parsed: "Id[Email]|null"
- 从 blob id 到每个成功解析的 blob 的已解析 Email 表示的映射,若无则为 null。
- notParsable: "Id[]|null"
- 给定 id 中对应无法作为 Email 解析的 blob 的列表,若无则为 null。
- notFound: "Id[]|null"
- 给定但无法找到的 blob id 的列表,若无则为 null。
如前所述,头的解析形式只能用于适当的头字段。试图获取被禁止的形式(例如 "header:From:asDate")必须导致方法调用被以 "invalidArguments" 错误拒绝。
当作为属性请求了特定头字段时,响应中属性名的大小写必须与请求中使用的大小写完全相同。
4.10. 示例
客户端首次登录。它首先获取 Mailbox 集合。现在它将向用户显示收件箱,我们假定其 Mailbox id 为 "fb666a55"。收件箱可能(非常!)大,但用户的屏幕只有那么大,因此客户端只需加载填满屏幕所需的 Thread,并仅在用户滚动时加载更多。客户端发送此请求:
[[ "Email/query",{
"accountId": "ue150411c",
"filter": {
"inMailbox": "fb666a55"
},
"sort": [{
"isAscending": false,
"property": "receivedAt"
}],
"collapseThreads": true,
"position": 0,
"limit": 30,
"calculateTotal": true
}, "0" ],
[ "Email/get", {
"accountId": "ue150411c",
"#ids": {
"resultOf": "0",
"name": "Email/query",
"path": "/ids"
},
"properties": [
"threadId"
]
}, "1" ],
[ "Thread/get", {
"accountId": "ue150411c",
"#ids": {
"resultOf": "1",
"name": "Email/get",
"path": "/list/*/threadId"
}
}, "2" ],
[ "Email/get", {
"accountId": "ue150411c",
"#ids": {
"resultOf": "2",
"name": "Thread/get",
"path": "/list/*/emailIds"
},
"properties": [
"threadId",
"mailboxIds",
"keywords",
"hasAttachment",
"from",
"subject",
"receivedAt",
"size",
"preview"
]
}, "3" ]]
让我们拆解这 4 个方法调用,看看它们在做什么:
- "0":它向服务器请求收件箱中前 30 个 Email 对象的 id,按从新到旧排序,忽略与 Mailbox 中更新的 Email 处于同一 Thread 的 Email(即前 30 个唯一 Thread)。
- "1":现在我们利用反向引用获取每个 Email id 的 Thread id。
- "2":另一个反向引用获取每个 Thread id 的 Thread 对象。
- "3":最后,我们获取显示 Mailbox 列表所需(但仅此而已!)的、这 30 个 Thread 中每封 Email 的信息。客户端可汇总此数据进行显示,例如,若 Thread 中任意 Email 具有 "$flagged" 关键字,则将该 Thread 显示为"已标记"。
服务器的响应可能类似于:
[[ "Email/query", {
"accountId": "ue150411c",
"queryState": "09aa9a075588-780599:0",
"canCalculateChanges": true,
"position": 0,
"total": 115,
"ids": [ "Ma783e5cdf5f2deffbc97930a",
"M9bd17497e2a99cb345fc1d0a", ... ]
}, "0" ],
[ "Email/get", {
"accountId": "ue150411c",
"state": "780599",
"list": [{
"id": "Ma783e5cdf5f2deffbc97930a",
"threadId": "T36703c2cfe9bd5ed"
}, {
"id": "M9bd17497e2a99cb345fc1d0a",
"threadId": "T0a22ad76e9c097a1"
}, ... ],
"notFound": []
}, "1" ],
[ "Thread/get", {
"accountId": "ue150411c",
"state": "22a8728b",
"list": [{
"id": "T36703c2cfe9bd5ed",
"emailIds": [ "Ma783e5cdf5f2deffbc97930a" ]
}, {
"id": "T0a22ad76e9c097a1",
"emailIds": [ "M3b568670a63e5d100f518fa5",
"M9bd17497e2a99cb345fc1d0a" ]
}, ... ],
"notFound": []
}, "2" ],
[ "Email/get", {
"accountId": "ue150411c",
"state": "780599",
"list": [{
"id": "Ma783e5cdf5f2deffbc97930a",
"threadId": "T36703c2cfe9bd5ed",
"mailboxIds": {
"fb666a55": true
},
"keywords": {
"$seen": true,
"$flagged": true
},
"hasAttachment": true,
"from": [{
"email": "jdoe@example.com",
"name": "Jane Doe"
}],
"subject": "The Big Reveal",
"receivedAt": "2018-06-27T00:20:35Z",
"size": 175047,
"preview": "As you may be aware, we are required to prepare a
presentation where we wow a panel of 5 random members of the
public, on or before 30 June each year. We have drafted..."
},
...
],
"notFound": []
}, "3" ]]
现在,在另一台设备上,用户将第一封 Email 标记为未读,发送此 API 请求:
[[ "Email/set", {
"accountId": "ue150411c",
"update": {
"Ma783e5cdf5f2deffbc97930a": {
"keywords/$seen": null
}
}
}, "0" ]]
服务器应用此变更并发送成功响应:
[[ "Email/set", {
"accountId": "ue150411c",
"oldState": "780605",
"newState": "780606",
"updated": {
"Ma783e5cdf5f2deffbc97930a": null
},
...
}, "0" ]]
用户还删除了几封 Email,随后一封新消息到达。
回到我们的原始机器,我们收到一个推送更新,指出 Email 的状态字符串现在为 "780800"。由于这与客户端当前状态不匹配,它发出请求以获取变更:
[[ "Email/changes", {
"accountId": "ue150411c",
"sinceState": "780605",
"maxChanges": 50
}, "3" ],
[ "Email/queryChanges", {
"accountId": "ue150411c",
"filter": {
"inMailbox": "fb666a55"
},
"sort": [{
"property": "receivedAt",
"isAscending": false
}],
"collapseThreads": true,
"sinceQueryState": "09aa9a075588-780599:0",
"upToId": "Mc2781d5e856a908d8a35a564",
"maxChanges": 25,
"calculateTotal": true
}, "11" ]]
响应:
[[ "Email/changes", {
"accountId": "ue150411c",
"oldState": "780605",
"newState": "780800",
"hasMoreChanges": false,
"created": [ "Me8de6c9f6de198239b982ea2" ],
"updated": [ "Ma783e5cdf5f2deffbc97930a" ],
"destroyed": [ "M9bd17497e2a99cb345fc1d0a", ... ]
}, "3" ],
[ "Email/queryChanges", {
"accountId": "ue150411c",
"oldQueryState": "09aa9a075588-780599:0",
"newQueryState": "e35e9facf117-780615:0",
"added": [{
"id": "Me8de6c9f6de198239b982ea2",
"index": 0
}],
"removed": [ "M9bd17497e2a99cb345fc1d0a" ],
"total": 115
}, "11" ]]
客户端可通过移除 "M9bd17497e2a99cb345fc1d0a" 然后将 "Me8de6c9f6de198239b982ea2" 拼接到位置 0,来更新其查询结果本地缓存。由于它没有这封新 Email 的数据,它将随后获取它(也可以在同一个请求中使用反向引用完成此操作)。
它知道 "Ma783e5cdf5f2deffbc97930a" 发生了某些变更,因此也将重新获取该 Email 的 Mailbox id 与关键字(仅有的可变属性)。
用户开始撰写一封新 Email。该邮件为纯文本,且客户端知道其为英文,于是将此项元数据添加到正文部分。用户在撰写仍在进行时保存了一个草稿。客户端发送:
[[ "Email/set", {
"accountId": "ue150411c",
"create": {
"k192": {
"mailboxIds": {
"2ea1ca41b38e": true
},
"keywords": {
"$seen": true,
"$draft": true
},
"from": [{
"name": "Joe Bloggs",
"email": "joe@example.com"
}],
"subject": "World domination",
"receivedAt": "2018-07-10T01:03:11Z",
"sentAt": "2018-07-10T11:03:11+10:00",
"bodyStructure": {
"type": "text/plain",
"partId": "bd48",
"header:Content-Language": "en"
},
"bodyValues": {
"bd48": {
"value": "I have the most brilliant plan. Let me tell
you all about it. What we do is, we",
"isTruncated": false
}
}
}
}
}, "0" ]]
服务器创建该消息并发送成功响应:
[[ "Email/set", {
"accountId": "ue150411c",
"oldState": "780823",
"newState": "780839",
"created": {
"k192": {
"id": "Mf40b5f831efa7233b9eb1c7f",
"blobId": "Gf40b5f831efa7233b9eb1c7f8f97d84eeeee64f7",
"threadId": "Td957e72e89f516dc",
"size": 359
}
},
...
}, "0" ]]
服务器上创建的消息类似于:
Message-Id: <bbce0ae9-58be-4b24-ac82-deb840d58016@sloti7d1t02> User-Agent: Cyrus-JMAP/3.1.6-736-gdfb8e44 Mime-Version: 1.0 Date: Tue, 10 Jul 2018 11:03:11 +1000 From: "Joe Bloggs" <joe@example.com> Subject: World domination Content-Language: en Content-Type: text/plain I have the most brilliant plan. Let me tell you all about it. What we do is, we
用户添加了一个收件人,并将该消息转换为 HTML 以便添加格式,随后保存更新后的草稿:
[[ "Email/set", {
"accountId": "ue150411c",
"create": {
"k1546": {
"mailboxIds": {
"2ea1ca41b38e": true
},
"keywords": {
"$seen": true,
"$draft": true
},
"from": [{
"name": "Joe Bloggs",
"email": "joe@example.com"
}],
"to": [{
"name": "John",
"email": "john@example.com"
}],
"subject": "World domination",
"receivedAt": "2018-07-10T01:05:08Z",
"sentAt": "2018-07-10T11:05:08+10:00",
"bodyStructure": {
"type": "multipart/alternative",
"subParts": [{
"partId": "a49d",
"type": "text/html",
"header:Content-Language": "en"
}, {
"partId": "bd48",
"type": "text/plain",
"header:Content-Language": "en"
}]
},
"bodyValues": {
"bd48": {
"value": "I have the most brilliant plan. Let me tell
you all about it. What we do is, we",
"isTruncated": false
},
"a49d": {
"value": "<!DOCTYPE html><html><head><title></title>
<style type=\"text/css\">div{font-size:16px}</style></head>
<body><div>I have the most <b>brilliant</b> plan. Let me
tell you all about it. What we do is, we</div></body>
</html>",
"isTruncated": false
}
}
}
},
"destroy": [ "Mf40b5f831efa7233b9eb1c7f" ]
}, "0" ]]
服务器创建新草稿、删除旧草稿,并发送成功响应:
[[ "Email/set", {
"accountId": "ue150411c",
"oldState": "780839",
"newState": "780842",
"created": {
"k1546": {
"id": "Md45b47b4877521042cec0938",
"blobId": "Ge8de6c9f6de198239b982ea214e0f3a704e4af74",
"threadId": "Td957e72e89f516dc",
"size": 11721
}
},
"destroyed": [ "Mf40b5f831efa7233b9eb1c7f" ],
...
}, "0" ]]
客户端将此草稿移动到另一账户。做到这一点的唯一方式是通过 "Email/copy" 方法。它必须设置一个新的 "mailboxIds" 属性,因为当前值在新目标账户中将不是有效的 Mailbox id:
[[ "Email/copy", {
"fromAccountId": "ue150411c",
"accountId": "u6c6c41ac",
"create": {
"k45": {
"id": "Md45b47b4877521042cec0938",
"mailboxIds": {
"75a4c956": true
}
}
},
"onSuccessDestroyOriginal": true
}, "0" ]]
服务器成功复制该 Email 并删除原始 Email。由于隐含调用了 "Email/set",对单一方法调用有两个响应,二者具有相同的方法调用 id:
[[ "Email/copy", {
"fromAccountId": "ue150411c",
"accountId": "u6c6c41ac",
"oldState": "7ee7e9263a6d",
"newState": "5a0d2447ed26",
"created": {
"k45": {
"id": "M138f9954a5cd2423daeafa55",
"blobId": "G6b9fb047cba722c48c611e79233d057c6b0b74e8",
"threadId": "T2f242ea424a4079a",
"size": 11721
}
},
"notCreated": null
}, "0" ],
[ "Email/set", {
"accountId": "ue150411c",
"oldState": "780842",
"newState": "780871",
"destroyed": [ "Md45b47b4877521042cec0938" ],
...
}, "0" ]]
5. 搜索摘要(Search Snippets)
当对 "String" 属性进行搜索时,客户端可能希望将正文中与搜索匹配的相关部分作为预览显示,并在该部分以及 Email 的主题中高亮任何匹配词。搜索摘要(Search Snippet)即表示此数据。
一个 *SearchSnippet* 对象具有以下属性:
- emailId: "Id"
- 该摘要所适用的 Email id。
- subject: "String|null"
- 若过滤器中的文本与主题匹配,则这是经过以下变换的 Email 主题:
- 以下三个字符的任意实例必须替换为适当的 HTML 实体:&(& 符)、<(小于号)与 >(大于号)[HTML]。其他字符也可替换为 HTML 实体形式。
- 过滤器中的匹配词/短语被包裹在 HTML 的 "<mark></mark>" 标签中。
- 若过滤器中的文本与主题匹配,则这是经过以下变换的 Email 主题:
若主题未匹配过滤器中的文本,此属性为 null。
- preview: "String|null"
- 若过滤器中的文本匹配纯文本或 HTML 正文,则这是正文的相关部分(若原为 HTML 则转换为纯文本),并经过与 "subject" 属性相同的变换。其大小不得超过 255 个八位组。若正文不包含对过滤器文本的匹配,此属性为 null。
正文的相关部分用于预览,由服务器定义。若服务器无法确定搜索摘要,它必须为 "subject" 与 "preview" 属性都返回 null。
注意,与大多数数据类型不同,SearchSnippet 没有名为 "id" 的属性。
支持以下 JMAP 方法。
5.1. SearchSnippet/get
要获取搜索摘要,调用 "SearchSnippet/get"。它接受以下参数:
- accountId: "Id"
- 要使用的账户 id。
- filter: "FilterOperator|FilterCondition|null"
- 与传递给 "Email/query" 的过滤器相同;详见第 4.4 节对该方法的描述。
- emailIds: "Id[]"
- 要获取摘要的 Email 的 id。
响应具有以下参数:
- accountId: "Id"
- 调用所用账户的 id。
- list: "SearchSnippet[]"
- 请求 Email id 的 SearchSnippet 对象数组。其顺序可能与请求中的 id 顺序不同。
- notFound: "Id[]|null"
- 请求的、无法找到的 Email id 数组,若全部找到则为 null。
由于搜索摘要派生于消息内容,且导出算法可能随时间改变,第二次获取相同摘要可能返回不同结果。然而,先前的值不被视为错误,因此不需要状态字符串或更新机制。
可能返回以下额外错误以代替 "SearchSnippet/get" 响应:
"requestTooLarge":客户端请求的 "emailIds" 数量超过了服务器愿意在单次方法调用中处理的最大数量。
"unsupportedFilter":服务器由于任何原因无法处理给定的 "filter"。
5.2. 示例
这里,我们执行了一个 "Email/query" 来搜索账户中包含单词 "foo" 的任何 Email;现在,我们为结果中返回的部分 id 获取搜索摘要:
[[ "SearchSnippet/get", {
"accountId": "ue150411c",
"filter": {
"text": "foo"
},
"emailIds": [
"M44200ec123de277c0c1ce69c",
"M7bcbcb0b58d7729686e83d99",
"M28d12783a0969584b6deaac0",
...
]
}, "0" ]]
响应示例:
[[ "SearchSnippet/get", {
"accountId": "ue150411c",
"list": [{
"emailId": "M44200ec123de277c0c1ce69c",
"subject": null,
"preview": null
}, {
"emailId": "M7bcbcb0b58d7729686e83d99",
"subject": "The <mark>Foo</mark>sball competition",
"preview": "...year the <mark>foo</mark>sball competition will
be held in the Stadium de ..."
}, {
"emailId": "M28d12783a0969584b6deaac0",
"subject": null,
"preview": "...the <mark>Foo</mark>/bar method results often
returns <1 widget rather than the complete..."
},
...
],
"notFound": null
}, "0" ]]
6. 身份(Identities)
一个 *Identity* 对象存储有关用户可从其发送邮件的电子邮件地址或域的信息。它具有以下属性:
- id: "Id"(不可变;服务器设定)
- Identity 的 id。
- name: "String"(默认:"")
- 客户端从此 Identity 创建新 Email 时应使用的 "From" 名称。
- email: "String"(不可变)
- 客户端从此 Identity 创建新 Email 时必须使用的 "From" 电子邮件地址。若地址的 "mailbox" 部分("@" 之前的区段)是单个字符 "*"(例如 "*@example.com"),客户端可使用任何以该域结尾的有效地址(例如 "foo@example.com")。
- replyTo: "EmailAddress[]|null"(默认:null)
- 客户端从此 Identity 创建新 Email 时应设置的 Reply-To 值。
- bcc: "EmailAddress[]|null"(默认:null)
- 客户端从此 Identity 创建新 Email 时应设置的 Bcc 值。
- textSignature: "String"(默认:"")
- 客户端应插入到将从此 Identity 发送的纯文本新消息中的签名。客户端可忽略此项和/或将其与客户端特定的签名偏好组合。
- htmlSignature: "String"(默认:"")
- 客户端应插入到将从此 Identity 发送的 HTML 新消息中的签名。此文本必须是要插入到 HTML 的 "<body></body>" 区段中的 HTML 片段。客户端可忽略此项和/或将其与客户端特定的签名偏好组合。
- mayDelete: "Boolean"(服务器设定)
- 是否允许用户删除此 Identity?对于用户的用户名或其他默认地址,服务器可将此项设为 false。试图销毁 "mayDelete: false" 的 Identity 将以标准的 "forbidden" SetError 被拒绝。
有关 EmailAddress 的定义,见 Email 对象(第 4.1.2.3 节)中的 "Addresses" 头字段形式描述。
允许存在具有相同电子邮件地址的多个 Identity,以便用户可在不同设置之间选择(例如具有不同名称/签名)。
支持以下 JMAP 方法。
6.1. Identity/get
这是 [RFC8620] 第 5.1 节所述的标准 "/get" 方法。"ids" 参数可以为 null 以一次性获取全部。
6.2. Identity/changes
这是 [RFC8620] 第 5.2 节所述的标准 "/changes" 方法。
6.3. Identity/set
这是 [RFC8620] 第 5.3 节所述的标准 "/set" 方法。定义了以下额外的 SetError 类型:
对于 "create":
- "forbiddenFrom":不允许用户从作为 Identity 的 "email" 属性给定的地址发送。
6.4. 示例
请求:
[ "Identity/get", {
"accountId": "acme"
}, "0" ]
响应:
[ "Identity/get", {
"accountId": "acme",
"state": "99401312ae-11-333",
"list": [
{
"id": "XD-3301-222-11_22AAz",
"name": "Joe Bloggs",
"email": "joe@example.com",
"replyTo": null,
"bcc": [{
"name": null,
"email": "joe+archive@example.com"
}],
"textSignature": "-- \nJoe Bloggs\nMaster of Email",
"htmlSignature": "<div><b>Joe Bloggs</b></div>
<div>Master of Email</div>",
"mayDelete": false
},
{
"id": "XD-9911312-11_22AAz",
"name": "Joe B",
"email": "*@example.com",
"replyTo": null,
"bcc": null,
"textSignature": "",
"htmlSignature": "",
"mayDelete": true
}
],
"notFound": []
}, "0" ]
7. 邮件提交(Email Submission)
一个 *EmailSubmission* 对象表示将 Email 提交投递给一个或多个收件人。它具有以下属性:
- id: "Id"(不可变;服务器设定)
- EmailSubmission 的 id。
- identityId: "Id"(不可变)
- 与本提交关联的 Identity 的 id。
- emailId: "Id"(不可变)
- 要发送的 Email 的 id。被发送的 Email 不必是草稿,例如将现有 Email"重定向"到不同地址时。
- threadId: "Id"(不可变;服务器设定)
- 要发送的 Email 的 Thread id。服务器将其设为 "emailId" 所引用 Email 的 "threadId" 属性。
- envelope: "Envelope|null"(不可变)
- 通过 SMTP 发送时使用的信息。一个 *Envelope* 对象具有以下属性:
- mailFrom: "Address"
- 在 SMTP 提交中用作返回地址的电子邮件地址,以及随 MAIL FROM 地址传递的任何参数。JMAP 服务器可允许该地址为空字符串。
- mailFrom: "Address"
- 通过 SMTP 发送时使用的信息。一个 *Envelope* 对象具有以下属性:
当 JMAP 服务器执行 SMTP 消息提交时,它可将同一 id 字符串用于 ENVID 参数 [RFC3461] 与 EmailSubmission 对象 id。这样做(将 ENVID 设为服务器提供值)的服务器可替换客户端提供的 ENVID 值。
- rcptTo: "Address[]"
- 要将消息发送到的电子邮件地址,以及随收件人传递的任何 RCPT TO 参数。
一个 *Address* 对象具有以下属性:
- email: "String"
- 该对象所表示的电子邮件地址。这是 [RFC5321] 中 MAIL FROM 或 RCPT TO 命令的 Reverse-path 或 Forward-path 中使用的 "Mailbox"。
- parameters: "Object|null"
- 随电子邮件地址发送的任何参数(视情况为 mail-parameter 或 rcpt-parameter,如 [RFC5321] 所规定)。若提供,对象中的每个键是一个参数名,值为参数值(类型 "String"),若参数不带值则为 null。对于名称与值,任何 xtext 或 unitext 编码都被去除(见 [RFC3461] 与 [RFC6533]),并应用 JSON 字符串编码。
若 "envelope" 属性在创建时为 null 或省略,服务器必须按如下方式从所引用的 Email 生成:
- "mailFrom":Sender 头字段中的电子邮件地址(若存在);否则为 From 头字段中的电子邮件地址(若存在)。两种情况下都不添加参数。
- 若这些头字段中存在多个地址,或存在多于一个 Sender/From 头字段,服务器应将 EmailSubmission 视为无效而拒绝;否则,它必须取最后一个 Sender/From 头字段中的第一个地址。
- 若由此找到的地址不被与本提交关联的 Identity 所允许,则必须改用该 Identity 的 "email" 属性。
- "rcptTo":来自 To、Cc 与 Bcc 头字段(若存在)的去重电子邮件地址集合,且它们都不带参数。
- sendAt: "UTCDate"(不可变;服务器设定)
- 提交已/将被释放以投递的日期。若客户端成功使用了 FUTURERELEASE [RFC4865] 与提交,则这必须是服务器将释放消息的时间;否则,它必须是 EmailSubmission 被创建的时间。
- undoStatus: "String"
- 这表示提交是否可被取消。这在创建时由服务器设定,且必须为以下值之一:
- "pending":可能可以取消此提交。
- "final":消息已以无法撤回的方式转发给至少一个收件人。不再可能取消此提交。
- "canceled":提交已被取消,不会投递给任何收件人。
- 这表示提交是否可被取消。这在创建时由服务器设定,且必须为以下值之一:
在不支持撤回的系统上,此属性的值将始终为 "final"。在支持取消提交的系统上,它将以 "pending" 开始,并可能在服务器确定其肯定无法召回消息时转变为 "final",但它也可能仅保持 "pending"。若处于 pending 状态,客户端可通过将此属性设为 "canceled" 来尝试取消提交;若更新成功,则提交被成功取消,且消息未被投递给任何原始收件人。
- deliveryStatus: "String[DeliveryStatus]|null"(服务器设定)
- 这表示提交各收件人的投递状态(若已知)。此属性可能不被所有服务器支持,在此情况下它将保持 null。支持它的服务器应在任何收件人状态变化时更新 EmailSubmission 对象,即使某些收件人仍在重试。
此值是从每个收件人的电子邮件地址到 DeliveryStatus 对象的映射。
一个 *DeliveryStatus* 对象具有以下属性:
- smtpReply: "String"
- 服务器上次尝试转发消息时为此收件人返回的 SMTP 回复字符串,或在消息后续的投递状态通知(DSN,如 [RFC3464] 所定义)响应中返回。这应为对 RCPT TO 阶段的响应,除非该阶段被接受而消息整体在 DATA 阶段末尾被拒绝,在此情况下应改用 DATA 阶段的回复。
多行 SMTP 响应应按如下方式连接为单个字符串:
- 除最后一行外,所有行中 SMTP 代码后的连字符被替换为空格。
- 第一行之后的行中,任何与第一行相同的公共前缀被去除。
- CRLF 被替换为空格。
例如:
550-5.7.1 Our system has detected that this message is
550 5.7.1 likely spam.
将变为:
550 5.7.1 Our system has detected that this message is likely spam.
对于经由 SMTP 替代方式转发的消息,服务器可生成表示状态的合成字符串。若如此,该字符串必须采用以下形式:
- 一个 3 位数的 SMTP 回复码,如 [RFC5321] 第 4.2.3 节所定义。
- 然后一个空格字符。
- 然后一个如 [RFC3463] 所定义、并在 [RFC5248] 中定义注册表的 SMTP 增强邮件系统状态码。
- 然后一个空格字符。
- 然后一个实现特定的信息字符串,带有对响应的可读解释。
- delivered: "String"
- 表示消息是否已成功投递给收件人。这必须为以下值之一:
- "queued":消息位于本地邮件队列中,一旦退出本地邮件队列状态将改变。"smtpReply" 属性仍可能改变。
- "yes":消息已成功投递到收件人的邮件存储。"smtpReply" 属性为最终值。
- "no":向收件人的投递永久失败。"smtpReply" 属性为最终值。
- "unknown":最终投递状态未知(例如,它被转发到外部机器且无可用的进一步信息)。若到达 DSN,"smtpReply" 属性仍可能改变。
- 表示消息是否已成功投递给收件人。这必须为以下值之一:
注意,成功转发到外部 SMTP 服务器不应被视为消息已成功到达最终邮件存储的指示。不过在此情况下,若被请求,服务器可能收到 DSN 响应。
若收到收件人的 DSN,且 Action 等于 "delivered"(依 [RFC3464] 第 2.3.3 节),则 "delivered" 属性应设为 "yes";若 Action 等于 "failed",该属性应设为 "no"。收到任何其他 DSN 不应影响此属性。
服务器也可基于其他反馈渠道设置此属性。
- displayed: "String"
- 表示消息是否已显示给收件人。这必须为以下值之一:
- "unknown":显示状态未知。这是初始值。
- "yes":收件人的系统声称消息内容已显示给收件人。注意,并不保证收件人已注意到、阅读或理解了内容。
- 表示消息是否已显示给收件人。这必须为以下值之一:
若收到此收件人的消息处置通知(MDN),且 Disposition-Type(依 [RFC8098] 第 3.2.6.2 节)等于 "displayed",则此属性应设为 "yes"。
服务器也可基于其他反馈渠道设置此属性。
- dsnBlobIds: "Id[]"(服务器设定)
- 为本提交收到的 DSN [RFC3464] 的 blob id 列表,按接收顺序,最旧的在前。该 blob 是整个 MIME 消息(顶层内容类型为 "multipart/report"),如所收到。
- mdnBlobIds: "Id[]"(服务器设定)
- 为本提交收到的 MDN [RFC8098] 的 blob id 列表,按接收顺序,最旧的在前。该 blob 是整个 MIME 消息(顶层内容类型为 "multipart/report"),如所收到。
若 JMAP 服务器关联到某个 EmailSubmission 对象,它可选择不将 DSN 与 MDN 响应作为 Email 对象暴露。它应仅在改为在 "dsnBlobIds" 与 "mdnBlobIds" 字段中暴露它们时才这样做,并且它期望用户使用能够通过 EmailSubmission 对象获取并显示投递状态的客户端。
为求效率,服务器可在消息成功发送后、或完成重试发送后随时销毁 EmailSubmission 对象。对于非常基础的 SMTP 代理,这可能是在创建后立即进行,因为它无法分配真实 id 并在稍后获取时再次返回该信息。
支持以下 JMAP 方法。
7.1. EmailSubmission/get
这是 [RFC8620] 第 5.1 节所述的标准 "/get" 方法。
7.2. EmailSubmission/changes
这是 [RFC8620] 第 5.2 节所述的标准 "/changes" 方法。
7.3. EmailSubmission/query
这是 [RFC8620] 第 5.5 节所述的标准 "/query" 方法。
一个 *FilterCondition* 对象具有以下属性,均可省略:
- identityIds: "Id[]"
- EmailSubmission 的 "identityId" 属性必须在此列表中才能匹配该条件。
- emailIds: "Id[]"
- EmailSubmission 的 "emailId" 属性必须在此列表中才能匹配该条件。
- threadIds: "Id[]"
- EmailSubmission 的 "threadId" 属性必须在此列表中才能匹配该条件。
- undoStatus: "String"
- EmailSubmission 的 "undoStatus" 属性必须与给定值完全相同才能匹配该条件。
- before: "UTCDate"
- EmailSubmission 对象的 "sendAt" 属性必须早于该日期时间才能匹配该条件。
- after: "UTCDate"
- EmailSubmission 对象的 "sendAt" 属性必须相同于或晚于该日期时间才能匹配该条件。
当且仅当所有给定条件都匹配时,EmailSubmission 对象才匹配该 FilterCondition。若未指定任何属性,则对所有对象自动为真。
以下 EmailSubmission 属性必须支持排序:
- "emailId"
- "threadId"
- "sentAt"
7.4. EmailSubmission/queryChanges
这是 [RFC8620] 第 5.6 节所述的标准 "/queryChanges" 方法。
7.5. EmailSubmission/set
这是 [RFC8620] 第 5.3 节所述的标准 "/set" 方法,带有以下两个额外请求参数:
- onSuccessUpdateEmail: "Id[PatchObject]|null"
- 从 EmailSubmission id 到包含要在所引用的 EmailSubmission 的 create/update/destroy 成功时更新到其所引用 Email 对象上的属性的对象的映射。(对于在同一 "/set" 调用中创建的 EmailSubmission 的引用,这等同于创建引用,因此 id 将以 "#" 为前缀。)
- onSuccessDestroyEmail: "Id[]|null"
- EmailSubmission id 列表,对于其中每一个,若 create/update/destroy 成功,则对应的 "emailId" 所引用的 Email 应被销毁。(对于 EmailSubmission 创建的引用,这等同于创建引用,因此 id 将以 "#" 为前缀。)
在处理完 "EmailSubmission/set" 调用中的所有 create/update/destroy 项之后,必须进行一次隐式的 "Email/set" 调用来执行这两个参数中请求的任何变更。该调用的响应必须在 "EmailSubmission/set" 响应之后返回。
通过创建 EmailSubmission 对象来发送 Email。在处理每个 create 时,服务器必须检查消息是否有效,且用户拥有充分授权来发送它。若创建成功,消息将被发送到信封 "rcptTo" 参数中给出的收件人。服务器必须在投递期间移除消息上存在的任何 Bcc 头字段。服务器可按照其策略在投递期间添加或移除提交消息的其他头字段,或进行进一步更改。
若在 EmailSubmission 对象创建之后的任何时刻所引用的 Email 被销毁,这不得改变提交的行为(即它不会取消未来的发送)。EmailSubmission 对象的 "emailId" 与 "threadId" 属性保留,但若相应对象已被销毁,尝试获取它们(通过标准 "Email/get" 调用)将返回 "notFound" 错误。
类似地,销毁 EmailSubmission 对象不得影响它所表示的投递。它纯粹移除该提交的记录。服务器可在一段时间后或响应其他触发器时自动销毁 EmailSubmission 对象,并可禁止客户端手动销毁 EmailSubmission 对象。
若待发送的消息大于服务器支持发送的大小,必须返回标准的 "tooLarge" SetError。SetError 上必须存在一个 "maxSize" "UnsignedInt" 属性,指定可发送消息的最大大小(以八位组计)。
若给定的 Email 或 Identity id 无法找到,提交创建以标准的 "invalidProperties" SetError 被拒绝。
定义了以下额外的 SetError 类型:
对于 "create":
- "invalidEmail" - 待发送的 Email 在某种方式上无效。SetError 应包含一个类型为 "String[]"、名为 "properties" 的属性,列出 Email 中所有无效的属性。
- "tooManyRecipients" - 信封(提供的或生成的)的收件人数量超过服务器允许的数量。SetError 上还必须存在 "maxRecipients" "UnsignedInt" 属性,指定允许的最大收件人数量。
- "noRecipients" - 信封(提供的或生成的)没有任何 rcptTo 电子邮件地址。
- "invalidRecipients" - 信封(提供的或生成的)的 "rcptTo" 属性包含至少一个不是可发送的有效电子邮件地址的 rcptTo 值。SetError 上还必须存在 "invalidRecipients" "String[]" 属性,其为无效地址的列表。
- "forbiddenMailFrom" - 服务器不允许用户发送带有信封 From 地址 [RFC5321] 的消息。
- "forbiddenFrom" - 服务器不允许用户发送带有待发送消息的 From 头字段 [RFC5322] 的消息。
- "forbiddenToSend" - 由于某种原因,用户当前根本没有发送权限。SetError 对象上可存在一个 "description" "String" 属性,向用户显示他们不被允许的原因。
对于 "update":
- "cannotUnsend" - 客户端试图将有效 EmailSubmission 对象的 "undoStatus" 从 "pending" 更新为 "canceled",但消息无法被撤回。
7.5.1. 示例
以下示例假定待发送 Email 的草稿已被保存,且其 Email id 为 "M7f6ed5bcfd7e2604d1753f6c"。此调用随后立即发送该 Email,并在成功时移除 "$draft" 标记,并将其从草稿文件夹(Mailbox id 为 "7cb4e8ee-df87-4757-b9c4-2ea1ca41b38e")移动到已发送文件夹(我们假定其 Mailbox id 为 "73dbcb4b-bffc-48bd-8c2a-a2e91ca672f6")。
[[ "EmailSubmission/set", {
"accountId": "ue411d190",
"create": {
"k1490": {
"identityId": "I64588216",
"emailId": "M7f6ed5bcfd7e2604d1753f6c",
"envelope": {
"mailFrom": {
"email": "john@example.com",
"parameters": null
},
"rcptTo": [{
"email": "jane@example.com",
"parameters": null
},
...
]
}
}
},
"onSuccessUpdateEmail": {
"#k1490": {
"mailboxIds/7cb4e8ee-df87-4757-b9c4-2ea1ca41b38e": null,
"mailboxIds/73dbcb4b-bffc-48bd-8c2a-a2e91ca672f6": true,
"keywords/$draft": null
}
}
}, "0" ]]
成功的响应可能如下所示。注意由于隐式的 "Email/set" 调用,存在两个响应,但二者具有相同的方法调用 id,因为它们源于请求中的同一次调用:
[[ "EmailSubmission/set", {
"accountId": "ue411d190",
"oldState": "012421s6-8nrq-4ps4-n0p4-9330r951ns21",
"newState": "355421f6-8aed-4cf4-a0c4-7377e951af36",
"created": {
"k1490": {
"id": "ES-3bab7f9a-623e-4acf-99a5-2e67facb02a0"
}
}
}, "0" ],
[ "Email/set", {
"accountId": "ue411d190",
"oldState": "778193",
"newState": "778197",
"updated": {
"M7f6ed5bcfd7e2604d1753f6c": null
}
}, "0" ]]
假设管理员改为移除了用户的发送权限,因此提交以 "forbiddenToSend" 错误被拒绝。该错误的 description 参数旨在显示给用户,因此应适当本地化。假设请求是带着如下 Accept-Language 头发送的:
Accept-Language: de;q=0.9,en;q=0.8
服务器应尝试基于 Accept-Language 头,从其所拥有的本地化中选择最佳的,如 [RFC8620] 第 3.8 节所述。若服务器拥有英语、法语与德语翻译,它将选择德语作为首选语言,并返回如下响应:
[[ "EmailSubmission/set", {
"accountId": "ue411d190",
"oldState": "012421s6-8nrq-4ps4-n0p4-9330r951ns21",
"newState": "012421s6-8nrq-4ps4-n0p4-9330r951ns21",
"notCreated": {
"k1490": {
"type": "forbiddenToSend",
"description": "Verzeihung, wegen verdaechtiger Aktivitaeten Ihres
Benutzerkontos haben wir den Versand von Nachrichten gesperrt.
Bitte wenden Sie sich fuer Hilfe an unser Support Team."
}
}
}, "0" ]]
8. 休假回复(Vacation Response)
休假回复在消息投递到邮件存储时发送自动回复,告知原始发件人其消息可能在一段时间内不会被阅读。
自动发送消息可能产生不良行为。为避免此问题,实现者必须遵循 [RFC3834] 中提出的建议。
*VacationResponse* 对象表示账户中与休假回复相关设置的状态。它具有以下属性:
- id: "Id"(不可变;服务器设定)
- 该对象的 id。永远只有一个 VacationResponse 对象,其 id 为 "singleton"。
- isEnabled: "Boolean"
- 若在 "fromDate" 与 "toDate" 之间到达消息,是否应发送休假回复?
- fromDate: "UTCDate|null"
- 若 "isEnabled" 为 true,则在此日期时间或之后(但在此之前若已定义 "toDate")到达的消息应收到用户的休假回复。若为 null,则休假回复立即生效。
- toDate: "UTCDate|null"
- 若 "isEnabled" 为 true,则在此日期时间之前(但在此日期或之后若已定义 "fromDate")到达的消息应收到用户的休假回复。若为 null,则休假回复无限期有效。
- subject: "String|null"
- 启用休假回复时,用于回复消息的主题。若为 null,服务器应设置适当的主题。
- textBody: "String|null"
- 启用休假回复时,用于回复消息的纯文本正文。若此值为 null,服务器在发送休假回复时应从 "htmlBody" 生成纯文本正文部分,但也可选择仅以 HTML 发送回复。若 "textBody" 与 "htmlBody" 均为 null,服务器应为回复生成适当的默认正文。
- htmlBody: "String|null"
- 启用休假回复时,用于回复消息的 HTML 正文。若此值为 null,服务器在发送休假回复时可选择从 "textBody" 生成 HTML 正文部分,或可选择仅以纯文本发送回复。
支持以下 JMAP 方法。
8.1. VacationResponse/get
这是 [RFC8620] 第 5.1 节所述的标准 "/get" 方法。
一个账户中必须且只能有一个 VacationResponse 对象。它必须具有 id "singleton"。
8.2. VacationResponse/set
这是 [RFC8620] 第 5.3 节所述的标准 "/set" 方法。
9. 安全考量
JMAP [RFC8620] 的所有安全考量均适用于本规范。针对本文档引入的数据类型与功能的额外考量在以下小节中描述。
9.1. EmailBodyPart 值
服务提供商通常对传入消息执行安全过滤,并且让安全过滤器对内容类型与字符集的检测与 JMAP 服务器所执行的启发式方法保持一致非常重要。对 EmailBodyValue 应用启发式方法来确定内容类型或字符集的服务器应记录这些启发式方法,并在其与特定邮件主机使用的安全过滤器不一致时提供将其关闭的机制。
允许为 ASCII 文本提供隐藏通道的字符集的自动转换(例如 UTF-7),在过去一直对安全过滤器造成问题,因此服务器实现可以通过默认关闭此类转换和/或使其可单独配置来缓解此风险。
为允许客户端限制其可在响应中接收的数据量,可以为文本正文部分返回的数据请求最大长度。然而,截断数据可能改变语义含义,例如截断 URL 会改变其位置。扫描恶意站点链接的服务器应小心确保截断不发生在语义重要点,或在返回之前重新扫描截断值中的恶意内容。
9.2. HTML 邮件显示
HTML 消息正文为消息提供了更丰富的格式,但也带来了一系列安全挑战,尤其是在与界面 HTML 结合嵌入到 webmail 上下文中时。渲染 HTML 消息的客户端应仔细考虑潜在风险,包括:
- 嵌入的 JavaScript 可在后续打开时重写消息以更改其内容,从而误导用户。在 webmail 系统中,若在同源下运行,它可访问并外泄用户可访问的所有私有数据,包括所有其他消息以及潜在的联系人、日历事件、设置与凭证。它还可通过外泄会话凭证或安装可拦截所有后续网络请求的服务工作线程(service worker)来重写界面以不易察觉地钓鱼密码(不过,这只有在同一源上也可进行 blob 下载且服务工作线程脚本附加到消息时才有可能是可行的)。
- HTML 文档可能直接从 Internet 加载内容,而不仅仅是引用附加资源。例如,你可能有一个带有外部 "src" 属性的 "<img>" 标签。这可能在一封消息被打开时向发件人泄露收件人的 IP 地址。Cookie 也可能被服务器发送与设置,从而在不同消息甚至网站访问与广告档案之间实现跟踪。
- 在 webmail 系统中,CSS 可破坏布局或制造钓鱼漏洞。例如,使用 "position:fixed" 可让消息在其正常边界之外绘制内容,潜在地点击劫持真实的界面元素。
- 若处于 webmail 上下文且不在独立框架内,CSS 规则中定义的任何样式只要选择器匹配也将应用于界面元素,从而允许修改界面。类似地,任何匹配消息中元素的界面样式将改变其外观,可能破坏消息的布局。
- HTML 中的链接文本与实际链接目标之间并无必然关联,这可用于使钓鱼攻击更具说服力。
- 从消息打开的链接或嵌入的外部内容可能在大多数系统默认发送的 Referer 头中泄露私有信息。
- 表单可用于模仿登录框,若允许直接从消息显示中提交,则提供一个强大的钓鱼途径。
客户端有多种方式可以缓解这些问题,采用结合多种技术的纵深防御方法将提供最强的安全性。
- HTML 可在渲染前过滤,剥离潜在恶意内容。正确净化 HTML 是棘手的,强烈建议实现者使用经过充分测试的库,并采用经过仔细审查的仅白名单方法。未来可能向 HTML 渲染引擎添加具有意外安全特性的新特性;黑名单方法很可能带来安全问题。
- HTML 解析上的细微差异可能引入安全缺陷:要以 100% 的准确度过滤,你需要使用 HTML 渲染引擎将使用的同一个解析器。
- 将消息封装在 "<iframe sandbox>" 中(如 [HTML] 第 4.7.6 节所定义)有助于缓解多种风险。这将:
- 禁用 JavaScript。
- 禁用表单提交。
- 防止在其边界之外绘制,或防止消息 CSS 与界面 CSS 之间的冲突。
- 建立一个独特的匿名源,与包含它的源相分离。
- 强大的内容安全策略(Content Security Policy,见 <https://www.w3.org/TR/CSP3/>)除其他外,可在 JavaScript 与(若其设法避开过滤器)外部内容的加载得逞时将其阻断。
- Referer 头中信息的泄露可通过使用引用策略(referrer policy,见 <https://www.w3.org/TR/referrer-policy/>)来缓解。
- 在加载远程内容的标签上添加 "crossorigin=anonymous" 属性可防止 Cookie 被发送。
- 若添加 "target=_blank" 以在新标签页中打开链接,还应添加 "rel=noopener" 以确保打开的页面无法更改原始标签页中的 URL 以将用户重定向到钓鱼站点。
作为高度复杂的软件组件,HTML 渲染引擎显著增加了客户端的攻击面,尤其是在用于处理不可信、可能恶意的 content 时。
过去曾在图像解码器、JavaScript 引擎与 HTML 解析器中发现了严重缺陷,可能导致整个系统被攻陷。使用某引擎的客户端应确保获取最新版本,并持续并入供应商发布的任何安全补丁。
9.3. 多部分显示
消息可能由多个要按顺序作为正文显示的部分组成。客户端必须隔离渲染每个部分,且不得连接原始文本值来渲染。这样做可能改变消息的整体语义。若客户端或服务器正在解密 Pretty Good Privacy(PGP)或 S/MIME 加密的部分,与其他部分连接可能将解密后的文本泄露给攻击者,如 [EFAIL] 所述。
9.4. 邮件提交
SMTP 提交服务器 [RFC6409] 使用多种机制来缓解受损用户账户与终端系统造成的损害,包括速率限制、反病毒/反垃圾邮件 milter(邮件过滤器)以及其他技术。当这些技术拥有关于客户端连接的更多信息时,它们工作得更好。若 JMAP 邮件提交实现为对 SMTP 提交服务器的代理,则将此信息从 JMAP 代理传达给提交服务器是有用的。事实上的 SMTP XCLIENT 扩展 [XCLIENT] 可用于此,但建议使用经认证的通道,以将该扩展的使用限制于显式授权的代理。
代理到 SMTP 提交服务器的 JMAP 服务器应允许使用提交端口 [RFC8314]。强烈鼓励实现类似 SMTP XCLIENT 的机制。虽然简单认证与安全层(SASL)PLAIN over TLS [RFC4616] 目前是与 SMTP 提交服务器 [RFC4954] 互操作必须实现的机制,但 JMAP 提交代理应针对此用例实现并偏好更强的机制,例如带有 SASL EXTERNAL 的 TLS 客户端证书认证([RFC4422] 附录 A)或加盐质询响应认证机制(SCRAM)[RFC7677]。
若 JMAP 服务器直接将邮件转发到其他管理域的 SMTP 服务器,强烈鼓励实现事实上的 [milter] 协议,以集成第三方产品,处理包括反病毒/反垃圾邮件、声誉保护、合规归档与数据丢失防护在内的安全问题。代理到本地 SMTP 提交服务器可能是提供此类安全服务的更简单方式。
9.5. 部分账户访问
用户可能仅有权访问一个账户中存在的部分数据。为避免泄露未授权信息,在此情况下,服务器必须将用户无权访问的任何数据视为如同其不存在。
例如,假设用户 A 有一个包含两个 Mailbox(inbox 与 sent)的账户,但仅与用户 B 共享 inbox。在此情况下,当用户 B 获取该账户的 Mailbox 时,服务器必须表现得如同 sent Mailbox 不存在。类似地,在查询或获取 Email 对象时,它必须将仅属于 sent Mailbox 的任何消息视为如同其不存在。获取 Thread 对象必须仅返回用户有权访问的 Email 对象的 id;若没有,则 Thread 再次必须被视为如同其不存在。
若服务器禁止单个账户拥有两条相同消息、或两条具有相同 Message-Id 头字段的消息,则具有写权限的用户可利用尝试创建/导入此类消息时返回的错误,来探测其是否已存在于账户中不可访问的部分。
9.6. 从某地址发送的权限
近年来,电子邮件生态系统已转向将信任与消息 [RFC5322] 的 From 地址相关联,尤其是通过基于域的消息认证、报告与一致性(DMARC)[RFC7489] 等方案。
账户中的 Identity 对象集合(见第 6 节)让客户端知道用户有权从中发送的电子邮件地址。每次邮件提交都关联一个 Identity,服务器应拒绝消息的 From 头字段与所关联 Identity 不对应的提交。
服务器可允许例外,将已接收到邮件存储中的现有消息的精确副本发送到另一地址(也称为"重定向"或"退回/bounce"),尽管建议服务器将此限制于用户已验证其也控制的接收目的地。
若用户尝试创建新的 Identity 对象,且用户无权使用该电子邮件地址发送,服务器必须以适当的错误拒绝它。
SMTP MAIL FROM 地址 [RFC5321] 常与 From 消息头字段 [RFC5322] 混淆。用户通常只看到消息头字段中的地址,这也是要强制执行的主要地址。然而,服务器也必须对 MAIL FROM 地址 [RFC5321] 强制执行适当的限制,以阻止用户用退回与非投递通知淹没第三方地址。
JMAP 提交模型为两种上下文中的不可允许地址提供了独立的错误。
10. IANA 考量
10.1. "mail" 的 JMAP 能力注册
IANA 已按如下方式注册 "mail" JMAP 能力:
- 能力名称(Capability Name):urn:ietf:params:jmap:mail
- 规范文档(Specification document):本文档
- 预期用途(Intended use):common(通用)
- 变更控制者(Change Controller):IETF
- 安全与隐私考量(Security and privacy considerations):本文档第 9 节
10.2. "submission" 的 JMAP 能力注册
IANA 已按如下方式注册 "submission" JMAP 能力:
- 能力名称:urn:ietf:params:jmap:submission
- 规范文档:本文档
- 预期用途:common
- 变更控制者:IETF
- 安全与隐私考量:本文档第 9 节
10.3. "vacationresponse" 的 JMAP 能力注册
IANA 已按如下方式注册 "vacationresponse" JMAP 能力:
- 能力名称:urn:ietf:params:jmap:vacationresponse
- 规范文档:本文档
- 预期用途:common
- 变更控制者:IETF
- 安全与隐私考量:本文档第 9 节
10.4. IMAP 与 JMAP 关键字注册表
本文档对 [RFC5788] 所定义的 IMAP 关键字注册表做了两处变更。
第一,将该注册表的名称改为"IMAP 与 JMAP 关键字(IMAP and JMAP Keywords)注册表"。
第二,在模板与注册表中新增一个"作用域(scope)"列,用以说明某关键字适用于 "IMAP-only"(仅 IMAP)、"JMAP-only"(仅 JMAP)、"both"(两者)还是 "reserved"(保留)。已经存在于 IMAP 关键字注册表中的所有关键字均被标记为作用域 "both"。"reserved" 状态可用于阻止将来注册某个一旦被注册会造成困扰的名称。作用域为 "reserved" 的关键字的注册可省略注册模板中的大部分字段(参见下文 "$recent" 的注册示例);此类注册应当很少发生。
若协议中出现被标记为 "JMAP-only" 或 "reserved" 的关键字,IMAP 客户端可静默忽略。若协议中出现被标记为 "IMAP-only" 或 "reserved" 的关键字,JMAP 客户端可静默忽略。
新增的 "JMAP-only" 关键字在各小节中注册。这些关键字对应于 IMAP 系统关键字,因此不适合在 IMAP 中使用。除非通过标准行动(standards action),否则它们此后不能再被注册用于 IMAP。
10.4.1. JMAP 关键字 "$draft" 的注册
本小节在"IMAP 与 JMAP 关键字"注册表中注册 "JMAP-only" 关键字 "$draft"。
- 关键字名称:
$draft - 作用域:JMAP-only
- 用途(描述):当用户希望将消息作为正在撰写的草稿处理时设置。这是 IMAP
\Draft标志的 JMAP 等价物。 - 在服务器上为私有还是共享:BOTH(两者皆可)
- 它是建议性关键字,还是可能引发自动动作:自动(Automatic)。若账户中存在带有
\Drafts特殊用途属性的 IMAP 邮箱 [RFC6154],设置该标志可能会自动使消息出现在该邮箱中。某些 JMAP 计算值(如unreadEmails)会随该标志的改变而变化。此外,邮件客户端通常会将草稿消息显示在撰写窗口而非查看窗口中。 - 由谁在何时设置/清除:通常由 JMAP 客户端在涉及草稿消息时设置。一种草稿 Email 模型会在 "EmailSubmission/set" 操作中通过 "onSuccessUpdateEmail" 参数清除该标志。在 JMAP 与 IMAP 共享的邮件存储中,该标志也会按需被设置或清除,以匹配 IMAP 的
\Draft标志。 - 相关关键字:无
- 相关 IMAP/JMAP 能力:SPECIAL-USE [RFC6154]
- 安全考量:若服务器将该关键字实现为共享关键字,可能会披露用户将某消息视为草稿这一信息,该信息会被对 Mailbox 关键字拥有读取权限的其他用户看到。
- 发布规范:本文档
- 进一步联系的负责人与电子邮箱:JMAP 邮件列表 <jmap@ietf.org>
- 预期用途:COMMON
- 所有者/变更控制者:IESG
10.4.2. JMAP 关键字 "$seen" 的注册
本小节在"IMAP 与 JMAP 关键字"注册表中注册 "JMAP-only" 关键字 "$seen"。
- 关键字名称:
$seen - 作用域:JMAP-only
- 用途(描述):当用户希望将消息视为已读时设置。这是 IMAP
\Seen标志的 JMAP 等价物。 - 在服务器上为私有还是共享:BOTH
- 它是建议性关键字,还是可能引发自动动作:建议性(Advisory)。然而,某些 JMAP 计算值(如
unreadEmails)会随该标志的改变而变化。 - 由谁在何时设置/清除:由 JMAP 客户端在将消息内容呈现给用户时设置;客户端通常提供清除该标志的选项。在 JMAP 与 IMAP 共享的邮件存储中,该标志也会按需被设置或清除,以匹配 IMAP 的
\Seen标志。 - 相关关键字:无
- 相关 IMAP/JMAP 能力:无
- 安全考量:若服务器将该关键字实现为共享关键字,可能会披露用户已将某消息视为已读这一信息,该信息会被对 Mailbox 关键字拥有读取权限的其他用户看到。
- 发布规范:本文档
- 进一步联系的负责人与电子邮箱:JMAP 邮件列表 <jmap@ietf.org>
- 预期用途:COMMON
- 所有者/变更控制者:IESG
10.4.3. JMAP 关键字 "$flagged" 的注册
本小节在"IMAP 与 JMAP 关键字"注册表中注册 "JMAP-only" 关键字 "$flagged"。
- 关键字名称:
$flagged - 作用域:JMAP-only
- 用途(描述):当用户希望将消息标记为需要紧急/特别关注时设置。这是 IMAP
\Flagged标志的 JMAP 等价物。 - 在服务器上为私有还是共享:BOTH
- 它是建议性关键字,还是可能引发自动动作:自动。若账户中存在带有
\Flagged特殊用途属性的 IMAP 邮箱 [RFC6154],设置该标志可能会自动使消息出现在该邮箱中。 - 由谁在何时设置/清除:JMAP 客户端通常允许用户按需设置/清除该标志。在 JMAP 与 IMAP 共享的邮件存储中,该标志也会按需被设置或清除,以匹配 IMAP 的
\Flagged标志。 - 相关关键字:无
- 相关 IMAP/JMAP 能力:SPECIAL-USE [RFC6154]
- 安全考量:若服务器将该关键字实现为共享关键字,可能会披露用户将某消息标记为需要紧急/特别关注这一信息,该信息会被对 Mailbox 关键字拥有读取权限的其他用户看到。
- 发布规范:本文档
- 进一步联系的负责人与电子邮箱:JMAP 邮件列表 <jmap@ietf.org>
- 预期用途:COMMON
- 所有者/变更控制者:IESG
10.4.4. JMAP 关键字 "$answered" 的注册
本小节在"IMAP 与 JMAP 关键字"注册表中注册 "JMAP-only" 关键字 "$answered"。
- 关键字名称:
$answered - 作用域:JMAP-only
- 用途(描述):当消息已被回复时设置。
- 在服务器上为私有还是共享:BOTH
- 它是建议性关键字,还是可能引发自动动作:建议性。
- 由谁在何时设置/清除:JMAP 客户端通常会在提交对该消息的回复或答复时设置。它也可能由 "EmailSubmission/set" 操作通过 "onSuccessUpdateEmail" 参数设置。在 JMAP 与 IMAP 共享的邮件存储中,该标志也会按需被设置或清除,以匹配 IMAP 的
\Answered标志。 - 相关关键字:无
- 相关 IMAP/JMAP 能力:无
- 安全考量:若服务器将该关键字实现为共享关键字,可能会披露用户已回复某消息这一信息,该信息会被对 Mailbox 关键字拥有读取权限的其他用户看到。
- 发布规范:本文档
- 进一步联系的负责人与电子邮箱:JMAP 邮件列表 <jmap@ietf.org>
- 预期用途:COMMON
- 所有者/变更控制者:IESG
10.4.5. "$recent" 关键字的注册
本小节在"IMAP 与 JMAP 关键字"注册表中注册关键字 "$recent"。
- 关键字名称:
$recent - 作用域:reserved(保留)
- 用途(描述):该关键字不使用,以避免与 IMAP
\Recent系统标志混淆。 - 发布规范:本文档
- 进一步联系的负责人与电子邮箱:JMAP 邮件列表 <jmap@ietf.org>
- 所有者/变更控制者:IESG
10.5. IMAP 邮箱名称属性注册表
10.5.1. "inbox" 角色的注册
本小节在 [RFC8457] 所建立的"IMAP 邮箱名称属性(IMAP Mailbox Name Attributes)注册表"中注册 "JMAP-only" 属性 "inbox"。
- 属性名称:Inbox
- 描述:新邮件默认投递于此。
- 引用:本文档第 10.5.1 节
- 使用说明:仅 JMAP
10.6. JMAP 错误代码注册表
以下小节在 [RFC8620] 所定义的"JMAP 错误代码(JMAP Error Codes)注册表"中注册若干新的错误代码。
10.6.1. mailboxHasChild
- JMAP 错误代码:mailboxHasChild
- 预期用途:common
- 变更控制者:IETF
- 引用:本文档第 2.5 节
- 描述:该 Mailbox 至少还有一个子 Mailbox。客户端必须先移除这些子 Mailbox 才能删除父 Mailbox。
10.6.2. mailboxHasEmail
- JMAP 错误代码:mailboxHasEmail
- 预期用途:common
- 变更控制者:IETF
- 引用:本文档第 2.5 节
- 描述:该 Mailbox 至少分配有一封邮件,且 onDestroyRemoveEmails 参数为 false。
10.6.3. blobNotFound
- JMAP 错误代码:blobNotFound
- 预期用途:common
- 变更控制者:IETF
- 引用:本文档第 4.6 节
- 描述:对象中引用的至少一个 blob id 不存在。
10.6.4. tooManyKeywords
- JMAP 错误代码:tooManyKeywords
- 预期用途:common
- 变更控制者:IETF
- 引用:本文档第 4.6 节
- 描述:对 Email 关键字(keywords)的变更将超过服务器定义的最大值。
10.6.5. tooManyMailboxes
- JMAP 错误代码:tooManyMailboxes
- 预期用途:common
- 变更控制者:IETF
- 引用:本文档第 4.6 节
- 描述:对该 Email 所在的 Mailbox 集合的变更将超过服务器定义的最大值。
10.6.6. invalidEmail
- JMAP 错误代码:invalidEmail
- 预期用途:common
- 变更控制者:IETF
- 引用:本文档第 7.5 节
- 描述:待发送的 Email 在某些方面无效。
10.6.7. tooManyRecipients
- JMAP 错误代码:tooManyRecipients
- 预期用途:common
- 变更控制者:IETF
- 引用:本文档第 7.5 节
- 描述:信封 [RFC5321](所提供或自动生成的)收件人数量超过服务器允许的上限。
10.6.8. noRecipients
- JMAP 错误代码:noRecipients
- 预期用途:common
- 变更控制者:IETF
- 引用:本文档第 7.5 节
- 描述:信封 [RFC5321](所提供或自动生成的)没有任何 rcptTo 电子邮箱地址。
10.6.9. invalidRecipients
- JMAP 错误代码:invalidRecipients
- 预期用途:common
- 变更控制者:IETF
- 引用:本文档第 7.5 节
- 描述:信封 [RFC5321](所提供或自动生成的)的 rcptTo 属性中至少包含一个不是可发送的有效电子邮箱地址的 rcptTo 值。
10.6.10. forbiddenMailFrom
- JMAP 错误代码:forbiddenMailFrom
- 预期用途:common
- 变更控制者:IETF
- 引用:本文档第 7.5 节
- 描述:服务器不允许用户使用此信封 From 地址 [RFC5321] 发送消息。
10.6.11. forbiddenFrom
- JMAP 错误代码:forbiddenFrom
- 预期用途:common
- 变更控制者:IETF
- 引用:本文档第 6.3 节与第 7.5 节
- 描述:服务器不允许用户使用待发送消息的 From 头字段 [RFC5322] 发送消息。
10.6.12. forbiddenToSend
- JMAP 错误代码:forbiddenToSend
- 预期用途:common
- 变更控制者:IETF
- 引用:本文档第 7.5 节
- 描述:用户当前根本没有被许可发送消息。
11. 参考文献
11.1. 规范性参考文献
- [HTML] Faulkner, S., Eicholz, A., Leithead, T., Danilo, A., 与 S. Moon,"HTML 5.2",万维网联盟(W3C)推荐标准 REC-html52-20171214,2017 年 12 月,<https://www.w3.org/TR/html52/>。
- [RFC1870] Klensin, J., Freed, N., 与 K. Moore,"SMTP Service Extension for Message Size Declaration",STD 10,RFC 1870,DOI 10.17487/RFC1870,1995 年 11 月,<https://www.rfc-editor.org/info/rfc1870>。
- [RFC2045] Freed, N. 与 N. Borenstein,"Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies",RFC 2045,DOI 10.17487/RFC2045,1996 年 11 月,<https://www.rfc-editor.org/info/rfc2045>。
- [RFC2047] Moore, K.,"MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text",RFC 2047,DOI 10.17487/RFC2047,1996 年 11 月,<https://www.rfc-editor.org/info/rfc2047>。
- [RFC2119] Bradner, S.,"Key words for use in RFCs to Indicate Requirement Levels",BCP 14,RFC 2119,DOI 10.17487/RFC2119,1997 年 3 月,<https://www.rfc-editor.org/info/rfc2119>。
- [RFC2231] Freed, N. 与 K. Moore,"MIME Parameter Value and Encoded Word Extensions: Character Sets, Languages, and Continuations",RFC 2231,DOI 10.17487/RFC2231,1997 年 11 月,<https://www.rfc-editor.org/info/rfc2231>。
- [RFC2369] Neufeld, G. 与 J. Baer,"The Use of URLs as Meta-Syntax for Core Mail List Commands and their Transport through Message Header Fields",RFC 2369,DOI 10.17487/RFC2369,1998 年 7 月,<https://www.rfc-editor.org/info/rfc2369>。
- [RFC2392] Levinson, E.,"Content-ID and Message-ID Uniform Resource Locators",RFC 2392,DOI 10.17487/RFC2392,1998 年 8 月,<https://www.rfc-editor.org/info/rfc2392>。
- [RFC2557] Palme, J., Hopmann, A., 与 N. Shelness,"MIME Encapsulation of Aggregate Documents, such as HTML (MHTML)",RFC 2557,DOI 10.17487/RFC2557,1999 年 3 月,<https://www.rfc-editor.org/info/rfc2557>。
- [RFC2852] Newman, D.,"Deliver By SMTP Service Extension",RFC 2852,DOI 10.17487/RFC2852,2000 年 6 月,<https://www.rfc-editor.org/info/rfc2852>。
- [RFC3282] Alvestrand, H.,"Content Language Headers",RFC 3282,DOI 10.17487/RFC3282,2002 年 5 月,<https://www.rfc-editor.org/info/rfc3282>。
- [RFC3461] Moore, K.,"Simple Mail Transfer Protocol (SMTP) Service Extension for Delivery Status Notifications (DSNs)",RFC 3461,DOI 10.17487/RFC3461,2003 年 1 月,<https://www.rfc-editor.org/info/rfc3461>。
- [RFC3463] Vaudreuil, G.,"Enhanced Mail System Status Codes",RFC 3463,DOI 10.17487/RFC3463,2003 年 1 月,<https://www.rfc-editor.org/info/rfc3463>。
- [RFC3464] Moore, K. 与 G. Vaudreuil,"An Extensible Message Format for Delivery Status Notifications",RFC 3464,DOI 10.17487/RFC3464,2003 年 1 月,<https://www.rfc-editor.org/info/rfc3464>。
- [RFC3834] Moore, K.,"Recommendations for Automatic Responses to Electronic Mail",RFC 3834,DOI 10.17487/RFC3834,2004 年 8 月,<https://www.rfc-editor.org/info/rfc3834>。
- [RFC4314] Melnikov, A.,"IMAP4 Access Control List (ACL) Extension",RFC 4314,DOI 10.17487/RFC4314,2005 年 12 月,<https://www.rfc-editor.org/info/rfc4314>。
- [RFC4422] Melnikov, A.(编)与 K. Zeilenga(编),"Simple Authentication and Security Layer (SASL)",RFC 4422,DOI 10.17487/RFC4422,2006 年 6 月,<https://www.rfc-editor.org/info/rfc4422>。
- [RFC4616] Zeilenga, K.(编),"The PLAIN Simple Authentication and Security Layer (SASL) Mechanism",RFC 4616,DOI 10.17487/RFC4616,2006 年 8 月,<https://www.rfc-editor.org/info/rfc4616>。
- [RFC4865] White, G. 与 G. Vaudreuil,"SMTP Submission Service Extension for Future Message Release",RFC 4865,DOI 10.17487/RFC4865,2007 年 5 月,<https://www.rfc-editor.org/info/rfc4865>。
- [RFC4954] Siemborski, R.(编)与 A. Melnikov(编),"SMTP Service Extension for Authentication",RFC 4954,DOI 10.17487/RFC4954,2007 年 7 月,<https://www.rfc-editor.org/info/rfc4954>。
- [RFC5198] Klensin, J. 与 M. Padlipsky,"Unicode Format for Network Interchange",RFC 5198,DOI 10.17487/RFC5198,2008 年 3 月,<https://www.rfc-editor.org/info/rfc5198>。
- [RFC5248] Hansen, T. 与 J. Klensin,"A Registry for SMTP Enhanced Mail System Status Codes",BCP 138,RFC 5248,DOI 10.17487/RFC5248,2008 年 6 月,<https://www.rfc-editor.org/info/rfc5248>。
- [RFC5256] Crispin, M. 与 K. Murchison,"Internet Message Access Protocol - SORT and THREAD Extensions",RFC 5256,DOI 10.17487/RFC5256,2008 年 6 月,<https://www.rfc-editor.org/info/rfc5256>。
- [RFC5321] Klensin, J.,"Simple Mail Transfer Protocol",RFC 5321,DOI 10.17487/RFC5321,2008 年 10 月,<https://www.rfc-editor.org/info/rfc5321>。
- [RFC5322] Resnick, P.(编),"Internet Message Format",RFC 5322,DOI 10.17487/RFC5322,2008 年 10 月,<https://www.rfc-editor.org/info/rfc5322>。
- [RFC5788] Melnikov, A. 与 D. Cridland,"IMAP4 Keyword Registry",RFC 5788,DOI 10.17487/RFC5788,2010 年 3 月,<https://www.rfc-editor.org/info/rfc5788>。
- [RFC6154] Leiba, B. 与 J. Nicolson,"IMAP LIST Extension for Special-Use Mailboxes",RFC 6154,DOI 10.17487/RFC6154,2011 年 3 月,<https://www.rfc-editor.org/info/rfc6154>。
- [RFC6409] Gellens, R. 与 J. Klensin,"Message Submission for Mail",STD 72,RFC 6409,DOI 10.17487/RFC6409,2011 年 11 月,<https://www.rfc-editor.org/info/rfc6409>。
- [RFC6532] Yang, A., Steele, S., 与 N. Freed,"Internationalized Email Headers",RFC 6532,DOI 10.17487/RFC6532,2012 年 2 月,<https://www.rfc-editor.org/info/rfc6532>。
- [RFC6533] Hansen, T.(编),Newman, C., 与 A. Melnikov,"Internationalized Delivery Status and Disposition Notifications",RFC 6533,DOI 10.17487/RFC6533,2012 年 2 月,<https://www.rfc-editor.org/info/rfc6533>。
- [RFC6710] Melnikov, A. 与 K. Carlberg,"Simple Mail Transfer Protocol Extension for Message Transfer Priorities",RFC 6710,DOI 10.17487/RFC6710,2012 年 8 月,<https://www.rfc-editor.org/info/rfc6710>。
- [RFC7677] Hansen, T.,"SCRAM-SHA-256 and SCRAM-SHA-256-PLUS Simple Authentication and Security Layer (SASL) Mechanisms",RFC 7677,DOI 10.17487/RFC7677,2015 年 11 月,<https://www.rfc-editor.org/info/rfc7677>。
- [RFC8098] Hansen, T.(编)与 A. Melnikov(编),"Message Disposition Notification",STD 85,RFC 8098,DOI 10.17487/RFC8098,2017 年 2 月,<https://www.rfc-editor.org/info/rfc8098>。
- [RFC8174] Leiba, B.,"Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words",BCP 14,RFC 8174,DOI 10.17487/RFC8174,2017 年 5 月,<https://www.rfc-editor.org/info/rfc8174>。
- [RFC8314] Moore, K. 与 C. Newman,"Cleartext Considered Obsolete: Use of Transport Layer Security (TLS) for Email Submission and Access",RFC 8314,DOI 10.17487/RFC8314,2018 年 1 月,<https://www.rfc-editor.org/info/rfc8314>。
- [RFC8457] Leiba, B.(编),"IMAP "$Important" Keyword and "\Important" Special-Use Attribute",RFC 8457,DOI 10.17487/RFC8457,2018 年 9 月,<https://www.rfc-editor.org/info/rfc8457>。
- [RFC8474] Gondwana, B.(编),"IMAP Extension for Object Identifiers",RFC 8474,DOI 10.17487/RFC8474,2018 年 9 月,<https://www.rfc-editor.org/info/rfc8474>。
- [RFC8620] Jenkins, N. 与 C. Newman,"The JSON Meta Application Protocol",RFC 8620,DOI 10.17487/RFC8620,2019 年 6 月,<https://www.rfc-editor.org/info/rfc8620>。
11.2. 资料性参考文献
- [EFAIL] Poddebniak, D., Dresen, C., Mueller, J., Ising, F., Schinzel, S., Friedberger, S., Somorovsky, J., 与 J. Schwenk,"Efail: Breaking S/MIME and OpenPGP Email Encryption using Exfiltration Channels",2018 年 8 月,<https://www.usenix.org/system/files/conference/usenixsecurity18/sec18-poddebniak.pdf>。
- [milter] Postfix,"Postfix before-queue Milter support",2019,<http://www.postfix.org/MILTER_README.html>。
- [RFC3501] Crispin, M.,"INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1",RFC 3501,DOI 10.17487/RFC3501,2003 年 3 月,<https://www.rfc-editor.org/info/rfc3501>。
- [RFC7489] Kucherawy, M.(编)与 E. Zwicky(编),"Domain-based Message Authentication, Reporting, and Conformance (DMARC)",RFC 7489,DOI 10.17487/RFC7489,2015 年 3 月,<https://www.rfc-editor.org/info/rfc7489>。
- [XCLIENT] Postfix,"Postfix XCLIENT Howto",2019,<http://www.postfix.org/XCLIENT_README.html>。
作者地址
Neil Jenkins
Fastmail
PO Box 234, Collins St. West
Melbourne, VIC 8007
Australia(澳大利亚)
电子邮箱:neilj@fastmailteam.com
URI:https://www.fastmail.com
Chris Newman
Oracle
440 E. Huntington Dr., Suite 400
Arcadia, CA 91006
United States of America(美国)
电子邮箱:chris.newman@oracle.com
