You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by rc...@apache.org on 2020/07/30 08:27:12 UTC

[james-project] branch master updated (89fb5f4 -> 30a99e3)

This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git.


    from 89fb5f4  [REFACTORING] Override cleanup for RabbitMQAwsS3Stepdefs
     new d69b48f  JAMES-3319 Effective blob deletion for RabbitMQMailQueue
     new 9356dac  JAMES-3319 Add a test ensuring deduplication blobStore do not accidentaly delete similar blobs
     new 2676b9c  JAMES-3319 Mitigate CassandraMailQueueView deletion concurrency
     new 67b3a65  JAMES-3099 copy core and mail new JMAP specs into our source code
     new 05d9267  JAMES-3099 Add a subsection in config.jmap about JMAP versions in James
     new c9ccd9f  JAMES-3099 Annotate JMAP RFC-8621 mail specifications
     new c297b6e  JAMES-3099 Annotate JMAP RFC-8620 core specifications
     new 30a99e3  JAMES-3099 Add a little explanation in the README about annotations

The 8 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 server/protocols/jmap-rfc-8621/doc/README.md       |   17 +
 .../protocols/jmap-rfc-8621/doc/specs/LICENSE.md   |  190 +++
 server/protocols/jmap-rfc-8621/doc/specs/README.md |   28 +
 .../jmap-rfc-8621/doc/specs/rfc/src/jmap.mdown     |   55 +
 .../jmap-rfc-8621/doc/specs/rfc/src/mail.mdown     |   59 +
 .../jmap-rfc-8621/doc/specs/spec/jmap/api.mdown    | 1161 +++++++++++++++++
 .../jmap-rfc-8621/doc/specs/spec/jmap/binary.mdown |   97 ++
 .../doc/specs/spec/jmap/ianaconsiderations.mdown   |  189 +++
 .../jmap-rfc-8621/doc/specs/spec/jmap/intro.mdown  |  133 ++
 .../jmap-rfc-8621/doc/specs/spec/jmap/push.mdown   |  250 ++++
 .../specs/spec/jmap/securityconsiderations.mdown   |   80 ++
 .../doc/specs/spec/jmap/session.mdown              |  213 ++++
 .../doc/specs/spec/mail/ianaconsiderations.mdown   |  421 +++++++
 .../doc/specs/spec/mail/identity.mdown             |   89 ++
 .../jmap-rfc-8621/doc/specs/spec/mail/intro.mdown  |  154 +++
 .../doc/specs/spec/mail/mailbox.mdown              |  310 +++++
 .../doc/specs/spec/mail/message.mdown              | 1319 ++++++++++++++++++++
 .../doc/specs/spec/mail/messagesubmission.mdown    |  304 +++++
 .../doc/specs/spec/mail/searchsnippet.mdown        |   98 ++
 .../specs/spec/mail/securityconsiderations.mdown   |   96 ++
 .../jmap-rfc-8621/doc/specs/spec/mail/thread.mdown |   60 +
 .../doc/specs/spec/mail/vacationresponse.mdown     |   38 +
 .../org/apache/james/queue/rabbitmq/Dequeuer.java  |    2 +-
 .../apache/james/queue/rabbitmq/MailLoader.java    |    5 +-
 .../james/queue/rabbitmq/MailWithEnqueueId.java    |    9 +-
 .../queue/rabbitmq/view/api/DeleteCondition.java   |   13 +-
 .../view/cassandra/CassandraMailQueueView.java     |   27 +-
 .../queue/rabbitmq/RabbitMQMailQueueTest.java      |  141 ++-
 .../rabbitmq/view/api/DeleteConditionTest.java     |    6 +-
 .../CassandraMailQueueViewTestFactory.java         |    4 +-
 src/site/xdoc/server/config-jmap.xml               |   23 +-
 31 files changed, 5565 insertions(+), 26 deletions(-)
 create mode 100644 server/protocols/jmap-rfc-8621/doc/README.md
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/LICENSE.md
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/README.md
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/rfc/src/jmap.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/rfc/src/mail.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/api.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/binary.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/ianaconsiderations.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/intro.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/push.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/securityconsiderations.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/session.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/mail/ianaconsiderations.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/mail/identity.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/mail/intro.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/mail/mailbox.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/mail/message.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/mail/searchsnippet.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/mail/securityconsiderations.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/mail/thread.mdown
 create mode 100644 server/protocols/jmap-rfc-8621/doc/specs/spec/mail/vacationresponse.mdown


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 04/08: JAMES-3099 copy core and mail new JMAP specs into our source code

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 67b3a65ecc870e474170c49b10a152d562188a34
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Tue Jul 28 12:01:24 2020 +0700

    JAMES-3099 copy core and mail new JMAP specs into our source code
---
 server/protocols/jmap-rfc-8621/doc/README.md       |   11 +
 .../protocols/jmap-rfc-8621/doc/specs/LICENSE.md   |  190 +++
 server/protocols/jmap-rfc-8621/doc/specs/README.md |   28 +
 .../jmap-rfc-8621/doc/specs/rfc/src/jmap.mdown     |   55 +
 .../jmap-rfc-8621/doc/specs/rfc/src/mail.mdown     |   59 +
 .../jmap-rfc-8621/doc/specs/spec/jmap/api.mdown    | 1135 +++++++++++++++++
 .../jmap-rfc-8621/doc/specs/spec/jmap/binary.mdown |   93 ++
 .../doc/specs/spec/jmap/ianaconsiderations.mdown   |  189 +++
 .../jmap-rfc-8621/doc/specs/spec/jmap/intro.mdown  |  125 ++
 .../jmap-rfc-8621/doc/specs/spec/jmap/push.mdown   |  250 ++++
 .../specs/spec/jmap/securityconsiderations.mdown   |   80 ++
 .../doc/specs/spec/jmap/session.mdown              |  173 +++
 .../doc/specs/spec/mail/ianaconsiderations.mdown   |  421 +++++++
 .../doc/specs/spec/mail/identity.mdown             |   85 ++
 .../jmap-rfc-8621/doc/specs/spec/mail/intro.mdown  |  126 ++
 .../doc/specs/spec/mail/mailbox.mdown              |  290 +++++
 .../doc/specs/spec/mail/message.mdown              | 1315 ++++++++++++++++++++
 .../doc/specs/spec/mail/messagesubmission.mdown    |  300 +++++
 .../doc/specs/spec/mail/searchsnippet.mdown        |   94 ++
 .../specs/spec/mail/securityconsiderations.mdown   |   96 ++
 .../jmap-rfc-8621/doc/specs/spec/mail/thread.mdown |   56 +
 .../doc/specs/spec/mail/vacationresponse.mdown     |   34 +
 22 files changed, 5205 insertions(+)

diff --git a/server/protocols/jmap-rfc-8621/doc/README.md b/server/protocols/jmap-rfc-8621/doc/README.md
new file mode 100644
index 0000000..32ac876
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/README.md
@@ -0,0 +1,11 @@
+Annotated JMAP documentation
+============================
+
+This directory contains annotated JMAP documentation as found on https://jmap.io/.
+
+Officially finalized specifications so far regarding JMAP are:
+
+* [The core protocol](https://jmap.io/spec-core.html) [[RFC 8620](https://tools.ietf.org/html/rfc8620)]
+* [JMAP Mail](https://jmap.io/spec-mail.html) [[RFC 8621](https://tools.ietf.org/html/rfc8621)]
+
+Annotations aim at tracking implementation progress in James project.
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/LICENSE.md b/server/protocols/jmap-rfc-8621/doc/specs/LICENSE.md
new file mode 100644
index 0000000..ed61ec4
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/LICENSE.md
@@ -0,0 +1,190 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   Copyright 2016 Fastmail Pty Ltd
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/README.md b/server/protocols/jmap-rfc-8621/doc/specs/README.md
new file mode 100644
index 0000000..a29f47b
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/README.md
@@ -0,0 +1,28 @@
+# JSON Meta Application Protocol (JMAP) Specification
+
+This repo contains the specifications for JMAP. 
+
+JMAP is a modern standard for email clients to connect to mail stores. It therefore primarily replaces IMAP + SMTP submission. It does not replace MTA-to-MTA SMTP transmission. JMAP was built by the community, and continues to improve via the [IETF standardization process](https://datatracker.ietf.org/wg/jmap/about/). Upcoming work includes adding contacts and calendars (replacing CardDAV/CalDAV).
+
+The pretty HTML version of the spec along with more JMAP info and resources can be found at http://jmap.io.
+
+## Get involved
+
+JMAP is being developed within the IETF by the [JMAP Working Group](https://datatracker.ietf.org/wg/jmap/about/).
+
+You can join the mailing list at https://www.ietf.org/mailman/listinfo/jmap. Feedback is welcome: send your thoughts or comments on anything that is imprecise, incomplete, or could simply be done better in another way. Discussion is preferred prior to pull requests, except in the case of minor typos etc.
+
+## Contributions
+
+This repository relates to activities in the Internet Engineering Task
+Force (IETF). All material in this repository is considered Contributions
+to the IETF Standards Process, as defined in the intellectual property
+policies of IETF currently designated as [BCP 78](https://www.rfc-editor.org/info/bcp78), [BCP 79](https://www.rfc-editor.org/info/bcp79) and the [IETF Trust Legal Provisions (TLP) Relating to IETF Documents](http://trustee.ietf.org/trust-legal-provisions.html).
+
+Any edit, commit, pull request, issue, comment or other change made to this
+repository constitutes Contributions to the [IETF Standards Process](https://www.ietf.org/).
+
+You agree to comply with all applicable IETF policies and procedures,
+including, BCP 78, 79, the TLP, and the TLP rules regarding code
+components (e.g. being subject to a Simplified BSD License) in
+Contributions.
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/rfc/src/jmap.mdown b/server/protocols/jmap-rfc-8621/doc/specs/rfc/src/jmap.mdown
new file mode 100644
index 0000000..41196c5
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/rfc/src/jmap.mdown
@@ -0,0 +1,55 @@
+%%%
+    title = "The JSON Meta Application Protocol"
+    abbrev = "JMAP"
+    category = "std"
+    docName = "draft-ietf-jmap-core-17"
+    ipr= "trust200902"
+    area = "Applications"
+    workgroup = "JMAP"
+    keyword = ["JMAP", "JSON"]
+
+    date = 2019-03-18T00:00:00Z
+
+    [[author]]
+    initials="N.M."
+    surname="Jenkins"
+    fullname="Neil Jenkins"
+    role="editor"
+    organization = "Fastmail"
+        [author.address]
+        email = "neilj@fastmailteam.com"
+        uri = "https://www.fastmail.com"
+        [author.address.postal]
+            street = "PO Box 234, Collins St West"
+            city = "Melbourne"
+            code = "VIC 8007"
+            country = "Australia"
+
+    [[author]]
+    initials="C."
+    surname="Newman"
+    fullname="Chris Newman"
+    role="editor"
+    organization = "Oracle"
+        [author.address]
+        email = "chris.newman@oracle.com"
+        [author.address.postal]
+            street = "440 E. Huntington Dr., Suite 400"
+            city = "Arcadia"
+            code = "CA 91006"
+            country = "United States of America"
+%%%
+
+.# Abstract
+
+This document specifies a protocol for clients to efficiently query, fetch, and modify JSON-based data objects, with support for push notification of changes and fast resynchronisation and for out-of-band binary data upload/download.
+
+{mainmatter}
+
+{{spec/jmap/intro.mdown}}
+{{spec/jmap/session.mdown}}
+{{spec/jmap/api.mdown}}
+{{spec/jmap/binary.mdown}}
+{{spec/jmap/push.mdown}}
+{{spec/jmap/securityconsiderations.mdown}}
+{{spec/jmap/ianaconsiderations.mdown}}
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/rfc/src/mail.mdown b/server/protocols/jmap-rfc-8621/doc/specs/rfc/src/mail.mdown
new file mode 100644
index 0000000..69f4e25
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/rfc/src/mail.mdown
@@ -0,0 +1,59 @@
+%%%
+    title = "JMAP (JSON Meta Application Protocol) for Mail"
+    abbrev = "JMAP Mail"
+    category = "std"
+    docName = "draft-ietf-jmap-mail-16"
+    updates = [5788]
+    ipr= "trust200902"
+    area = "Applications"
+    workgroup = "JMAP"
+    keyword = ["JMAP", "JSON", "email"]
+
+    date = 2019-03-08T00:00:00Z
+
+    [[author]]
+    initials="N.M."
+    surname="Jenkins"
+    fullname="Neil Jenkins"
+    role="editor"
+    organization = "Fastmail"
+        [author.address]
+        email = "neilj@fastmailteam.com"
+        uri = "https://www.fastmail.com"
+        [author.address.postal]
+            street = "PO Box 234, Collins St West"
+            city = "Melbourne"
+            code = "VIC 8007"
+            country = "Australia"
+
+    [[author]]
+    initials="C."
+    surname="Newman"
+    fullname="Chris Newman"
+    role="editor"
+    organization = "Oracle"
+        [author.address]
+        email = "chris.newman@oracle.com"
+        [author.address.postal]
+            street = "440 E. Huntington Dr., Suite 400"
+            city = "Arcadia"
+            code = "CA 91006"
+            country = "United States of America"
+%%%
+
+.# Abstract
+
+This document specifies a data model for synchronising email data with a server using JMAP (the JSON Meta Application Protocol). Clients can use this to efficiently search, access, organise and send messages, and get pushed notifications for fast resynchronisation when new messages are delivered or a change is made in another client.
+
+{mainmatter}
+
+{{spec/mail/intro.mdown}}
+{{spec/mail/mailbox.mdown}}
+{{spec/mail/thread.mdown}}
+{{spec/mail/message.mdown}}
+{{spec/mail/searchsnippet.mdown}}
+{{spec/mail/identity.mdown}}
+{{spec/mail/messagesubmission.mdown}}
+{{spec/mail/vacationresponse.mdown}}
+{{spec/mail/securityconsiderations.mdown}}
+{{spec/mail/ianaconsiderations.mdown}}
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/api.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/api.mdown
new file mode 100644
index 0000000..4a69f90
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/api.mdown
@@ -0,0 +1,1135 @@
+# Structured Data Exchange
+
+The client may make an API request to the server to get or set structured data. This request consists of an ordered series of method calls. These are processed by the server, which then returns an ordered series of responses.
+
+## Making an API Request
+
+To make an API request, the client makes an authenticated POST request to the API resource, which is defined by the *apiUrl* property in the Session object (see Section 2).
+
+The request MUST be of type `application/json` and consist of a single JSON-encoded **Request** object, as defined in Section 3.3. If successful, the response MUST also be of type `application/json` and consist of a single **Response** object, as defined in Section 3.4.
+
+## The Invocation Data Type
+
+Method calls and responses are represented by the **Invocation** data type. This is a tuple, represented as a JSON array containing three elements:
+
+1. A `String` **name** of the method to call or of the response.
+2. A `String[*]` object containing *named* **arguments** for that method or
+   response.
+3. A `String` **method call id**: an arbitrary string from the client to be
+   echoed back with the responses emitted by that method call (a method may return 1 or more responses, as it may make implicit calls to other methods; all responses initiated by this method call get the same method call id in the response).
+
+## The Request Object
+
+A **Request** object has the following properties:
+
+- **using**: `String[]`
+  The set of capabilities the client wishes to use. The client MAY include capability identifiers even if the method calls it makes do not utilise those capabilities.
+
+    The server advertises the set of specifications it supports in the Session object (see Section 2), as keys on the *capabilities* property.
+
+- **methodCalls**: `Invocation[]`
+  An array of method calls to process on the server. The method calls MUST be processed sequentially, in order.
+
+- **createdIds**: `Id[Id]` (optional)
+  A map of a (client-specified) creation id to the id the server assigned when a record was successfully created.
+
+    As described later in this specification, some records may have a property that contains the id of another record. To allow more efficient network usage, you can set this property to reference a record created earlier in the same API request. Since the real id is unknown when the request is created, the client can instead specify the creation id it assigned, prefixed with a `#` (see Section 5.3 for more details).
+
+    As the server processes API requests, any time it successfully creates a new record, it adds the creation id to this map (see the *create* argument to "/set" in Section 5.3), with the server-assigned real id as the value. If it comes across a reference to a creation id in a create/update, it looks it up in the map and replaces the reference with the real id, if found.
+
+    The client can pass an initial value for this map as the *createdIds* property of the Request object. This may be an empty object. If given in the request, the response will also include a createdIds property. This allows proxy servers to easily split a JMAP request into multiple JMAP requests to send to different servers. For example, it could send the first two method calls to server A, then the third to server B, before sending the fourth to server A again. By passing the createdI [...]
+
+Future specifications MAY add further properties to the Request object to extend the semantics. To ensure forwards compatibility, a server MUST ignore any other properties it does not understand on the JMAP Request object.
+
+### Example Request
+
+    {
+      "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+      "methodCalls": [
+        [ "method1", {
+          "arg1": "arg1data",
+          "arg2": "arg2data"
+        }, "c1" ],
+        [ "method2", {
+          "arg1": "arg1data"
+        }, "c2" ],
+        [ "method3", {}, "c3" ]
+      ]
+    }
+
+## The Response Object
+
+A **Response** object has the following properties:
+
+- **methodResponses**: `Invocation[]`
+  An array of responses, in the same format as the *methodCalls* on the Request object. The output of the methods MUST be added to the *methodResponses* array in the same order that the methods are processed.
+- **createdIds**: `Id[Id]` (optional; only returned if given in the request)
+  A map of a (client-specified) creation id to the id the server assigned when a
+  record was successfully created. This MUST include all creation ids passed in
+  the original createdIds parameter of the Request object, as well as any
+  additional ones added for newly created records.
+- **sessionState**: `String` The current value of the "state" string on the
+  Session object, as described in Section 2. Clients may use this to detect if this object has changed and needs to be refetched.
+
+Unless otherwise specified, if the method call completed successfully, its response name is the same as the method name in the request.
+
+### Example Response:
+
+    {
+      "methodResponses": [
+        [ "method1", {
+          "arg1": 3,
+          "arg2": "foo"
+        }, "c1" ],
+        [ "method2", {
+          "isBlah": true
+        }, "c2" ],
+        [ "anotherResponseFromMethod2", {
+          "data": 10,
+          "yetmoredata": "Hello"
+        }, "c2"],
+        [ "error", {
+          "type":"unknownMethod"
+        }, "c3" ]
+      ],
+      "sessionState": "75128aab4b1b"
+    }
+
+## Omitting Arguments
+
+An argument to a method may be specified to have a default value. If omitted by the client, the server MUST treat the method call the same as if the default value had been specified. Similarly, the server MAY omit any argument in a response that has the default value.
+
+Unless otherwise specified in a method description, `null` is the default value for any argument in a request or response where this is allowed by the type signature. Other arguments may only be omitted if an explicit default value is defined in the method description.
+
+## Errors
+
+There are three different levels of granularity at which an error may be returned in JMAP.
+
+When an API request is made, the request as a whole may be rejected due to rate limiting, malformed JSON, request for an unknown capability, etc. In this case, the entire request is rejected with an appropriate HTTP error response code and an additional JSON body with more detail for the client.
+
+Provided the request itself is syntactically valid (the JSON is valid and when decoded, it matches the type signature of a Request object), the methods within it are executed sequentially by the server. Each method may individually fail, for example, if invalid arguments are given or an unknown method name is called.
+
+Finally, methods that make changes to the server state often act upon a number of different records within a single call. Each record change may be separately rejected with a SetError, as described in Section 5.3.
+
+### Request-Level Errors
+
+When an HTTP error response is returned to the client, the server
+SHOULD return a JSON "problem details" object as the response body,
+as per [@!RFC7807].
+
+The following problem types are defined:
+
+- `urn:ietf:params:jmap:error:unknownCapability`
+  The client included a capability in the "using" property of the request that the server does not support.
+- `urn:ietf:params:jmap:error:notJSON`
+  The content type of the request was not `application/json` or the request did not parse as I-JSON.
+- `urn:ietf:params:jmap:error:notRequest`
+  The request parsed as JSON but did not match the type signature of the Request object.
+- `urn:ietf:params:jmap:error:limit`
+  The request was not processed as it would have exceeded one of the **request**
+  limits defined on the capability object, such as maxSizeRequest,
+  maxCallsInRequest, or maxConcurrentRequests. A "limit" property MUST also be
+  present on the "problem details" object, containing the name of the limit
+  being applied.
+
+#### Example
+
+    {
+      "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."
+    }
+
+Another example:
+
+    {
+      "type": "urn:ietf:params:jmap:error:limit",
+      "limit": "maxSizeRequest",
+      "status": 400,
+      "detail": "The request is larger than the server is willing to process."
+    }
+
+
+### Method-Level Errors
+
+If a method encounters an error, the appropriate `error` response MUST be inserted at the current point in the *methodResponses* array and, unless otherwise specified, further processing MUST NOT happen within that method call.
+
+Any further method calls in the request MUST then be processed as normal. Errors at the method level MUST NOT generate an HTTP-level error.
+
+An `error` response looks like this:
+
+    [ "error", {
+      "type": "unknownMethod"
+    }, "call-id" ]
+
+The response name is `error`, and it MUST have a type property. Other properties may be present with further information; these are detailed in the error type descriptions where appropriate.
+
+With the exception of when the `serverPartialFail` error is returned, the externally visible state of the server MUST NOT have changed if an error is returned at the method level.
+
+The following error types are defined, which may be returned for any method call where appropriate:
+
+`serverUnavailable`: Some internal server resource was temporarily unavailable. Attempting the same operation later (perhaps after a backoff with a random factor) may succeed.
+
+`serverFail`: An unexpected or unknown error occurred during the processing of the call. A *description* property should provide more details about the error. The method call made no changes to the server's state. Attempting the same operation again is expected to fail again. Contacting the service administrator is likely necessary to resolve this problem if it is persistent.
+
+`serverPartialFail`: Some, but not all, expected changes described by the method occurred. The client MUST resynchronise impacted data to determine server state. Use of this error is strongly discouraged.
+
+`unknownMethod`: The server does not recognise this method name.
+
+`invalidArguments`: One of the arguments is of the wrong type or is otherwise invalid, or a required argument is missing. A `description` property MAY be present to help debug with an explanation of what the problem was. This is a non-localised string, and it is not intended to be shown directly to end users.
+
+`invalidResultReference`: The method used a result reference for one of its arguments (see Section 3.7), but this failed to resolve.
+
+`forbidden`: The method and arguments are valid, but executing the method would violate an Access Control List (ACL) or other permissions policy.
+
+`accountNotFound`: The *accountId* does not correspond to a valid account.
+
+`accountNotSupportedByMethod`: The *accountId* given corresponds to a valid account, but the account does not support this method or data type.
+
+`accountReadOnly`: This method modifies state, but the account is read-only (as returned on the corresponding Account object in the JMAP Session resource).
+
+Further possible errors for a particular method are specified in the method descriptions.
+
+Further general errors MAY be defined in future RFCs. Should a client receive an error type it does not understand, it MUST treat it the same as the `serverFail` type.
+
+## References to Previous Method Results
+
+To allow clients to make more efficient use of the network and avoid round
+trips, an argument to one method can be taken from the result of a previous
+method call in the same request.
+
+To do this, the client prefixes the argument name with `#` (an octothorpe). The value is a *ResultReference* object as described below. When processing a method call, the server MUST first check the arguments object for any names beginning with `#`. If found, the result reference should be resolved and the value used as the "real" argument. The method is then processed as normal. If any result reference fails to resolve, the whole method MUST be rejected with an `invalidResultReference`  [...]
+
+A **ResultReference** object has the following properties:
+
+- **resultOf**: `String`
+  The method call id (see Section 3.1.1) of a previous method call in the current request.
+- **name**: `String`
+  The required name of a response to that method call.
+- **path**: `String`
+  A pointer into the arguments of the response selected via the name and resultOf properties. This is a JSON Pointer [@!RFC6901], except it also allows the use of `*` to map through an array (see the description below).
+
+To resolve:
+
+1. Find the first response with a method call id identical to the *resultOf*
+   property of the *ResultReference* in the *methodResponses* array from previously processed method calls in the same request. If none, evaluation fails.
+
+2. If the response name is not identical to the *name* property of the
+   *ResultReference*, evaluation fails.
+
+3. Apply the *path* to the arguments object of the response (the second item in
+   the response array) following the JSON Pointer algorithm [@!RFC6901], except with the following addition in "Evaluation" (see Section 4):
+
+     If the currently referenced value is a JSON array, the reference token may
+     be exactly the single character `*`, making the new referenced value the result of applying the rest of the JSON Pointer tokens to every item in the array and returning the results in the same order in a new array.
+
+     If the result of applying the rest of the pointer tokens to each item was itself an array, the contents of this array are added to the output rather than the array itself (i.e., the result is flattened from an array of arrays to a single array).
+
+As a simple example, suppose we have the following API request *methodCalls*:
+
+    [[ "Foo/changes", {
+        "accountId": "A1",
+        "sinceState": "abcdef"
+    }, "t0" ],
+    [ "Foo/get", {
+        "accountId": "A1",
+        "#ids": {
+            "resultOf": "t0",
+            "name": "Foo/changes",
+            "path": "/created"
+        }
+    }, "t1" ]]
+
+After executing the first method call, the *methodResponses* array is:
+
+    [[ "Foo/changes", {
+        "accountId": "A1",
+        "oldState": "abcdef",
+        "newState": "123456",
+        "hasMoreChanges": false,
+        "created": [ "f1", "f4" ],
+        "updated": [],
+        "destroyed": []
+    }, "t0" ]]
+
+To execute the Foo/get call, we look through the arguments and find there is one with a `#` prefix. To resolve this, we apply the algorithm above:
+
+1. Find the first response with method call id "t0". The Foo/changes response
+   fulfils this criterion.
+2. Check that the response name is the same as in the result reference. It is,
+   so this is fine.
+3. Apply the *path* as a JSON Pointer to the arguments object. This simply
+   selects the "created" property, so the result of evaluating is:
+   `[ "f1", "f4" ]`.
+
+The JMAP server now continues to process the Foo/get call as though the arguments were:
+
+    {
+        "accountId": "A1",
+        "ids": [ "f1", "f4" ]
+    }
+
+Now, a more complicated example using the JMAP Mail data model: fetch the "from"/"date"/"subject" for every Email in the first 10 Threads in the inbox (sorted newest first):
+
+    [[ "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" ]]
+
+After executing the first 3 method calls, the *methodResponses* array might be:
+
+    [[ "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" ]]
+
+To execute the final Email/get call, we look through the arguments and find there is one with a `#` prefix. To resolve this, we apply the algorithm:
+
+1. Find the first response with method call id "t2". The "Thread/get" response
+   fulfils this criterion.
+2. "Thread/get" is the name specified in the result reference, so this is fine.
+3. Apply the *path* as a JSON Pointer to the arguments object. Token by token:
+   1) `list`: get the array of thread objects
+   2) `*`: for each of the items in the array:
+      a) `emailIds`: get the array of Email ids
+      b) Concatenate these into a single array of all the ids in the result.
+
+The JMAP server now continues to process the Email/get call as though the arguments were:
+
+    {
+        "accountId": "A1",
+        "ids": [ "msg1020", "msg1021", "msg1023", "msg201", "msg223", ... ],
+        "properties": [ "from", "receivedAt", "subject" ]
+    }
+
+The ResultReference performs a similar role to that of the creation id, in
+that it allows a chained method call to refer to information not available when
+the request is generated.  However, they are different things and not
+interchangeable; the only commonality is the octothorpe used to indicate
+them.
+
+## Localisation of User-Visible Strings
+
+If returning a custom string to be displayed to the user, for example, an error message, the server SHOULD use information from the Accept-Language header of the request (as defined in Section 5.3.5 of [@!RFC7231]) to choose the best available localisation. The Content-Language header of the response (see section 3.1.3.2 of [@!RFC7231]) SHOULD indicate the language being used for user-visible strings.
+
+For example, suppose a request was made with the following header:
+
+    Accept-Language: fr-CH, fr;q=0.9, de;q=0.8, en;q=0.7, *;q=0.5
+
+and a method generated an error to display to the user. The server has translations of the error message in English and German. Looking at the Accept-Language header, the user's preferred language is French. Since we don't have a translation for this, we look at the next most preferred, which is German. We have a German translation, so the server returns this and indicates the language chosen in a Content-Language header like so:
+
+    Content-Language: de
+
+## Security
+
+As always, the server must be strict about data received from the client. Arguments need to be checked for validity; a malicious user could attempt to find an exploit through the API. In case of invalid arguments (unknown/insufficient/wrong type for data, etc.), the method MUST return an `invalidArguments` error and terminate.
+
+## Concurrency
+
+Method calls within a single request MUST be executed in order. However, method calls from different concurrent API requests may be interleaved. This means that the data on the server may change between two method calls within a single API request.
+
+
+# The Core/echo Method
+
+The *Core/echo* method returns exactly the same arguments as it is given. It is useful for testing if you have a valid authenticated connection to a JMAP API endpoint.
+
+## Example
+
+Request:
+
+    [[ "Core/echo", {
+      "hello": true,
+      "high": 5
+    }, "b3ff" ]]
+
+Response:
+
+    [[ "Core/echo", {
+      "hello": true,
+      "high": 5
+    }, "b3ff" ]]
+
+
+# Standard Methods and Naming Convention
+
+JMAP provides a uniform interface for creating, retrieving, updating, and deleting objects of a particular type. For a `Foo` data type, records of that type would be fetched via a `Foo/get` call and modified via a `Foo/set` call. Delta updates may be fetched via a `Foo/changes` call. These methods all follow a standard format as described below.
+
+Some types may not have all these methods. Specifications defining types MUST specify which methods are available for the type.
+
+## /get
+
+Objects of type **Foo** are fetched via a call to *Foo/get*.
+
+It takes the following arguments:
+
+- **accountId**: `Id`
+  The id of the account to use.
+- **ids**: `Id[]|null`
+  The ids of the Foo objects to return. If `null`, then **all** records of the data type are returned, if this is supported for that data type and the number of records does not exceed the *maxObjectsInGet* limit.
+- **properties**: `String[]|null`
+  If supplied, only the properties listed in the array are returned for each Foo object. If `null`, all properties of the object are returned. The id property of the object is **always** returned, even if not explicitly requested. If an invalid property is requested, the call MUST be rejected with an `invalidArguments` error.
+
+The response has the following arguments:
+
+- **accountId**: `Id`
+  The id of the account used for the call.
+- **state**: `String`
+  A (preferably short) string representing the state on the server for **all** the data of this type in the account (not just the objects returned in this call). If the data changes, this string MUST change. If the Foo data is unchanged, servers SHOULD return the same state string on subsequent requests for this data type.
+
+    When a client receives a response with a different state string to a previous call, it MUST either throw away all currently cached objects for the type or call *Foo/changes* to get the exact changes.
+
+- **list**: `Foo[]`
+  An array of the Foo objects requested. This is the **empty array** if no objects were found or if the *ids* argument passed in was also an empty array. The results MAY be in a different order to the *ids* in the request arguments. If an identical id is included more than once in the request, the server MUST only include it once in either the *list* or the *notFound* argument of the response.
+- **notFound**: `Id[]`
+  This array contains the ids passed to the method for records that do not exist. The array is empty if all requested ids were found or if the *ids* argument passed in was either `null` or an empty array.
+
+The following additional error may be returned instead of the *Foo/get* response:
+
+`requestTooLarge`: The number of *ids* requested by the client exceeds the maximum number the server is willing to process in a single method call.
+
+## /changes
+
+When the state of the set of Foo records in an account changes on the server (whether due to creation, updates, or deletion), the *state* property of the *Foo/get* response will change. The *Foo/changes* method allows a client to efficiently update the state of its Foo cache to match the new state on the server. It takes the following arguments:
+
+- **accountId**: `Id`
+  The id of the account to use.
+- **sinceState**: `String`
+  The current state of the client. This is the string that was returned as the *state* argument in the *Foo/get* response. The server will return the changes that have occurred since this state.
+- **maxChanges**: `UnsignedInt|null`
+  The maximum number of ids to return in the response. The server MAY choose to return fewer than this value but MUST NOT return more. If not given by the client, the server may choose how many to return. If supplied by the client, the value MUST be a positive integer greater than 0. If a value outside of this range is given, the server MUST reject the call with an `invalidArguments` error.
+
+The response has the following arguments:
+
+- **accountId**: `Id`
+  The id of the account used for the call.
+- **oldState**: `String`
+  This is the *sinceState* argument echoed back; it's the state from which the server is returning changes.
+- **newState**: `String`
+  This is the state the client will be in after applying the set of changes to the old state.
+- **hasMoreChanges**: `Boolean`
+  If `true`, the client may call *Foo/changes* again with the *newState* returned to get further updates. If `false`, *newState* is the current server state.
+- **created**: `Id[]`
+  An array of ids for records that have been created since the old state.
+- **updated**: `Id[]`
+  An array of ids for records that have been updated since the old state.
+- **destroyed**: `Id[]`
+  An array of ids for records that have been destroyed since the old state.
+
+If a record has been created AND updated since the old state, the server SHOULD just return the id in the *created* list but MAY return it in the *updated* list as well.
+
+If a record has been updated AND destroyed since the old state, the server SHOULD just return the id in the *destroyed* list but MAY return it in the *updated* list as well.
+
+If a record has been created AND destroyed since the old state, the server SHOULD remove the id from the response entirely. However, it MAY include it in just the *destroyed* list or in both the *destroyed* and *created* lists.
+
+If a *maxChanges* is supplied, or set automatically by the server, the server MUST ensure the number of ids returned across *created*, *updated*, and *destroyed* does not exceed this limit. If there are more changes than this between the client's state and the current server state, the server SHOULD generate an update to take the client to an intermediate state, from which the client can continue to call *Foo/changes* until it is fully up to date. If it is unable to calculate an intermed [...]
+
+When generating intermediate states, the server may choose how to divide up the changes. For many types, it will provide a better user experience to return the more recent changes first, as this is more likely to be what the user is most interested in. The client can then continue to page in the older changes while the user is viewing the newer data. For example, suppose a server went through the following states:
+
+    A -> B -> C -> D -> E
+
+And a client asks for changes from state `B`. The server might first get the ids of records created, updated, or destroyed between states D and E, returning them with:
+
+    state: "B-D-E"
+    hasMoreChanges: true
+
+The client will then ask for the change from state `B-D-E`, and the server can return the changes between states C and D, returning:
+
+    state: "B-C-E"
+    hasMoreChanges: true
+
+Finally, the client will request the changes from `B-C-E` and the server can return the changes between states B and C, returning:
+
+    state: "E"
+    hasMoreChanges: false
+
+Should the state on the server be modified in the middle of all this (to `F`), the server still does the same, but now when the update to state `E` is returned, it would indicate that it still has more changes for the client to fetch.
+
+Where multiple changes to a record are split across different intermediate states, the server MUST NOT return a record as created after a response that deems it as updated or destroyed, and it MUST NOT return a record as destroyed before a response that deems it as created or updated. The server may have to coalesce multiple changes to a record to satisfy this requirement.
+
+The following additional errors may be returned instead of the *Foo/changes* response:
+
+`cannotCalculateChanges`: The server cannot calculate the changes from the state string given by the client. Usually, this is due to the client's state being too old or the server being unable to produce an update to an intermediate state when there are too many updates. The client MUST invalidate its Foo cache.
+
+Maintaining state to allow calculation of *Foo/changes* can be expensive for the server, but always returning *cannotCalculateChanges* severely increases network traffic and resource usage for the client. To allow efficient sync, servers SHOULD be able to calculate changes from any state string that was given to a client within the last 30 days (but of course may support calculating updates from states older than this).
+
+## /set
+
+Modifying the state of Foo objects on the server is done via the *Foo/set* method. This encompasses creating, updating, and destroying Foo records. This allows the server to sort out ordering and dependencies that may exist if doing multiple operations at once (for example, to ensure there is always a minimum number of a certain record type).
+
+The *Foo/set* method takes the following arguments:
+
+- **accountId**: `Id`
+  The id of the account to use.
+- **ifInState**: `String|null`
+  This is a state string as returned by the *Foo/get* method (representing the state of all objects of this type in the account). If supplied, the string must match the current state; otherwise, the method will be aborted and a `stateMismatch` error returned. If `null`, any changes will be applied to the current state.
+- **create**: `Id[Foo]|null`
+  A map of a *creation id* (a temporary id set by the client) to Foo objects,
+  or `null` if no objects are to be created.
+
+    The Foo object type definition may define default values for properties. Any such property may be omitted by the client.
+
+    The client MUST omit any properties that may only be set by the server (for example, the *id* property on most object types).
+
+- **update**: `Id[PatchObject]|null`
+  A map of an id to a Patch object to apply to the current Foo object with that id, or `null` if no objects are to be updated.
+
+    A *PatchObject* is of type `String[*]` and represents an unordered set of patches.  The keys are a path in JSON Pointer Format [@!RFC6901], with an implicit leading "/" (i.e., prefix each key with "/" before applying the JSON Pointer evaluation algorithm).
+
+    All paths MUST also conform to the following restrictions; if there is any violation, the update MUST be rejected with an `invalidPatch` error:
+
+    * The pointer MUST NOT reference inside an array (i.e., you MUST NOT insert/delete from an array; the array MUST be replaced in its entirety instead).
+    * All parts prior to the last (i.e., the value after the final slash) MUST already exist on the object being patched.
+    * There MUST NOT be two patches in the PatchObject where the pointer of one is the prefix of the pointer of the other, e.g., "alerts/1/offset" and "alerts".
+
+    The value associated with each pointer determines how to apply that patch:
+
+    * If `null`, set to the default value if specified for this property; otherwise, remove the property from the patched object. If the key is not present in the parent, this a no-op.
+    * Anything else: The value to set for this property (this may be a replacement or addition to the object being patched).
+
+    Any server-set properties MAY be included in the patch if their value is identical to the current server value (before applying the patches to the object). Otherwise, the update MUST be rejected with an *invalidProperties* SetError.
+
+    This patch definition is designed such that an entire Foo object is also a valid PatchObject. The client may choose to optimise network usage by just sending the diff or may send the whole object; the server processes it the same either way.
+
+- **destroy**: `Id[]|null`
+  A list of ids for Foo objects to permanently delete, or `null` if no objects are to be destroyed.
+
+Each creation, modification, or destruction of an object is considered an atomic unit. It is permissible for the server to commit changes to some objects but not others, however it MUST NOT only commit part of an update to a single record (e.g., update a *name* property but not a *count* property, if both are supplied in the update object).
+
+The final state MUST be valid after the Foo/set is finished; however, the server may have to transition through invalid intermediate states (not exposed to the client) while processing the individual create/update/destroy requests. For example, suppose there is a "name" property that must be unique. A single method call could rename an object A => B and simultaneously rename another object B => A. If the final state is valid, this is allowed. Otherwise, each creation, modification, or de [...]
+
+If a create, update, or destroy is rejected, the appropriate error MUST be added to the notCreated/notUpdated/notDestroyed property of the response, and the server MUST continue to the next create/update/destroy. It does not terminate the method.
+
+If an id given cannot be found, the update or destroy MUST be rejected with a `notFound` set error.
+
+The server MAY skip an update (rejecting it with a `willDestroy` SetError) if that object is destroyed in the same /set request.
+
+Some records may hold references to other records (foreign keys). That reference may be set (via create or update) in the same request as the referenced record is created. To do this, the client refers to the new record using its creation id prefixed with a `#`. The order of the method calls in the request by the client MUST be such that the record being referenced is created in the same or an earlier call. Thus, the server never has to look ahead. Instead, while processing a request, th [...]
+
+Creation ids are not scoped by type but are a single map for all types. A client SHOULD NOT reuse a creation id anywhere in the same API request. If a creation id is reused, the server MUST map the creation id to the most recently created item with that id. To allow easy proxying of API requests, an initial set of creation id to real id values may be passed with a request (see "The Request Object", Section 3.3) and the final state of the map passed out with the response (see "The Respons [...]
+
+The response has the following arguments:
+
+- **accountId**: `Id`
+  The id of the account used for the call.
+- **oldState**: `String|null`
+  The state string that would have been returned by *Foo/get* before making the requested changes, or `null` if the server doesn't know what the previous state string was.
+- **newState**: `String`
+  The state string that will now be returned by *Foo/get*.
+- **created**: `Id[Foo]|null`
+  A map of the creation id to an object containing any properties of the created Foo object that were not sent by the client. This includes all server-set properties (such as the *id* in most object types) and any properties that were omitted by the client and thus set to a default by the server.
+
+    This argument is `null` if no Foo objects were successfully created.
+
+- **updated**: `Id[Foo|null]|null`
+  The *keys* in this map are the ids of all Foos that were successfully updated.
+
+    The *value* for each id is a Foo object containing any property that
+    changed in a way *not* explicitly requested by the *PatchObject* sent to the server, or `null` if none. This lets the client know of any changes to server-set or computed properties.
+
+    This argument is `null` if no Foo objects were successfully updated.
+
+- **destroyed**: `Id[]|null`
+  A list of Foo ids for records that were successfully destroyed, or `null` if none.
+- **notCreated**: `Id[SetError]|null`
+  A map of the creation id to a SetError object for each record that failed to be created, or `null` if all successful.
+- **notUpdated**: `Id[SetError]|null`
+  A map of the Foo id to a SetError object for each record that failed to be updated, or `null` if all successful.
+- **notDestroyed**: `Id[SetError]|null`
+  A map of the Foo id to a SetError object for each record that failed to be destroyed, or `null` if all successful.
+
+A **SetError** object has the following properties:
+
+- **type**: `String`
+  The type of error.
+- **description**: `String|null`
+  A description of the error to help with debugging that includes an explanation of what the problem was. This is a non-localised string and is not intended to be shown directly to end users.
+
+The following SetError types are defined and may be returned for set operations on any record type where appropriate:
+
+- `forbidden`: (create; update; destroy). The create/update/destroy would
+   violate an ACL or other permissions policy.
+- `overQuota`: (create; update). The create would exceed a server-defined limit
+  on the number or total size of objects of this type.
+- `tooLarge`: (create; update). The create/update would result in an object
+  that exceeds a server-defined limit for the maximum size of a single object of this type.
+- `rateLimit`: (create). Too many objects of this type have been created
+  recently, and a server-defined rate limit has been reached. It may work if
+  tried again later.
+- `notFound`: (update; destroy). The id given to update/destroy cannot be found.
+- `invalidPatch`: (update) The PatchObject given to update the record was
+  not a valid patch (see the patch description).
+- `willDestroy` (update).
+  The client requested that an object be both updated and destroyed in the same
+  /set request, and the server has decided to therefore ignore the update.
+- `invalidProperties`: (create; update). The record given is invalid in
+  some way. For example:
+
+    - It contains properties that are invalid according to the type specification of this record type.
+    - It contains a property that may only be set by the server (e.g., "id") and is different to the current value. Note, to allow clients to pass whole objects back, it is not an error to include a server-set property in an update as long as the value is identical to the current value on the server.
+    - There is a reference to another record (foreign key), and the given id does not correspond to a valid record.
+
+    The SetError object SHOULD also have a property called *properties* of type `String[]` that lists **all** the properties that were invalid.
+
+    Individual methods MAY specify more specific errors for certain conditions that would otherwise result in an invalidProperties error. If the condition of one of these is met, it MUST be returned instead of the invalidProperties error.
+
+- `singleton`: (create; destroy). This is a singleton type, so you cannot create
+  another one or destroy the existing one.
+
+Other possible SetError types MAY be given in specific method descriptions. Other properties MAY also be present on the *SetError* object, as described in the relevant methods.
+
+The following additional errors may be returned instead of the *Foo/set* response:
+
+`requestTooLarge`: The total number of objects to create, update, or destroy exceeds the maximum number the server is willing to process in a single method call.
+
+`stateMismatch`: An `ifInState` argument was supplied, and it does not match the current state.
+
+## /copy
+
+The only way to move Foo records **between** two different accounts is to copy them using the *Foo/copy* method; once the copy has succeeded, delete the original. The *onSuccessDestroyOriginal* argument allows you to try to do this in one method call; however, note that the two different actions are not atomic, so it is possible for the copy to succeed but the original not to be destroyed for some reason.
+
+The copy is conceptually in three phases:
+
+1. Reading the current values from the "from" account.
+2. Writing the new copies to the other account.
+3. Destroying the originals in the "from" account, if requested.
+
+Data may change in between phases due to concurrent requests.
+
+The *Foo/copy* method takes the following arguments:
+
+- **fromAccountId**: `Id`
+  The id of the account to copy records from.
+- **ifFromInState**: `String|null`
+  This is a state string as returned by the *Foo/get* method. If supplied, the string must match the current state of the account referenced by the fromAccountId when reading the data to be copied; otherwise, the method will be aborted and a `stateMismatch` error returned. If `null`, the data will be read from the current state.
+- **accountId**: `Id`
+  The id of the account to copy records to. This MUST be different to the `fromAccountId`.
+- **ifInState**: `String|null`
+  This is a state string as returned by the *Foo/get* method. If supplied, the string must match the current state of the account referenced by the accountId; otherwise, the method will be aborted and a `stateMismatch` error returned. If `null`, any changes will be applied to the current state.
+- **create**: `Id[Foo]`
+  A map of the *creation id* to a Foo object. The Foo object MUST contain an id property, which is the id (in the fromAccount) of the record to be copied. When creating the copy, any other properties included are used instead of the current value for that property on the original.
+- **onSuccessDestroyOriginal**: `Boolean` (default: false)
+  If `true`, an attempt will be made to destroy the original records that were successfully copied: after emitting the *Foo/copy* response, but before processing the next method, the server MUST make a single call to *Foo/set* to destroy the original of each successfully copied record; the output of this is added to the responses as normal, to be returned to the client.
+- **destroyFromIfInState**: `String|null`
+  This argument is passed on as the `ifInState` argument to the implicit *Foo/set* call, if made at the end of this request to destroy the originals that were successfully copied.
+
+Each record copy is considered an atomic unit that may succeed or fail individually.
+
+The response has the following arguments:
+
+- **fromAccountId**: `Id`
+  The id of the account records were copied from.
+- **accountId**: `Id`
+  The id of the account records were copied to.
+- **oldState**: `String|null`
+  The state string that would have been returned by *Foo/get* on the account records that were copied to before making the requested changes, or `null` if the server doesn't know what the previous state string was.
+- **newState**: `String`
+  The state string that will now be returned by *Foo/get* on the account records were copied to.
+- **created**: `Id[Foo]|null`
+  A map of the creation id to an object containing any properties of the copied
+  Foo object that are set by the server (such as the *id* in most object types;
+  note, the id is likely to be different to the id of the object in the account
+  it was copied from).
+
+    This argument is `null` if no Foo objects were successfully copied.
+
+- **notCreated**: `Id[SetError]|null`
+  A map of the creation id to a SetError object for each record that failed to be copied, or `null` if none.
+
+The **SetError** may be any of the standard set errors returned for a *create* or *update*. In addition, the following SetError is defined:
+
+`alreadyExists`: The server forbids duplicates, and the record already exists in the target account. An *existingId* property of type `Id` MUST be included on the SetError object with the id of the existing record.
+
+The following additional errors may be returned instead of the *Foo/copy* response:
+
+`fromAccountNotFound`: The *fromAccountId* does not correspond to a valid account.
+
+`fromAccountNotSupportedByMethod`: The *fromAccountId* given corresponds to a valid account, but the account does not support this data type.
+
+`stateMismatch`: An `ifInState` argument was supplied and it does not match the current state, or an `ifFromInState` argument was supplied and it does not match the current state in the from account.
+
+
+## /query
+
+For data sets where the total amount of data is expected to be very small, clients can just fetch the complete set of data and then do any sorting/filtering locally. However, for large data sets (e.g., multi-gigabyte mailboxes), the client needs to be able to search/sort/window the data type on the server.
+
+A query on the set of Foos in an account is made by calling *Foo/query*. This takes a number of arguments to determine which records to include, how they should be sorted, and which part of the result should be returned (the full list may be *very* long). The result is returned as a list of Foo ids.
+
+A call to *Foo/query* takes the following arguments:
+
+- **accountId**: `Id`
+  The id of the account to use.
+- **filter**: `FilterOperator|FilterCondition|null`
+  Determines the set of Foos returned in the results. If `null`, all objects in the account of this type are included in the results.
+
+    A **FilterOperator** object has the following properties:
+
+    - **operator**: `String`
+      This MUST be one of the following strings: "AND" / "OR" / "NOT":
+      - **AND**: All of the conditions must match for the filter to match.
+      - **OR**: At least one of the conditions must match for the filter to match.
+      - **NOT**: None of the conditions must match for the filter to match.
+    - **conditions**: `(FilterOperator|FilterCondition)[]`
+      The conditions to evaluate against each record.
+
+    A **FilterCondition** is an `object` whose allowed properties and semantics depend on the data type and is defined in the */query* method specification for that type. It MUST NOT have an *operator* property.
+
+- **sort**: `Comparator[]|null`
+  Lists the names of properties to compare between two Foo records, and how to compare them, to determine which comes first in the sort. If two Foo records have an identical value for the first comparator, the next comparator will be considered, and so on. If all comparators are the same (this includes the case where an empty array or `null` is given as the *sort* argument), the sort order is server dependent, but it MUST be stable between calls to Foo/query.
+
+    A **Comparator** has the following properties:
+
+    - **property**: `String`
+      The name of the property on the Foo objects to compare.
+    - **isAscending**: `Boolean` (optional; default: true)
+      If `true`, sort in ascending order. If `false`, reverse the comparator's results to sort in descending order.
+    - **collation**: `String` (optional; default is server dependent)
+      The identifier, as registered in the collation registry defined in [@!RFC4790], for the algorithm to use when comparing the order of strings. The algorithms the server supports are advertised in the capabilities object returned with the Session object (see Section 2).
+
+        If omitted, the default algorithm is server-dependent, but:
+
+        1. It MUST be unicode-aware.
+        2. It MAY be selected based on an Accept-Language header in the
+           request (as defined in [@!RFC7231], Section 5.3.5), or out-of-band information about the user's language/locale.
+        3. It SHOULD be case insensitive where such a concept makes sense for a
+           language/locale. Where the user's language is unknown, it is RECOMMENDED to follow the advice in Section 5.2.3 of [@!RFC8264].
+
+        The "i;unicode-casemap" collation [@!RFC5051] and the Unicode Collation Algorithm (http://www.unicode.org/reports/tr10/) are two examples that fulfil these criterion and provide reasonable behaviour for a large number of languages.
+
+        When the property being compared is not a string, the *collation* property is ignored, and the following comparison rules apply based on the type. In ascending order:
+
+        - `Boolean`: `false` comes before `true`.
+        - `Number`: A lower number comes before a higher number.
+        - `Date`/`UTCDate`: The earlier date comes first.
+
+    The Comparator object may also have additional properties as required for
+    specific sort operations defined in a type's /query method.
+
+- **position**: `Int` (default: 0)
+  The zero-based index of the first id in the full list of results to return.
+
+    If a negative value is given, it is an offset from the end of the list. Specifically, the negative value MUST be added to the total number of results given the filter, and if still negative, it's clamped to `0`. This is now the zero-based index of the first id to return.
+
+    If the index is greater than or equal to the total number of objects in the results list, then the *ids* array in the response will be empty, but this is not an error.
+- **anchor**: `Id|null`
+  A Foo id. If supplied, the *position* argument is ignored. The index of this id in the results will be used in combination with the `anchorOffset` argument to determine the index of the first result to return (see below for more details).
+- **anchorOffset**: `Int` (default: 0)
+  The index of the first result to return relative to the index of the anchor, if an anchor is given. This MAY be negative. For example, `-1` means the Foo immediately preceding the anchor is the first result in the list returned (see below for more details).
+- **limit**: `UnsignedInt|null`
+  The maximum number of results to return. If `null`, no limit presumed. The server MAY choose to enforce a maximum `limit` argument. In this case, if a greater value is given (or if it is `null`), the limit is clamped to the maximum; the new limit is returned with the response so the client is aware. If a negative value is given, the call MUST be rejected with an `invalidArguments` error.
+- **calculateTotal**: `Boolean` (default: false)
+  Does the client wish to know the total number of results in the query? This may be slow and expensive for servers to calculate, particularly with complex filters, so clients should take care to only request the total when needed.
+
+If an **anchor** argument is given, the anchor is looked for in the results after filtering and sorting. If found, the *anchorOffset* is then added to its index. If the resulting index is now negative, it is clamped to 0. This index is now used exactly as though it were supplied as the `position` argument. If the anchor is not found, the call is rejected with an `anchorNotFound` error.
+
+If an *anchor* is specified, any position argument supplied by the client MUST be ignored. If no *anchor* is supplied, any *anchorOffset* argument MUST be ignored.
+
+A client can use *anchor* instead of *position* to find the index of an id
+within a large set of results.
+
+The response has the following arguments:
+
+- **accountId**: `Id`
+  The id of the account used for the call.
+- **queryState**: `String`
+  A string encoding the current state of the query on the server. This string MUST change if the results of the query (i.e., the matching ids and their sort order) have changed. The queryState string MAY change if something has changed on the server, which means the results may have changed but the server doesn't know for sure.
+
+    The queryState string only represents the ordered list of ids that match the particular query (including its sort/filter). There is no requirement for it to change if a property on an object matching the query changes but the query results are unaffected (indeed, it is more efficient if the queryState string does not change in this case). The queryState string only has meaning when compared to future responses to a query with the same type/sort/filter or when used with /queryChanges  [...]
+
+    Should a client receive back a response with a different queryState string to a previous call, it MUST either throw away the currently cached query and fetch it again (note, this does not require fetching the records again, just the list of ids) or call *Foo/queryChanges* to get the difference.
+
+- **canCalculateChanges**: `Boolean`
+  This is `true` if the server supports calling *Foo/queryChanges* with these `filter`/`sort` parameters. Note, this does not guarantee that the *Foo/queryChanges* call will succeed, as it may only be possible for a limited time afterwards due to server internal implementation details.
+- **position**: `UnsignedInt`
+  The zero-based index of the first result in the `ids` array within the complete list of query results.
+- **ids**: `Id[]`
+  The list of ids for each Foo in the query results, starting at the index given by the *position* argument of this response and continuing until it hits the end of the results or reaches the `limit` number of ids. If *position* is >= *total*, this MUST be the empty list.
+- **total**: `UnsignedInt` (only if requested)
+  The total number of Foos in the results (given the *filter*). This argument MUST be omitted if the *calculateTotal* request argument is not `true`.
+- **limit**: `UnsignedInt` (if set by the server)
+  The limit enforced by the server on the maximum number of results to return.
+  This is only returned if the server set a limit or used a different limit than
+  that given in the request.
+
+The following additional errors may be returned instead of the *Foo/query* response:
+
+`anchorNotFound`: An anchor argument was supplied, but it cannot be found in the results of the query.
+
+`unsupportedSort`: The *sort* is syntactically valid, but it includes a property the server does not support sorting on or a collation method it does not recognise.
+
+`unsupportedFilter`: The *filter* is syntactically valid, but the server cannot
+process it. If the filter was the result of a user's search input, the client
+SHOULD suggest that the user simplify their search.
+
+## /queryChanges
+
+The `Foo/queryChanges` method allows a client to efficiently update the state of a cached query to match the new state on the server. It takes the following arguments:
+
+- **accountId**: `Id`
+  The id of the account to use.
+- **filter**: `FilterOperator|FilterCondition|null`
+  The filter argument that was used with *Foo/query*.
+- **sort**: `Comparator[]|null`
+  The sort argument that was used with *Foo/query*.
+- **sinceQueryState**: `String`
+  The current state of the query in the client. This is the string that was returned as the *queryState* argument in the *Foo/query* response with the same sort/filter. The server will return the changes made to the query since this state.
+- **maxChanges**: `UnsignedInt|null`
+  The maximum number of changes to return in the response. See error descriptions below for more details.
+- **upToId**: `Id|null`
+  The last (highest-index) id the client currently has cached from the query results. When there are a large number of results, in a common case, the client may have only downloaded and cached a small subset from the beginning of the results. If the sort and filter are both only on immutable properties, this allows the server to omit changes after this point in the results, which can significantly increase efficiency. If they are not immutable, this argument is ignored.
+- **calculateTotal**: `Boolean` (default: false)
+  Does the client wish to know the total number of results now in the query? This may be slow and expensive for servers to calculate, particularly with complex filters, so clients should take care to only request the total when needed.
+
+The response has the following arguments:
+
+- **accountId**: `Id`
+  The id of the account used for the call.
+- **oldQueryState**: `String`
+  This is the `sinceQueryState` argument echoed back; that is, the state from which the server is returning changes.
+- **newQueryState**: `String`
+  This is the state the query will be in after applying the set of changes to the old state.
+- **total**: `UnsignedInt` (only if requested)
+  The total number of Foos in the results (given the *filter*). This argument MUST be omitted if the *calculateTotal* request argument is not `true`.
+- **removed**: `Id[]`
+  The *id* for every Foo that was in the query results in the old state and that is not in the results in the new state.
+
+    If the server cannot calculate this exactly, the server MAY return the ids of extra Foos in addition that may have been in the old results but are not in the new results.
+
+    If the sort and filter are both only on immutable properties and an *upToId* is supplied and exists in the results, any ids that were removed but have a higher index than *upToId* SHOULD be omitted.
+
+    If the *filter* or *sort* includes a mutable property, the server MUST include all Foos in the current results for which this property may have changed. The position of these may have moved in the results, so must be
+    reinserted by the client to ensure its query cache is correct.
+
+- **added**: `AddedItem[]`
+  The id and index in the query results (in the new state) for every Foo that has been added to the results since the old state AND every Foo in the current results that was included in the *removed* array (due to a filter or sort based upon a mutable property).
+
+    If the sort and filter are both only on immutable properties and an *upToId* is supplied and exists in the results, any ids that were added but have a higher index than *upToId* SHOULD be omitted.
+
+    The array MUST be sorted in order of index, with the lowest index first.
+
+    An **AddedItem** object has the following properties:
+
+    - **id**: `Id`
+    - **index**: `UnsignedInt`
+
+The result of this is that if the client has a cached sparse array of Foo ids corresponding to the results in the old state, then:
+
+    fooIds = [ "id1", "id2", null, null, "id3", "id4", null, null, null ]
+
+If it **splices out** all ids in the removed array that it has in its cached results, then:
+
+    removed = [ "id2", "id31", ... ];
+    fooIds => [ "id1", null, null, "id3", "id4", null, null, null ]
+
+and **splices in** (one by one in order, starting with the lowest index) all of the ids in the added array:
+
+    added = [{ id: "id5", index: 0, ... }];
+    fooIds => [ "id5", "id1", null, null, "id3", "id4", null, null, null ]
+
+and **truncates** or **extends** to the new total length, then the results will now be in the new state.
+
+Note: splicing in adds the item at the given index, incrementing the index of all items previously at that or a higher index. Splicing out is the inverse, removing the item and decrementing the index of every item after it in the array.
+
+The following additional errors may be returned instead of the *Foo/queryChanges* response:
+
+`tooManyChanges`: There are more changes than the client's *maxChanges* argument. Each item in the removed or added array is considered to be one change. The client may retry with higher max changes or invalidate its cache of the query results.
+
+`cannotCalculateChanges`: The server cannot calculate the changes from the queryState string given by the client, usually due to the client's state being too old. The client MUST invalidate its cache of the query results.
+
+## Examples
+
+Suppose we have a type *Todo* with the following properties:
+
+- **id**: `Id` (immutable; server-set)
+  The id of the object.
+- **title**: `String`
+  A brief summary of what is to be done.
+- **keywords**: `String[Boolean]` (default: \{\})
+  A set of keywords that apply to the Todo. The set is represented as an object, with the keys being the *keywords*. The value for each key in the object MUST be `true`. (This format allows you to update an individual key using patch syntax rather than having to update the whole set of keywords as one, which a `String[]` representation would require.)
+- **neuralNetworkTimeEstimation**: `Number` (server-set)
+  The title and keywords are fed into the server's state-of-the-art neural
+  network to get an estimation of how long this Todo will take, in seconds.
+- **subTodoIds**: `Id[]|null`
+  The ids of a list of other Todos to complete as part of this Todo.
+
+Suppose also that all the standard methods are defined for this type and the FilterCondition object supports a `hasKeyword` property to match Todos with the given keyword.
+
+A client might want to display the list of Todos with either a "music" keyword or a "video" keyword, so it makes the following method call:
+
+    [[ "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" ]]
+
+This would query the server for the set of Todos with a keyword of either "music" or "video", sorted by title, and limited to the first 10 results. It fetches the full object for each of these Todos using back-references to reference the result of the query. The response might look something like:
+
+    [[ "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" ]]
+
+Now, suppose the user adds a keyword "chopin" and removes the keyword "mozart" from the "Practise Piano" task. The client may send the whole object to the server, as this is a valid 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" ]]
+
+or it may send a minimal patch:
+
+    [[ "Todo/set", {
+      "accountId": "x",
+      "ifInState": "10324",
+      "update": {
+        "a": {
+          "keywords/chopin": true,
+          "keywords/mozart": null
+        }
+      }
+    }, "0" ]]
+
+The effect is exactly the same on the server in either case, and presuming the server is still in state "10324", it will probably return success:
+
+    [[ "Todo/set", {
+      "accountId": "x",
+      "oldState": "10324",
+      "newState": "10329",
+      "updated": {
+        "a": {
+          "neuralNetworkTimeEstimation": 5400
+        }
+      }
+    }, "0" ]]
+
+The server changed the "neuralNetworkTimeEstimation" property on the object as part of this change; as this changed in a way *not* explicitly requested by the PatchObject sent to the server, it is returned with the "updated" confirmation.
+
+Let us now add a sub-Todo to our new "Practise Piano" Todo. In this example, we can see the use of a reference to a creation id to allow us to set a foreign key reference to a record created in the same request:
+
+    [[ "Todo/set", {
+      "accountId": "x",
+      "create": {
+        "k15": {
+          "title": "Warm up with scales"
+        }
+      },
+      "update": {
+        "a": {
+          "subTodoIds": [ "#k15" ]
+        }
+      }
+    }, "0" ]]
+
+Now, suppose another user deleted the "Listen to Daft Punk" Todo. The first user will receive a push notification (see Section 7) with the changed state string for the "Todo" type. Since the new string does not match its current state, it knows it needs to check for updates. It may make a request like:
+
+    [[ "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" ]]
+
+and receive in response:
+
+    [[ "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" ]]
+
+Suppose the user has access to another account `y`, for example, a team account shared between multiple users. To move an existing Todo from account `x`, the client would call:
+
+    [[ "Todo/copy", {
+      "fromAccountId": "x",
+      "accountId": "y",
+      "create": {
+        "k5122": {
+          "id": "a"
+        }
+      },
+      "onSuccessDestroyOriginal": true
+    }, "0" ]]
+
+The server successfully copies the Todo to a new account (where it receives a new id) and deletes the original. Due to the implicit call to "Todo/set", there are two responses to the single method call, both with the same method call 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" ]]
+
+## Proxy Considerations
+
+JMAP has been designed to allow an API endpoint to easily proxy through to one
+or more JMAP servers. This may be useful for load balancing, augmenting capabilities, or presenting a single endpoint to accounts hosted on different JMAP servers (splitting the request based on each method's "accountId" argument). The proxy need only understand the general structure of a JMAP Request object; it does not need to know anything specifically about the methods and arguments it will pass through to other servers.
+
+If splitting up the methods in a request to call them on different backend servers, the proxy must do two things to ensure back-references and creation-id references resolve the same as if the entire request were processed on a single server:
+
+1. It must pass a `createdIds` property with each subrequest. If this is not
+   given by the client, an empty object should be used for the first subrequest. The `createdIds` property of each subresponse should be passed on in the next subrequest.
+
+2. It must resolve back-references to previous method results that were
+   processed on a different server. This is a relatively simple syntactic substitution, described in Section 3.7.
+
+When splitting a request based on accountId, proxy implementors do need to be aware of `/copy` methods that copy between accounts. If the accounts are on different servers, the proxy will have to implement this functionality directly.
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/binary.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/binary.mdown
new file mode 100644
index 0000000..9574f08
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/binary.mdown
@@ -0,0 +1,93 @@
+# Binary Data
+
+Binary data is referenced by a *blobId* in JMAP and uploaded/downloaded separately to the core API. The blobId solely represents the raw bytes of data, not any associated metadata such as a file name or content type. Such metadata is stored alongside the blobId in the object referencing it. The data represented by a blobId is immutable.
+
+Any blobId that exists within an account may be used when creating/updating another object in that account. For example, an Email type may have a blobId that represents the object in Internet Message Format [@!RFC5322]. A client could create a new Email object with an attachment and use this blobId, in effect attaching the old message to the new one. Similarly, it could attach any existing attachment of an old message without having to download and upload it again.
+
+When the client uses a blobId in a create/update, the server MAY assign a new blobId to refer to the same binary data within the new/updated object. If it does so, it MUST return any properties that contain a changed blobId in the created/updated response, so the client gets the new ids.
+
+A blob that is not referenced by a JMAP object (e.g., as a message attachment) MAY be deleted by the server to free up resources. Uploads (see below) are initially unreferenced blobs. To ensure interoperability:
+
+* The server SHOULD use a separate quota for unreferenced blobs to the
+  account's usual quota. In the case of shared accounts, this quota SHOULD be separate per user.
+* This quota SHOULD be at least the maximum total size that a single
+  object can reference on this server. For example, if supporting JMAP Mail, this should be at least the maximum total attachments size for a message.
+* When an upload would take the user over quota, the server MUST delete
+  unreferenced blobs in date order, oldest first, until there is room for the new blob.
+* Except where quota restrictions force early deletion, an unreferenced blob
+  MUST NOT be deleted for at least 1 hour from the time of upload; if reuploaded, the same blobId MAY be returned, but this SHOULD reset the expiry time.
+* A blob MUST NOT be deleted during the method call that removed the last
+  reference, so that a client can issue a create and a destroy that both reference the blob within the same method call.
+
+## Uploading Binary Data
+
+There is a single endpoint that handles all file uploads for an account, regardless of what they are to be used for. The Session object (see Section 2) has an *uploadUrl* property in URI Template (level 1) format [@!RFC6570], which MUST contain a variable called `accountId`. The client may use this template in combination with an *accountId* to get the URL of the file upload resource.
+
+To upload a file, the client submits an authenticated POST request to the file upload resource.
+
+A successful request MUST return a single JSON object with the following properties as the response:
+
+- **accountId**: `Id`
+  The id of the account used for the call.
+- **blobId**: `Id`
+  The id representing the binary data uploaded. The data for this id is immutable. The id *only* refers to the binary data, not any metadata.
+- **type**: `String`
+  The media type of the file (as specified in [@!RFC6838], Section 4.2) as set in the Content-Type header of the upload HTTP request.
+- **size**: `UnsignedInt`
+  The size of the file in octets.
+
+If identical binary content to an existing blob in the account is uploaded, the existing blobId MAY be returned.
+
+Clients should use the blobId returned in a timely manner. Under rare circumstances, the server may have deleted the blob before the client uses it; the client should keep a reference to the local file so it can upload it again in such a situation.
+
+When an HTTP error response is returned to the client, the server SHOULD return a JSON "problem details" object as the response body, as per [@!RFC7807].
+
+As access controls are often determined by the object holding the reference to a blob, unreferenced blobs MUST only be accessible to the uploader, even in shared accounts.
+
+## Downloading Binary Data
+
+The Session object (see Section 2) has a *downloadUrl* property, which is in URI Template (level 1) format [@!RFC6570]. The URL MUST contain variables called `accountId`, `blobId`, `type`, and `name`.
+
+To download a file, the client makes an authenticated GET request to the download URL with the appropriate variables substituted in:
+
+- `accountId`: The id of the account to which the record with the blobId
+   belongs.
+- `blobId`: The blobId representing the data of the file to download.
+- `type`: The type for the server to set in the `Content-Type` header of the
+  response; the blobId only represents the binary data and does not have a content-type innately associated with it.
+- `name`: The name for the file; the server MUST return this as the filename if
+  it sets a `Content-Disposition` header.
+
+As the data for a particular blobId is immutable, and thus the response in the generated download URL is too, implementors are recommended to set long cache times and use the "immutable" Cache-Control extension [@?RFC8246] for successful responses, for example, `Cache-Control: private, immutable, max-age=31536000`.
+
+When an HTTP error response is returned to the client, the server SHOULD return a JSON "problem details" object as the response body, as per [@!RFC7807].
+
+## Blob/copy
+
+Binary data may be copied **between** two different accounts using the *Blob/copy* method rather than having to download and then reupload on the client.
+
+The *Blob/copy* method takes the following arguments:
+
+- **fromAccountId**: `Id`
+  The id of the account to copy blobs from.
+- **accountId**: `Id`
+  The id of the account to copy blobs to.
+- **blobIds**: `Id[]`
+  A list of ids of blobs to copy to the other account.
+
+The response has the following arguments:
+
+- **fromAccountId**: `Id`
+  The id of the account blobs were copied from.
+- **accountId**: `Id`
+  The id of the account blobs were copied to.
+- **copied**: `Id[Id]|null`
+  A map of the blobId in the *fromAccount* to the id for the blob in the account it was copied to, or `null` if none were successfully copied.
+- **notCopied**: `Id[SetError]|null`
+  A map of blobId to a SetError object for each blob that failed to be copied, or `null` if none.
+
+The **SetError** may be any of the standard set errors that may be returned for a *create*, as defined in Section 5.3. In addition, the `notFound` SetError error may be returned if the blobId to be copied cannot be found.
+
+The following additional method-level error may be returned instead of the *Blob/copy* response:
+
+`fromAccountNotFound`: The *fromAccountId* included with the request does not correspond to a valid account.
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/ianaconsiderations.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/ianaconsiderations.mdown
new file mode 100644
index 0000000..c3b3f70
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/ianaconsiderations.mdown
@@ -0,0 +1,189 @@
+# IANA Considerations
+
+## Assignment of jmap Service Name
+
+IANA has assigned the 'jmap' service name in the "Service Name and Transport Protocol Port Number Registry" [@!RFC6335].
+
+Service Name: jmap
+
+Transport Protocol(s): tcp
+
+Assignee: IESG
+
+Contact: IETF Chair
+
+Description: JSON Meta Application Protocol
+
+Reference: [@!RFC8620]
+
+Assignment Notes: This service name was previously assigned under the name *JSON Mail Access Protocol*. This has been de-assigned and re-assigned with the approval of the previous assignee.
+
+## Registration of Well-Known URI suffix for JMAP
+
+IANA has registered the following suffix in the "Well-Known URIs" registry for JMAP, as described in [@!RFC8615]:
+
+URI Suffix: jmap
+
+Change Controller: IETF
+
+Specification Document: [@!RFC8620], Section 2.2.
+
+## Registration of the jmap URN Sub-namespace
+
+IANA has registered the following URN sub-namespace in the "IETF URN Sub-namespace for Registered Protocol Parameter Identifiers" registry within the "Uniform Resource Name (URN) Namespace for IETF Use" registry as described in [@!RFC3553].
+
+Registered Parameter Identifier: jmap
+
+Reference: [@!RFC8620], Section 9.4
+
+IANA Registry Reference: http://www.iana.org/assignments/jmap
+
+## Creation of "JMAP Capabilities" Registry
+
+IANA has created the "JMAP Capabilities" registry as described in Section 2. JMAP capabilities are advertised in the *capabilities* property of the Session object. They are used to extend the functionality of a JMAP server. A capability is referenced by a URI. The JMAP capability URI can be a URN starting with `urn:ietf:params:jmap:` plus a unique suffix that is the index value in the jmap URN sub-namespace. Registration of a JMAP capability with another form of URI has no impact on the  [...]
+
+This registry follows the expert review process unless the "intended use" field is *common* or *placeholder*, in which case registration follows the specification required process.
+
+A JMAP capability registration can have an intended use of *common*, *placeholder*, *limited*, or *obsolete*. IANA will list common-use registrations prominently and separately from those with other intended use values.
+
+The JMAP capability registration procedure is not a formal standards process but rather an administrative procedure intended to allow community comment and sanity checking without excessive time delay.
+
+A *placeholder* registration reserves part of the jmap URN namespace for another purpose but is typically not included in the *capabilities* property of the Session object.
+
+### Preliminary Community Review
+
+Notice of a potential JMAP common-use registration SHOULD be sent to the JMAP mailing list <jm...@ietf.org> for review. This mailing list is appropriate to solicit community feedback on a proposed JMAP capability. Registrations that are not intended for common use MAY be sent to the list for review as well; doing so is entirely OPTIONAL, but is encouraged.
+
+The intent of the public posting to this list is to solicit comments and feedback on the choice of the capability name, the unambiguity of the specification document, and a review of any interoperability or security considerations. The submitter may submit a revised registration proposal or abandon the registration completely at any time.
+
+### Submit Request to IANA
+
+Registration requests can be sent to <ia...@iana.org>.
+
+### Designated Expert Review
+
+For a limited-use registration, the primary concern of the designated expert (DE) is preventing name collisions and encouraging the submitter to document security and privacy considerations; a published specification is not required. For a common-use registration, the DE is expected to confirm that suitable documentation, as described in Section 4.6 of [@!RFC8126], is available. The DE should also verify that the capability does not conflict with work that is active or already published  [...]
+
+Before a period of 30 days has passed, the DE will either approve or deny the registration request and publish a notice of the decision to the JMAP WG mailing list or its successor, as well as inform IANA. A denial notice must be justified by an explanation, and, in the cases where it is possible, concrete suggestions on how the request can be modified so as to become acceptable should be provided.
+
+If the DE does not respond within 30 days, the registrant may request the IESG take action to process the request in a timely manner.
+
+### Change Procedures
+
+Once a JMAP capability has been published by the IANA, the change controller may request a change to its definition. The same procedure that would be appropriate for the original registration request is used to process a change request.
+
+JMAP capability registrations may not be deleted; capabilities that are no longer believed appropriate for use can be declared obsolete by a change to their "intended use" field; such capabilities will be clearly marked in the lists published by the IANA.
+
+Significant changes to a capability's definition should be requested only when there are serious omissions or errors in the published specification. When review is required, a change request may be denied if it renders entities that were valid under the previous definition invalid under the new definition.
+
+The owner of a JMAP capability may pass responsibility to another person or agency by informing the IANA; this can be done without discussion or review.
+
+The IESG may reassign responsibility for a JMAP capability. The most common case of this will be to enable changes to be made to capabilities where the author of the registration has died, moved out of contact, or is otherwise unable to make changes that are important to the community.
+
+### JMAP Capabilities Registry Template:
+
+Capability name: (see capability property in Section 2)
+
+Specification document:
+
+Intended use: (one of common, limited, placeholder, or obsolete)
+
+Change controller: (*IETF* for Standards Track / BCP RFCs)
+
+Security and privacy considerations:
+
+### Initial Registration for JMAP Core
+
+Capability Name: `urn:ietf:params:jmap:core`
+
+Specification document: [@!RFC8620], Section 2
+
+Intended use: common
+
+Change Controller: IETF
+
+Security and privacy considerations: [@!RFC8620], Section 8.
+
+### Registration for JMAP Error Placeholder in JMAP Capabilities Registry
+
+Capability Name: `urn:ietf:params:jmap:error:`
+
+Specification document: [@!RFC8620], Section 9.5
+
+Intended use: placeholder
+
+Change Controller: IETF
+
+Security and privacy considerations: [@!RFC8620], Section 8.
+
+## Creation of "JMAP Error Codes" registry
+
+IANA has created the "JMAP Error Codes" registry. JMAP error codes appear in the "type" member of a JSON problem details object (as described in Section 3.6.1), the "type" member in a JMAP error object (as described in Section 3.6.2), or the "type" member of a JMAP method-specific error object (such as SetError in Section 5.3). When used in a problem details object, the prefix "urn:ietf:params:jmap:error:" is always included; when used in JMAP objects, the prefix is always omitted.
+
+This registry follows the expert review process. Preliminary community review for this registry follows the same procedures as the "JMAP Capabilities" registry but it is optional. The change procedures for this registry are the same as the change procedures for the "JMAP Capabilities" registry.
+
+### Designated Expert Review
+
+The designated expert should review the following aspects of the registration:
+
+1. Verify the error code does not conflict with existing names.
+
+2. Verify the error code follows the syntax limitations (does not require URI encoding).
+
+3. Encourage the submitter to follow the naming convention of previously registered errors.
+
+4. Encourage the submitter to describe client behaviours that are recommended in response to the error code. These may distinguish the error code from other error codes.
+
+5. Encourage the submitter to describe when the server should issue the error as opposed to some other error code.
+
+6. Encourage the submitter to note any security considerations associated with the error, if any (e.g., an error code that might disclose existence of data the authenticated user does not have permission to know about).
+
+Steps 3-6 are meant to promote a higher-quality registry. However, the expert is encouraged to approve any registration that would not actively harm JMAP interoperability to make this a relatively lightweight process.
+
+### JMAP Error Codes Registry Template:
+
+JMAP Error Code:
+
+Intended use: (one of *common*, *limited*, *obsolete*)
+
+Change Controller: (*IETF* for Standards Track / BCP RFCs)
+
+Reference: (Optional. Only required if defined in an RFC.)
+
+Description:
+
+### Initial Contents for the JMAP Error Codes
+
+| JMAP Error Code | Intended Use | Change Controller | Reference | Description |
+| --- | --- | --- | --- | --- |
+| accountNotFound | common | IETF | [@!RFC8620] Section 3.6.2 | The accountId does not correspond to a valid account. |
+| accountNotSupportedByMethod | common | IETF | [@!RFC8620] Section 3.6.2 | The accountId given corresponds to a valid account, but the account does not support this method or data type. |
+| accountReadOnly | common | IETF | [@!RFC8620] Section 3.6.2 | This method modifies state, but the account is read-only (as returned on the corresponding Account in the Session object). |
+| anchorNotFound | common | IETF | [@!RFC8620] Section 5.5 | An anchor argument was supplied, but it cannot be found in the results of the query. |
+| alreadyExists | common | IETF | [@!RFC8620] Section 5.4 | The server forbids duplicates, and the record already exists in the target account. An existingId property of type Id MUST be included on the SetError object with the id of the existing record. |
+| cannotCalculateChanges | common | IETF | [@!RFC8620] sections 5.2 and 5.6 |  The server cannot calculate the changes from the state string given by the client. |
+| forbidden | common | IETF | [@!RFC8620] sections 3.5.2, 5.3, and 7.2.1 | The action would violate an ACL or other permissions policy. |
+| fromAccountNotFound | common | IETF | [@!RFC8620] sections 5.4 and 6.3 | The fromAccountId does not correspond to a valid account. |
+| fromAccountNotSupportedByMethod | common | IETF | [@!RFC8620] Section 5.4 | The fromAccountId given corresponds to a valid account, but the account does not support this data type. |
+| invalidArguments | common | IETF | [@!RFC8620] Section 3.6.2 | One of the arguments is of the wrong type or otherwise invalid, or a required argument is missing. |
+| invalidPatch | common | IETF | [@!RFC8620] Section 5.3 |  The PatchObject given to update the record was not a valid patch. |
+| invalidProperties | common | IETF | [@!RFC8620] Section 5.3 | The record given is invalid. |
+| notFound | common | IETF | [@!RFC8620] Section 5.3 | The id given cannot be found. |
+| notJSON | common | IETF | [@!RFC8620] Section 3.6.1 | The content type of the request was not application/json or the request did not parse as I-JSON. |
+| notRequest | common | IETF | [@!RFC8620] Section 3.6.1 | The request parsed as JSON but did not match the type signature of the Request object. |
+| overQuota | common | IETF | [@!RFC8620] Section 5.3 | The create would exceed a server-defined limit on the number or total size of objects of this type. |
+| rateLimit | common | IETF | [@!RFC8620] Section 5.3 | Too many objects of this type have been created recently, and a server-defined rate limit has been reached. It may work if tried again later. |
+| requestTooLarge | common | IETF | [@!RFC8620] sections 5.1 and 5.3 | The total number of actions exceeds the maximum number the server is willing to process in a single method call. |
+| invalidResultReference | common | IETF | [@!RFC8620] Section 3.6.2 | The method used a result reference for one of its arguments, but this failed to resolve. |
+| serverFail | common | IETF | [@!RFC8620] Section 3.6.2 | An unexpected or unknown error occurred during the processing of the call. The method call made no changes to the server’s state. |
+| serverPartialFail | limited | IETF | [@!RFC8620] Section 3.6.2 | Some, but not all, expected changes described by the method occurred. The client MUST re-synchronise impacted data to determine server state. Use of this error is strongly discouraged. |
+| serverUnavailable | common | IETF | [@!RFC8620] Section 3.6.2 | Some internal server resource was temporarily unavailable. Attempting the same operation later (perhaps after a backoff with a random factor) may succeed. |
+| singleton | common | IETF | [@!RFC8620] Section 5.3 | This is a singleton type, so you cannot create another one or destroy the existing one. |
+| stateMismatch | common | IETF | [@!RFC8620] Section 5.3 | An ifInState argument was supplied, and it does not match the current state. |
+| tooLarge | common | IETF | [@!RFC8620] Section 5.3 | The action would result in an object that exceeds a server-defined limit for the maximum size of a single object of this type. |
+| tooManyChanges | common | IETF | [@!RFC8620] Section 5.6 | There are more changes than the client’s maxChanges argument. |
+| unknownCapability | common | IETF | [@!RFC8620] Section 3.6.1 | The client included a capability in the “using” property of the request that the server does not support. |
+| unknownMethod | common | IETF | [@!RFC8620] Section 3.6.2 | The server does not recognise this method name. |
+| unsupportedFilter | common | IETF | [@!RFC8620] Section 5.5 | The filter is syntactically valid, but the server cannot process it. |
+| unsupportedSort | common | IETF | [@!RFC8620] Section 5.5 |The sort is syntactically valid, but includes a property the server does not support sorting on, or a collation method it does not recognise. |
+| willDestroy | common | IETF | [@!RFC8620] Section 5.3 | The client requested an object be both updated and destroyed in the same /set request, and the server has decided to therefore ignore the update. |
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/intro.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/intro.mdown
new file mode 100644
index 0000000..2ba63a8
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/intro.mdown
@@ -0,0 +1,125 @@
+# Introduction
+
+ The JSON Meta Application Protocol (JMAP) is used for synchronising data, such as mail, calendars, or contacts, between a client and a server. It is optimised for mobile and web environments and aims to provide a consistent interface to different data types.
+
+This specification is for the generic mechanism of data synchronisation. Further specifications define the data models for different data types that may be synchronised via JMAP.
+
+JMAP is designed to make efficient use of limited network resources. Multiple API calls may be batched in a single request to the server, reducing round trips and improving battery life on mobile devices. Push connections remove the need for polling, and an efficient delta update mechanism ensures a minimum amount of data is transferred.
+
+JMAP is designed to be horizontally scalable to a very large number of users. This is facilitated by separate endpoints for users after login, the separation of binary and structured data, and a data model for sharing that does not allow data dependencies between accounts.
+
+## Notational Conventions
+
+The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [@!RFC2119] [@!RFC8174] when, and only when, they appear in all capitals, as shown here.
+
+The underlying format used for this specification is JSON. Consequently, the terms "object" and "array" as well as the four primitive types (strings, numbers, booleans, and null) are to be interpreted as described in Section 1 of [@!RFC8259]. Unless otherwise noted, all the property names and values are case sensitive.
+
+Some examples in this document contain "partial" JSON documents used for illustrative purposes.  In these examples, three periods "..." are used to indicate a portion of the document that has been removed for compactness.
+
+For compatibility with publishing requirements, line breaks have been inserted inside long JSON strings, with the following continuation lines indented. To form the valid JSON example, any line breaks inside a string must be replaced with a space and any other white space after the line break removed.
+
+Unless otherwise specified, examples of API exchanges only show the methodCalls array of the Request object or the methodResponses array of the Response object. For compactness, the rest of the Request/Response object is omitted.
+
+Type signatures are given for all JSON values in this document. The following conventions are used:
+
+* `*` – The type is undefined (the value could be any type, although permitted
+  values may be constrained by the context of this value).
+* `String` – The JSON string type.
+* `Number` – The JSON number type.
+* `Boolean` – The JSON boolean type.
+* `A[B]` – A JSON object where the keys are all of type `A`, and the values
+  are all of type `B`.
+* `A[]` – An array of values of type `A`.
+* `A|B` – The value is either of type `A` or of type `B`.
+
+Other types may also be given, with their representation defined elsewhere in this document.
+
+Object properties may also have a set of attributes defined along with the type
+signature. These have the following meanings:
+
+* `server-set` – Only the server can set the value for this property. The
+  client MUST NOT send this property when creating a new object of this type.
+* `immutable` – The value MUST NOT change after the object is created.
+* `default` – (This is followed by a JSON value). The value that will be used
+  for this property if it is omitted in an argument or when creating a new object of this type.
+
+## The Id Data Type
+
+All record ids are assigned by the server and are immutable.
+
+Where `Id` is given as a data type, it means a `String` of at least 1 and a maximum of 255 octets in size, and it MUST only contain characters from the "URL and Filename Safe" base64 alphabet, as defined in Section 5 of [@!RFC4648], excluding the pad character (`=`). This means the allowed characters are the ASCII alphanumeric characters (`A-Za-z0-9`), hyphen (`-`), and underscore (`_`).
+
+These characters are safe to use in almost any context (e.g., filesystems,
+URIs, and IMAP atoms). For maximum safety, servers SHOULD also follow defensive
+allocation strategies to avoid creating risks where glob completion or data type
+detection may be present (e.g., on filesystems or in spreadsheets). In
+particular, it is wise to avoid:
+
+* Ids starting with a dash
+* Ids starting with digits
+* Ids that contain only digits
+* Ids that differ only by ASCII case (for example, A vs. a)
+* the specific sequence of three characters "NIL" (because this sequence can be
+  confused with the IMAP protocol expression of the null value)
+
+A good solution to these issues is to prefix every id with a single
+alphabetical character.
+
+## The Int and UnsignedInt Data Types
+
+Where `Int` is given as a data type, it means an integer in the range -2^53+1 <= value <= 2^53-1, the safe range for integers stored in a floating-point double, represented as a JSON `Number`.
+
+Where `UnsignedInt` is given as a data type, it means an `Int` where the value MUST be in the range 0 <= value <= 2^53-1.
+
+## The Date and UTCDate Data Types
+
+Where `Date` is given as a type, it means a string in *date-time* format [@!RFC3339]. To ensure a normalised form, the *time-secfrac* MUST always be omitted if zero, and any letters in the string (e.g., "T" and "Z") MUST be uppercase. For example, `"2014-10-30T14:12:00+08:00"`.
+
+Where `UTCDate` is given as a type, it means a `Date` where the *time-offset* component MUST be `Z` (i.e., it must be in UTC time). For example, `"2014-10-30T06:12:00Z"`.
+
+## JSON as the Data Encoding Format
+
+JSON is a text-based data interchange format as specified in [@!RFC8259]. The Internet JSON (I-JSON) format defined in [@!RFC7493] is a strict subset of this, adding restrictions to avoid potentially confusing scenarios (for example, it mandates that an object MUST NOT have two members with the same name).
+
+All data sent from the client to the server or from the server to the client (except binary file upload/download) MUST be valid I-JSON according to the RFC and is therefore case sensitive and encoded in UTF-8 [@!RFC3629].
+
+## Terminology
+
+### User
+
+A user is a person accessing data via JMAP. A user has a set of permissions determining the data that they can see.
+
+### Accounts
+
+An account is a collection of data. A single account may contain an arbitrary set of data types, for example, a collection of mail, contacts, and calendars. Most JMAP methods take a mandatory *accountId* argument that specifies on which account the operations are to take place.
+
+An account is not the same as a user, although it is common for a primary account to directly belong to the user. For example, you may have an account that contains data for a group or business, to which multiple users have access.
+
+A single set of credentials may provide access to multiple accounts, for example, if another user is sharing their work calendar with the authenticated user or if there is a group mailbox for a support-desk inbox.
+
+In the event of a severe internal error, a server may have to reallocate ids or do something else that violates standard JMAP data constraints for an account. In this situation, the data on the server is no longer compatible with cached data the client may have from before. The server MUST treat this as though the account has been deleted and then recreated with a new account id. Clients will then be forced to throw away any data with the old account id and refetch all data from scratch.
+
+### Data Types and Records
+
+JMAP provides a uniform interface for creating, retrieving, updating, and deleting various types of objects. A **data type** is a collection of named, typed properties, just like the schema for a database table. Each instance of a data type is called a **record**.
+
+The id of a record is immutable and assigned by the server. The id MUST be unique among all records of the **same type** within the **same account**. Ids may clash across accounts or for two records of different types within the same account.
+
+## The JMAP API Model
+
+JMAP uses HTTP [@!RFC7230] to expose API, push, upload and download resources. All HTTP requests MUST use the `https://` scheme (HTTP over TLS [@!RFC2818]).
+All HTTP requests MUST be authenticated.
+
+An authenticated client can fetch the user's Session object with details about the data and capabilities the server can provide as shown in Section 2. The client may then exchange data with the server in the following ways:
+
+1. The client may make an API request to the server to get or set structured data. This request consists of an ordered series of method calls. These are processed by the server, which then returns an ordered series of responses. This is described in Sections 3, 4, and 5.
+2. The client may download or upload binary files from/to the server. This is detailed in Section 6.
+3. The client may connect to a push channel on the server, to be notified when data has changed. This is explained in Section 7.
+
+## Vendor-Specific Extensions
+
+Individual services will have custom features they wish to expose over JMAP. This may take the form of extra data types and/or methods not in the spec, extra arguments to JMAP methods, or extra properties on existing data types (which may also appear in arguments to methods that take property names).
+
+The server can advertise custom extensions it supports by including the identifiers in the capabilities object. Identifiers for vendor extensions MUST be a URL belonging to a domain owned by the vendor, to avoid conflict. The URL SHOULD resolve to documentation for the changes the extension makes.
+
+The client MUST opt in to use an extension by passing the appropriate capability identifier in the *using* array of the Request object, as described in Section 3.3. The server MUST only follow the specifications that are opted into and behave as though it does not implement anything else when processing a request. This is to ensure compatibility with clients that don't know about a specific custom extension and for compatibility with future versions of JMAP.
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/push.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/push.mdown
new file mode 100644
index 0000000..4b22bac
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/push.mdown
@@ -0,0 +1,250 @@
+# Push
+
+Push notifications allow clients to efficiently update (almost) instantly to stay in sync with data changes on the server. The general model for push is simple and sends minimal data over the push channel: just enough for the client to know whether it needs to resync. The format allows multiple changes to be coalesced into a single push update and the frequency of pushes to be rate limited by the server. It doesn't matter if some push events are dropped before they reach the client; the  [...]
+
+There are two different mechanisms by which a client can receive push notifications, to allow for the different environments in which a client may exist. An event source resource (see Section 7.3) allows clients that can hold transport connections open to receive push notifications directly from the JMAP server. This is simple and avoids third parties, but it is often not feasible on constrained platforms such as mobile devices. Alternatively, clients can make use of any push service sup [...]
+
+## The StateChange Object
+
+When something changes on the server, the server pushes a **StateChange** object to the client. A **StateChange** object has the following properties:
+
+- **@type**: `String`
+  This MUST be the string "StateChange".
+- **changed**: `Id[TypeState]`
+  A map of an *account id* to an object encoding the state of data types that have changed for that account since the last StateChange object was pushed, for each of the accounts to which the user has access and for which something has changed.
+
+    A **TypeState** object is a map. The keys are the type name `Foo` (e.g., "Mailbox" or "Email"), and the value is the *state* property that would currently be returned by a call to *Foo/get*.
+
+    The client can compare the new state strings with its current values to see whether it has the current data for these types. If not, the changes can then be efficiently fetched in a single standard API request (using the */changes* type methods).
+
+### Example
+
+In this example, the server has amalgamated a few changes together across two different accounts the user has access to, before pushing the following StateChange object to the client:
+
+    {
+      "@type": "StateChange",
+      "changed": {
+        "a3123": {
+          "Email": "d35ecb040aab",
+          "EmailDelivery": "428d565f2440",
+          "CalendarEvent": "87accfac587a"
+        },
+        "a43461d": {
+          "Mailbox": "0af7a512ce70",
+          "CalendarEvent": "7a4297cecd76"
+        }
+      }
+    }
+
+The client can compare the state strings with its current state for the Email, CalendarEvent, etc., object types in the appropriate accounts to see if it needs to fetch changes.
+
+If the client is itself making changes, it may receive a StateChange object while the /set API call is in flight. It can wait until the call completes and then compare if the new state string after the /set is the same as was pushed in the StateChange object; if so, and the old state of the /set response matches the client's previous state, it does not need to waste a request asking for changes it already knows.
+
+## PushSubscription
+
+Clients may create a *PushSubscription* to register a URL with the JMAP server. The JMAP server will then make an HTTP POST request to this URL for each push notification it wishes to send to the client.
+
+As a push subscription causes the JMAP server to make a number of requests to a previously unknown endpoint, it can be used as a vector for launching a denial-of-service attack. To prevent this, when a subscription is created, the JMAP server immediately sends a PushVerification object to that URL (see Section 7.2.2). The JMAP server MUST NOT make any further requests to the URL until the client receives the push and updates the subscription with the correct verification code.
+
+A **PushSubscription** object has the following properties:
+
+- **id**: `Id` (immutable; server-set)
+  The id of the push subscription.
+- **deviceClientId**: `String` (immutable)
+  An id that uniquely identifies the client + device it is running on. The purpose of this is to allow clients to identify which PushSubscription objects they created even if they lose their local state, so they can revoke or update them. This string MUST be different on different devices and be different from apps from other vendors. It SHOULD be easy to regenerate and not depend on persisted state. It is RECOMMENDED to use a secure hash of a string that contains:
+
+    1. A unique identifier associated with the device where the JMAP client is
+       running, normally supplied by the device's operating system.
+    2. A custom vendor/app id, including a domain controlled by the vendor of
+       the JMAP client.
+
+    To protect the privacy of the user, the deviceClientId id MUST NOT contain
+    an unobfuscated device id.
+
+- **url**: `String` (immutable)
+  An absolute URL where the JMAP server will POST the data for the push message.
+  This MUST begin with `https://`.
+- **keys**: `Object|null` (immutable)
+  Client-generated encryption keys. If supplied, the server MUST use them as specified in [@!RFC8291] to encrypt all data sent to the push subscription. The object MUST have the following properties:
+
+    - **p256dh**: `String`
+    The P-256 Elliptic Curve Diffie-Hellman (ECDH) public key as described in [@!RFC8291], encoded in URL-safe base64 representation as defined in [@!RFC4648].
+    - **auth**: `String`
+    The authentication secret as described in [@!RFC8291], encoded in URL-safe base64 representation as defined in [@!RFC4648].
+
+- **verificationCode**: `String|null`
+  This MUST be `null` (or omitted) when the subscription is created. The JMAP server then generates a verification code and sends it in a push message, and the client updates the PushSubscription object with the code; see Section 7.2.2 for details.
+- **expires**: `UTCDate|null`
+  The time this push subscription expires. If specified, the JMAP server MUST NOT make further requests to this resource after this time. It MAY automatically destroy the push subscription at or after this time.
+
+    The server MAY choose to set an expiry if none is given by the client or modify the expiry time given by the client to a shorter duration.
+
+- **types**: `String[]|null`
+  A list of types the client is interested in (using the same names as the keys in the *TypeState* object defined in the previous section). A StateChange notification will only be sent if the data for one of these types changes. Other types are omitted from the TypeState object. If `null`, changes will be pushed for all types.
+
+The POST request MUST have a content type of `application/json` and contain the UTF-8 JSON-encoded object as the body. The request MUST have a `TTL` header and MAY have `Urgency` and/or `Topic` headers, as specified in Section 5 of [@!RFC8030]. The JMAP server is expected to understand and handle HTTP status responses in a reasonable manner. A `429` (Too Many Requests) response MUST cause the JMAP server to reduce the frequency of pushes; the JMAP push structure allows multiple changes t [...]
+
+The JMAP server acts as an application server as defined in [@!RFC8030]. A client MAY use the rest of [@!RFC8030] in combination with its own push service to form a complete end-to-end solution, or MAY rely on alternative mechanisms to ensure the delivery of the pushed data after it leaves the JMAP server.
+
+The push subscription is tied to the credentials used to authenticate the API request that created it. Should these credentials expire or be revoked, the push subscription MUST be destroyed by the JMAP server. Only subscriptions created by these credentials are returned when the client fetches existing subscriptions.
+
+When these credentials have their own expiry (i.e., it is a session with a timeout), the server SHOULD NOT set or bound the expiry time for the push subscription given by the client but MUST expire it when the session expires.
+
+When these credentials are not time bounded (e.g., Basic Authentication [@!RFC7617]), the server SHOULD set an expiry time for the push subscription if none is given and limit the expiry time if set too far in the future. This maximum expiry time MUST be at least 48 hours in the future and SHOULD be at least 7 days in the future. An app running on a mobile device may only be able to refresh the push subscription lifetime when it is in the foreground, so this gives a reasonable time frame [...]
+
+In the case of separate access and refresh credentials, as in Oauth 2.0 [@!RFC6749], the server SHOULD tie the push subscription to the validity of the refresh token rather than the access token and behave according to whether this is time-limited or not.
+
+When a push subscription is destroyed, the server MUST securely erase the URL and encryption keys from memory and storage as soon as possible.
+
+### PushSubscription/get
+
+Standard */get* method as described in Section 5.1, except it does **not** take or return an *accountId* argument, as push subscriptions are not tied to specific accounts. It also does **not** return a *state* argument. The *ids* argument may be `null` to fetch all at once.
+
+The server MUST only return push subscriptions that were created using the same authentication credentials as for this PushSubscription/get request.
+
+As the *url* and *keys* properties may contain data that is private to a particular device, the values for these properties MUST NOT be returned. If the *properties* argument is `null` or omitted, the server MUST default to all properties excluding these two. If one of them is explicitly requested, the method call MUST be rejected with a `forbidden` error.
+
+### PushSubscription/set
+
+Standard */set* method  as described in Section 5.3, except it does **not** take or return an *accountId* argument, as push subscriptions are not tied to specific accounts. It also does **not** take an *ifInState* argument or return *oldState* or *newState* arguments.
+
+The *url* and *keys* properties are immutable; if the client wishes to change these, it must destroy the current push subscription and create a new one.
+
+When a PushSubscription is created, the server MUST immediately push a **PushVerification** object to the URL. It has the following properties:
+
+- **@type**: `String`
+  This MUST be the string "PushVerification".
+- **pushSubscriptionId**: `String`
+  The id of the push subscription that was created.
+- **verificationCode**: `String`
+  The verification code to add to the push subscription. This MUST contain sufficient entropy to avoid the client being able to guess the code via brute force.
+
+The client MUST update the push subscription with the correct verification code
+before the server makes any further requests to the subscription's URL. Attempts to update the subscription with an invalid verification code MUST be rejected by the server with an `invalidProperties` SetError.
+
+The client may update the *expires* property to extend (or, less commonly, shorten) the lifetime of a push subscription. The server MAY modify the proposed new expiry time to enforce server-defined limits. Extending the lifetime does not require the subscription to be verified again.
+
+Clients SHOULD NOT update or destroy a push subscription that they did not create (i.e., has a *deviceClientId* that they do not recognise).
+
+### Example
+
+At `2018-07-06T02:14:29Z`, a client with deviceClientId `a889-ffea-910` fetches the set of push subscriptions currently on the server, making an API request with:
+
+    [[ "PushSubscription/get", {
+      "ids": null
+    }, "0" ]]
+
+Which returns:
+
+    [[ "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" ]]
+
+Since neither of the returned push subscription objects have the client's deviceClientId, it knows it does not have a current push subscription active on the server. So it creates one, sending this request:
+
+    [[ "PushSubscription/set", {
+      "create": {
+        "4f29": {
+          "deviceClientId": "a889-ffea-910",
+          "url": "https://example.com/push/?device=X8980fc&client=12c6d086",
+          "types": null
+        }
+      }
+    }, "0" ]]
+
+The server creates the push subscription but limits the expiry time to 7 days in the future, returning this response:
+
+    [[ "PushSubscription/set", {
+      "created": {
+        "4f29": {
+          "id": "P43dcfa4-1dd4-41ef-9156-2c89b3b19c60",
+          "keys": null,
+          "expires": "2018-07-13T02:14:29Z"
+        }
+      }
+    }, "0" ]]
+
+The server also immediately makes a POST request to `https://example.com/push/?device=X8980fc&client=12c6d086` with the data:
+
+    {
+      "@type": "PushVerification",
+      "pushSubscriptionId": "P43dcfa4-1dd4-41ef-9156-2c89b3b19c60",
+      "verificationCode": "da1f097b11ca17f06424e30bf02bfa67"
+    }
+
+The client receives this and updates the subscription with the verification code (note there is a potential race condition here; the client MUST be able to handle receiving the push while the request creating the subscription is still in progress):
+
+    [[ "PushSubscription/set", {
+      "update": {
+        "P43dcfa4-1dd4-41ef-9156-2c89b3b19c60": {
+          "verificationCode": "da1f097b11ca17f06424e30bf02bfa67"
+        }
+      }
+    }, "0" ]]
+
+The server confirms the update was successful and will now make requests to the registered URL when the state changes.
+
+Two days later, the client updates the subscription to extend its lifetime, sending this request:
+
+    [[ "PushSubscription/set", {
+      "update": {
+        "P43dcfa4-1dd4-41ef-9156-2c89b3b19c60": {
+          "expires": "2018-08-13T00:00:00Z"
+        }
+      }
+    }, "0" ]]
+
+The server extends the expiry time, but only again to its maximum limit of 7 days in the future, returning this response:
+
+    [[ "PushSubscription/set", {
+      "updated": {
+        "P43dcfa4-1dd4-41ef-9156-2c89b3b19c60": {
+          "expires": "2018-07-15T02:22:50Z"
+        }
+      }
+    }, "0" ]]
+
+## Event Source
+
+Clients that can hold transport connections open can connect directly to the JMAP server to receive push notifications via a `text/event-stream` resource, as described in [EventSource](https://www.w3.org/TR/eventsource/). This is a long running HTTP request, where the server can push data to the client by appending data without ending the response.
+
+When a change occurs in the data on the server, it pushes an event called `state` to any connected clients, with the *StateChange* object as the data.
+
+The server SHOULD also send a new event id that encodes the entire server state visible to the user immediately after sending a *state* event. When a new connection is made to the event-source endpoint, a client following the server-sent events specification will send a Last-Event-ID HTTP header field with the last id it saw, which the server can use to work out whether the client has missed some changes. If so, it SHOULD send these changes immediately on connection.
+
+The Session object (see Section 2) has an *eventSourceUrl* property, which is in URI Template (level 1) format [@!RFC6570]. The URL MUST contain variables called `types`, `closeafter`, and `ping`.
+
+To connect to the resource, the client makes an authenticated GET request to the event-source URL with the appropriate variables substituted in:
+
+- `types`: This MUST be either:
+  - A comma-separated list of type names, e.g., `Email,CalendarEvent`. The
+    server MUST only push changes for the types in this list.
+  - The single character: `*`. Changes to all types are pushed.
+- `closeafter`: This MUST be one of the following values:
+  - `state`: The server MUST end the HTTP response after pushing a state event.
+    This can be used by clients in environments where buffering proxies prevent the pushed data from arriving immediately, or indeed at all, when operating in the usual mode.
+  - `no`: The connection is persisted by the server as a standard event-source
+    resource.
+- `ping`: A positive integer value representing a length of time in seconds,
+   e.g., `300`. If non-zero, the server MUST send an event called `ping` whenever this time elapses since the previous event was sent. This MUST NOT set a new event id. If the value is `0`, the server MUST NOT send ping events.
+
+     The server MAY modify a requested ping interval to be subject to a minimum and/or maximum value. For interoperability, servers MUST NOT have a minimum allowed value higher than 30 or a maximum allowed value less than 300.
+
+     The data for the ping event MUST be a JSON object containing an *interval* property, the value (type `UnsignedInt`) being the interval in seconds the server is using to send pings (this may be different to the requested value if the server clamped it to be within a min/max value).
+
+     Clients can monitor for the ping event to help determine when the closeafter mode may be required.
+
+A client MAY hold open multiple connections to the event-source resource, although it SHOULD try to use a single connection for efficiency.
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/securityconsiderations.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/securityconsiderations.mdown
new file mode 100644
index 0000000..75ac4b4
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/securityconsiderations.mdown
@@ -0,0 +1,80 @@
+# Security Considerations
+
+## Transport Confidentiality
+
+To ensure the confidentiality and integrity of data sent and received via JMAP, all requests MUST use TLS 1.2 [@!RFC5246] [@!RFC8446] or later, following the recommendations in [@!RFC7525]. Servers SHOULD support TLS 1.3 [@!RFC8446] or later.
+
+Clients MUST validate TLS certificate chains to protect against man-in-the-middle attacks [@!RFC5280].
+
+## Authentication Scheme
+
+A number of HTTP authentication schemes have been standardised (see https://www.iana.org/assignments/http-authschemes/). Servers should take care to assess the security characteristics of different schemes in relation to their needs when deciding what to implement.
+
+Use of the Basic authentication scheme is NOT RECOMMENDED. Services that choose to use it are strongly recommended to require generation of a unique "app password" via some external mechanism for each client they wish to connect. This allows connections from different devices to be differentiated by the server and access to be individually revoked.
+
+## Service Autodiscovery
+
+Unless secured by something like DNSSEC, autodiscovery of server
+details using SRV DNS records is vulnerable to a DNS poisoning attack, which can lead to the client talking to an attacker's server instead of the real JMAP server. The attacker may then intercept requests to execute man-in-the-middle attacks and, depending on the authentication scheme, steal credentials to generate its own requests.
+
+Clients that do not support SRV lookups are likely to try just using the `/.well-known/jmap` path directly against the domain of the username over HTTPS. Servers SHOULD ensure this path resolves or redirects to the correct JMAP Session resource to allow this to work. If this is not feasible, servers MUST ensure this path cannot be controlled by an attacker, as again it may be used to steal credentials.
+
+## JSON Parsing
+
+The Security Considerations of [@!RFC8259] apply to the use of JSON as the data interchange format.
+
+As for any serialization format, parsers need to thoroughly check the
+syntax of the supplied data. JSON uses opening and closing tags for
+several types and structures, and it is possible that the end of supplied
+data will be reached when scanning for a matching closing tag; this is an
+error condition, and implementations need to stop scanning at the end of the
+supplied data.
+
+JSON also uses a string encoding with some escape sequences to encode
+special characters within a string. Care is needed when processing these
+escape sequences to ensure that they are fully formed before
+the special processing is triggered, with special care taken when the
+escape sequences appear adjacent to other (non-escaped) special characters
+or adjacent to the end of data (as in the previous paragraph).
+
+If parsing JSON into a non-textual structured data format, implementations
+may need to allocate storage to hold JSON string elements.  Since JSON
+does not use explicit string lengths, the risk of denial of service due to
+resource exhaustion is small, but implementations may still wish to place
+limits on the size of allocations they are willing to make in any given
+context, to avoid untrusted data causing excessive memory allocation.
+
+## Denial of Service
+
+A small request may result in a very large response and require considerable
+work on the server if resource limits are not enforced. JMAP provides mechanisms for advertising and enforcing a wide variety of limits for mitigating this threat, including limits on the number of objects fetched in a single method call, number of methods in a single request, number of concurrent requests, etc.
+
+JMAP servers MUST implement sensible limits to mitigate against resource exhaustion attacks.
+
+## Connection to Unknown Push Server
+
+When a push subscription is registered, the application server will make POST requests to the given URL. There are a number of security considerations that MUST be considered when implementing this.
+
+The server MUST ensure the URL is externally resolvable to avoid server-side request forgery, where the server makes a request to a resource on its internal network.
+
+A malicious client may use the push subscription to attempt to flood a third party server with requests, creating a denial-of-service attack and masking the attacker's true identity. There is no guarantee that the URL given to the JMAP server is actually a valid push server. Upon creation of a push subscription, the JMAP server sends a PushVerification object to the URL and MUST NOT send any further requests until the client verifies it has received the initial push. The verification cod [...]
+
+The verification code does not guarantee the URL is a valid push server, only
+that the client is able to access the data submitted to it. While the
+verification step significantly reduces the set of potential targets, there is
+still a risk that the server is unrelated to the client and being targeted for
+a denial-of-service attack.
+
+The server MUST limit the number of push subscriptions any one user may have to ensure the user cannot cause the server to send a large number of push notifications at once, which could again be used as part of a denial-of-service attack. The rate of creation MUST also be limited to minimise the ability to abuse the verification request as an attack vector.
+
+## Push Encryption
+
+When data changes, a small object is pushed with the new state strings for the types that have changed. While the data here is minimal, a passive man-in-the-middle attacker may be able to gain useful information. To ensure confidentiality and integrity, if the push is sent via a third party outside of the control of the client and JMAP server, the client MUST specify encryption keys when establishing the PushSubscription and ignore any push notification received that is not encrypted wit [...]
+
+The privacy and security considerations of [@!RFC8030] and [@!RFC8291] also apply to the use of the PushSubscription mechanism.
+
+As there is no crypto algorithm agility in Web Push Encryption [@!RFC8291], a new specification will be needed to provide this if new algorithms are required in the future.
+
+## Traffic Analysis
+
+While the data is encrypted, a passive observer with the ability to monitor network traffic may be able to glean information from the timing of API requests and push notifications. For example, suppose an email or calendar invitation is sent from User A (hosted on Server X) to User B (hosted on Server Y). If Server X hosts data for many users, a passive observer can see that the two servers connected but does not know who the data was for. However, if a push notification is immediately s [...]
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/session.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/session.mdown
new file mode 100644
index 0000000..d813e0d
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/session.mdown
@@ -0,0 +1,173 @@
+# The JMAP Session Resource
+
+You need two things to connect to a JMAP server:
+
+1. The URL for the JMAP Session resource. This may be requested directly from
+   the user or discovered automatically based on a username domain (see Section 2.2 below).
+2. Credentials to authenticate with. How to obtain credentials is out of scope
+   for this document.
+
+A successful authenticated GET request to the JMAP Session resource MUST return a JSON-encoded **Session** object, giving details about the data and capabilities the server can provide to the client given those credentials. It has the following properties:
+
+- **capabilities**: `String[Object]`
+  An object specifying the capabilities of this server. Each key is a URI for a capability supported by the server. The value for each of these keys is an object with further information about the server's capabilities in relation to that capability.
+
+    The client MUST ignore any properties it does not understand.
+
+    The capabilities object MUST include a property called `urn:ietf:params:jmap:core`. The value of this property is an object that MUST contain the following information on server capabilities (suggested minimum values for limits are supplied that allow clients to make efficient use of the network):
+
+    - **maxSizeUpload**: `UnsignedInt`
+      The maximum file size, in octets, that the server will accept for a single file upload (for any purpose). Suggested minimum: 50,000,000.
+    - **maxConcurrentUpload**: `UnsignedInt`
+      The maximum number of concurrent requests the server will accept to the upload endpoint.  Suggested minimum: 4.
+    - **maxSizeRequest**: `UnsignedInt`
+      The maximum size, in octets, that the server will accept for a single
+      request to the API endpoint. Suggested minimum: 10,000,000.
+    - **maxConcurrentRequests**: `UnsignedInt`
+      The maximum number of concurrent requests the server will accept to
+      the API endpoint. Suggested minimum: 4.
+    - **maxCallsInRequest**: `UnsignedInt`
+      The maximum number of method calls the server will accept in a single request to the API endpoint.  Suggested minimum: 16.
+    - **maxObjectsInGet**: `UnsignedInt`
+      The maximum number of objects that the client may request in a single `/get` type method call. Suggested minimum: 500.
+    - **maxObjectsInSet**: `UnsignedInt`
+      The maximum number of objects the client may send to create, update, or destroy in a single `/set` type method call. This is the combined total, e.g., if the maximum is 10, you could not create 7 objects and destroy 6, as this would be 13 actions, which exceeds the limit. Suggested minimum: 500.
+    - **collationAlgorithms**: `String[]`
+      A list of identifiers for algorithms registered in the collation registry, as defined in [@!RFC4790], that the server supports for sorting when querying records.
+
+    Specifications for future capabilities will define their own properties on the capabilities object.
+
+    Servers MAY advertise vendor-specific JMAP extensions, as described in Section 1.8. To avoid conflict, an identifier for a vendor-specific extension MUST be a URL with a domain owned by the vendor. Clients MUST opt in to any capability it wishes to use (see Section 3.3).
+
+- **accounts**: `Id[Account]`
+  A map of an **account id** to an Account object for each account (see Section 1.6.2) the user has access to. An **Account** object has the following properties:
+
+    - **name**: `String`
+      A user-friendly string to show when presenting content from this account, e.g., the email address representing the owner of the account.
+    - **isPersonal**: `Boolean`
+      This is `true` if the account belongs to the authenticated user rather than a group account or a personal account of another user that has been shared with them.
+    - **isReadOnly**: `Boolean`
+      This is `true` if the entire account is read-only.
+    - **accountCapabilities**: `String[Object]`
+      The set of capability URIs for the methods supported in this account. Each key is a URI for a capability that has methods you can use with this account. The value for each of these keys is an object with further information about the account's permissions and restrictions with respect to this capability, as defined in the capability's specification.
+
+        The client MUST ignore any properties it does not understand.
+
+        The server advertises the full list of capabilities it supports in the
+        capabilities object, as defined above. If the capability defines new
+        methods, the server MUST include it in the *accountCapabilities* object
+        if the user may use those methods with this account. It MUST NOT
+        include it in the *accountCapabilities* object if the user cannot use
+        those methods with this account.
+
+        For example, you may have access to your own account with mail,
+        calendars, and contacts data and also a shared account that only has
+        contacts data (a  business address book, for example). In this case, the
+        *accountCapabilities* property on the first account would include
+        something like `urn:ietf:params:jmap:mail`,
+        `urn:ietf:params:jmap:calendars`, and `urn:ietf:params:jmap:contacts`,
+        while the second account would just have the last of these.
+
+        Attempts to use the methods defined in a capability with one of the
+        accounts that does not support that capability are rejected with an
+        *accountNotSupportedByMethod* error (see "Method-Level
+         Errors", Section 3.6.2).
+
+- **primaryAccounts**: `String[Id]`
+  A map of capability URIs (as found in *accountCapabilities*) to the account id that is considered to be the user's main or default account for data pertaining to that capability. If no account being returned belongs to the user, or in any other way there is no appropriate way to determine a default account, there MAY be no entry for a particular URI, even though that capability is supported by the server (and in the capabilities object). `urn:ietf:params:jmap:core` SHOULD NOT be present.
+- **username**: `String`
+  The username associated with the given credentials, or the empty string if none.
+- **apiUrl**: `String`
+  The URL to use for JMAP API requests.
+- **downloadUrl**: `String`
+  The URL endpoint to use when downloading files, in URI Template (level 1) format [@!RFC6570]. The URL MUST contain variables called `accountId`, `blobId`, `type`, and `name`. The use of these variables is described in Section 6.2. Due to potential encoding issues with slashes in content types, it is RECOMMENDED to put the `type` variable in the query section of the URL.
+- **uploadUrl**: `String`
+  The URL endpoint to use when uploading files, in URI Template (level 1) format [@!RFC6570]. The URL MUST contain a variable called `accountId`. The use of this variable is described in Section 6.1.
+- **eventSourceUrl**: `String`
+  The URL to connect to for push events, as described in Section 7.3, in URI Template (level 1) format [@!RFC6570]. The URL MUST contain variables called `types`, `closeafter`, and `ping`. The use of these variables is described in Section 7.3.
+- **state**: `String`
+  A (preferably short) string representing the state of this object on the server. If the value of any other property on the Session object changes, this string will change. The current value is also returned on the API Response object (see Section 3.4), allowing clients to quickly determine if the session information has changed (e.g., an account has been added or removed), so they need to refetch the object.
+
+To ensure future compatibility, other properties MAY be included on the Session object. Clients MUST ignore any properties they are not expecting.
+
+Implementors must take care to avoid inappropriate caching of the Session object at the HTTP layer. Since the client should only refetch when it detects there is a change (via the sessionState property of an API response), it is RECOMMENDED to disable HTTP caching altogether, for example, by setting `Cache-Control: no-cache, no-store, must-revalidate` on the response.
+
+## Example
+
+In the following example Session object, the user has access to their own mail and contacts via JMAP, as well as read-only access to shared mail from another user. The server is advertising a custom `https://example.com/apis/foobar` capability.
+
+    {
+      "capabilities": {
+        "urn:ietf:params:jmap:core": {
+          "maxSizeUpload": 50000000,
+          "maxConcurrentUpload": 8,
+          "maxSizeRequest": 10000000,
+          "maxConcurrentRequests": 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"
+    }
+
+## Service Autodiscovery
+
+There are two standardised autodiscovery methods in use for Internet protocols:
+
+- **DNS SRV** (see [@!RFC2782], [@!RFC6186], and [@!RFC6764])
+- **.well-known/servicename** (see [@!RFC8615])
+
+A JMAP-supporting host for the domain `example.com` SHOULD publish a SRV record `_jmap._tcp.example.com` that gives a *hostname* and *port* (usually port `443`). The JMAP Session resource is then `https://${hostname}[:${port}]/.well-known/jmap` (following any redirects).
+
+If the client has a username in the form of an email address, it MAY use the domain portion of this to attempt autodiscovery of the JMAP server.
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/ianaconsiderations.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/ianaconsiderations.mdown
new file mode 100644
index 0000000..8a1f974
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/ianaconsiderations.mdown
@@ -0,0 +1,421 @@
+# IANA Considerations
+
+## JMAP Capability Registration for "mail"
+
+IANA has registered the "mail" JMAP Capability as follows:
+
+Capability Name: `urn:ietf:params:jmap:mail`
+
+Specification document: this document
+
+Intended use: common
+
+Change Controller: IETF
+
+Security and privacy considerations: this document, Section 9
+
+## JMAP Capability Registration for "submission"
+
+IANA has registered the "submission" JMAP Capability as follows:
+
+Capability Name: `urn:ietf:params:jmap:submission`
+
+Specification document: this document
+
+Intended use: common
+
+Change Controller: IETF
+
+Security and privacy considerations: this document, Section 9
+
+## JMAP Capability Registration for "vacationresponse"
+
+IANA has registered the "vacationresponse" JMAP Capability as follows:
+
+Capability Name: `urn:ietf:params:jmap:vacationresponse`
+
+Specification document: this document
+
+Intended use: common
+
+Change Controller: IETF
+
+Security and privacy considerations: this document, Section 9
+
+## IMAP and JMAP Keywords Registry
+
+This document makes two changes to the IMAP keywords registry as defined in [@!RFC5788].
+
+First, the name of the registry is changed to the "IMAP and JMAP Keywords" registry.
+
+Second, a scope column is added to the template and registry indicating
+whether a keyword applies to "IMAP-only", "JMAP-only", "both", or "reserved".
+All keywords already in the IMAP keyword registry have been marked with a
+scope of "both". The "reserved" status can be used to prevent future
+registration of a name that would be confusing if registered.
+Registration of keywords with scope "reserved" omit most fields in the
+registration template (see registration of `$recent` below for an example); such registrations are intended to be infrequent.
+
+IMAP clients MAY silently ignore any keywords marked "JMAP-only" or
+"reserved" in the event they appear in protocol. JMAP clients MAY silently
+ignore any keywords marked "IMAP-only" or "reserved" in the event they appear
+in protocol.
+
+New "JMAP-only" keywords are registered in the following subsections.
+These keywords correspond to IMAP system keywords and are thus not
+appropriate for use in IMAP. These keywords cannot be subsequently
+registered for use in IMAP except via standards action.
+
+### Registration of JMAP Keyword '$draft'
+
+This registers the "JMAP-only" keyword `$draft` in the "IMAP and JMAP Keywords" registry.
+
+Keyword name: `$draft`
+
+Scope: JMAP-only
+
+Purpose (description): This is set when the user wants to treat the
+message as a draft the user is composing. This is the JMAP equivalent of the IMAP \Draft flag.
+
+Private or Shared on a server: BOTH
+
+Is it an advisory keyword or may it cause an automatic action:
+Automatic. If the account has an IMAP mailbox marked with the \Drafts special
+use attribute [RFC6154], setting this flag MAY cause the message to appear in
+that mailbox automatically. Certain JMAP computed values such as
+*unreadEmails* will change as a result of changing this flag. In
+addition, mail clients will typically present draft messages in a
+composer window rather than a viewer window.
+
+When/by whom the keyword is set/cleared:
+This is typically set by a JMAP client when referring to a draft
+message. One model for draft Emails would result in clearing this flag
+in an EmailSubmission/set operation with an onSuccessUpdateEmail
+argument. In a mail store shared by JMAP and IMAP, this is also set and
+cleared as necessary so it matches the IMAP \Draft flag.
+
+Related keywords: None
+
+Related IMAP/JMAP Capabilities: SPECIAL-USE [RFC6154]
+
+Security Considerations:
+A server implementing this keyword as a shared keyword may disclose that
+a user considers the message a draft message. This information would be
+exposed to other users with read permission for the Mailbox keywords.
+
+Published specification: this document
+
+Person & email address to contact for further information:
+JMAP mailing list <jm...@ietf.org>
+
+Intended usage: COMMON
+
+Owner/Change controller: IESG
+
+### Registration of JMAP Keyword '$seen'
+
+This registers the "JMAP-only" keyword `$seen` in the "IMAP and JMAP
+Keywords" registry.
+
+Keyword name: `$seen`
+
+Scope: JMAP-only
+
+Purpose (description): This is set when the user wants to treat the
+message as read. This is the JMAP equivalent of the IMAP \Seen flag.
+
+Private or Shared on a server: BOTH
+
+Is it an advisory keyword or may it cause an automatic action:
+Advisory. However, certain JMAP computed values such as *unreadEmails*
+will change as a result of changing this flag.
+
+When/by whom the keyword is set/cleared:
+This is set by a JMAP client when it presents the message content to the
+user; clients often offer an option to clear this flag. In a mail store
+shared by JMAP and IMAP, this is also set and cleared as necessary so it
+matches the IMAP \Seen flag.
+
+Related keywords: None
+
+Related IMAP/JMAP Capabilities: None
+
+Security Considerations:
+A server implementing this keyword as a shared keyword may disclose that
+a user considers the message to have been read. This information would be
+exposed to other users with read permission for the Mailbox keywords.
+
+Published specification: this document
+
+Person & email address to contact for further information:
+JMAP mailing list <jm...@ietf.org>
+
+Intended usage: COMMON
+
+Owner/Change controller: IESG
+
+### Registration of JMAP Keyword '$flagged'
+
+This registers the "JMAP-only" keyword `$flagged` in the "IMAP and JMAP
+Keywords" registry.
+
+Keyword name: `$flagged`
+
+Scope: JMAP-only
+
+Purpose (description): This is set when the user wants to treat the
+message as flagged for urgent/special attention. This is the JMAP
+equivalent of the IMAP \Flagged flag.
+
+Private or Shared on a server: BOTH
+
+Is it an advisory keyword or may it cause an automatic action:
+Automatic. If the account has an IMAP mailbox marked with the \Flagged special
+use attribute [RFC6154], setting this flag MAY cause the message to appear in
+that mailbox automatically.
+
+When/by whom the keyword is set/cleared:
+JMAP clients typically allow a user to set/clear this flag as desired.
+In a mail store shared by JMAP and IMAP, this is also set and cleared as
+necessary so it matches the IMAP \Flagged flag.
+
+Related keywords: None
+
+Related IMAP/JMAP Capabilities: SPECIAL-USE [RFC6154]
+
+Security Considerations:
+A server implementing this keyword as a shared keyword may disclose that
+a user considers the message as flagged for urgent/special attention.
+This information would be exposed to other users with read permission
+for the Mailbox keywords.
+
+Published specification: this document
+
+Person & email address to contact for further information:
+JMAP mailing list <jm...@ietf.org>
+
+Intended usage: COMMON
+
+Owner/Change controller: IESG
+
+### Registration of JMAP Keyword '$answered'
+
+This registers the "JMAP-only" keyword `$answered` in the "IMAP and JMAP
+Keywords" registry.
+
+Keyword name: `$answered`
+
+Scope: JMAP-only
+
+Purpose (description): This is set when the message has been answered.
+
+Private or Shared on a server: BOTH
+
+Is it an advisory keyword or may it cause an automatic action:
+Advisory.
+
+When/by whom the keyword is set/cleared:
+JMAP clients typically set this when submitting a reply or answer to the
+message. It may be set by the EmailSubmission/set operation with an
+onSuccessUpdateEmail argument. In a mail store shared by JMAP and IMAP,
+this is also set and cleared as necessary so it matches the IMAP
+\Answered flag.
+
+Related keywords: None
+
+Related IMAP/JMAP Capabilities: None
+
+Security Considerations:
+A server implementing this keyword as a shared keyword may disclose that
+a user has replied to a message.
+This information would be exposed to other users with read permission
+for the Mailbox keywords.
+
+Published specification: this document
+
+Person & email address to contact for further information:
+JMAP mailing list <jm...@ietf.org>
+
+Intended usage: COMMON
+
+Owner/Change controller: IESG
+
+### Registration of '$recent' Keyword
+
+This registers the keyword `$recent` in the "IMAP and JMAP Keywords" registry.
+
+Keyword name: `$recent`
+
+Scope: reserved
+
+Purpose (description): This keyword is not used to avoid confusion with
+the IMAP \Recent system flag.
+
+Published specification: this document
+
+Person & email address to contact for further information:
+JMAP mailing list <jm...@ietf.org>
+
+Owner/Change controller: IESG
+
+## IMAP Mailbox Name Attributes Registry
+
+### Registration of "inbox" Role
+
+This registers the "JMAP-only" `inbox` attribute in the "IMAP Mailbox Name Attributes" registry, as established in [@!RFC8457].
+
+Attribute Name: Inbox
+
+Description: New mail is delivered here by default.
+
+Reference: This document, Section 10.5.
+
+Usage Notes: JMAP only
+
+## JMAP Error Codes Registry
+
+The following subsections register several new error codes in the "JMAP Error Codes" registry, as defined in [@!RFC8620].
+
+### mailboxHasChild
+
+JMAP Error Code: mailboxHasChild
+
+Intended use: common
+
+Change controller: IETF
+
+Reference: This document, Section 2.5
+
+Description: The Mailbox still has at least one child Mailbox. The client MUST remove these before it can delete the parent Mailbox.
+
+### mailboxHasEmail
+
+JMAP Error Code: mailboxHasEmail
+
+Intended use: common
+
+Change controller: IETF
+
+Reference: This document, Section 2.5
+
+Description: The Mailbox has at least one message assigned to it and the onDestroyRemoveEmails argument was false.
+
+### blobNotFound
+
+JMAP Error Code: blobNotFound
+
+Intended use: common
+
+Change controller: IETF
+
+Reference: This document, Section 4.6
+
+Description: At least one blob id referenced in the object doesn’t exist.
+
+### tooManyKeywords
+
+JMAP Error Code: tooManyKeywords
+
+Intended use: common
+
+Change controller: IETF
+
+Reference: This document, Section 4.6
+
+Description: The change to the Email’s keywords would exceed a server-defined maximum.
+
+### tooManyMailboxes
+
+JMAP Error Code: tooManyMailboxes
+
+Intended use: common
+
+Change controller: IETF
+
+Reference: This document, Section 4.6
+
+Description: The change to the set of Mailboxes that this Email is in would exceed a server-defined maximum.
+
+### invalidEmail
+
+JMAP Error Code: invalidEmail
+
+Intended use: common
+
+Change controller: IETF
+
+Reference: This document, Section 7.5
+
+Description: The Email to be sent is invalid in some way.
+
+### tooManyRecipients
+
+JMAP Error Code: tooManyRecipients
+
+Intended use: common
+
+Change controller: IETF
+
+Reference: This document, Section 7.5
+
+Description: The envelope [@!RFC5321]  (supplied or generated) has more recipients than the server allows.
+
+### noRecipients
+
+JMAP Error Code: noRecipients
+
+Intended use: common
+
+Change controller: IETF
+
+Reference: This document, Section 7.5
+
+Description: The envelope [@!RFC5321]  (supplied or generated) does not have any rcptTo email addresses.
+
+### invalidRecipients
+
+JMAP Error Code: invalidRecipients
+
+Intended use: common
+
+Change controller: IETF
+
+Reference: This document, Section 7.5
+
+Description:  The rcptTo property of the envelope [@!RFC5321]  (supplied or generated) contains at least one rcptTo value that is not a valid email address for sending to.
+
+### forbiddenMailFrom
+
+JMAP Error Code: forbiddenMailFrom
+
+Intended use: common
+
+Change controller: IETF
+
+Reference: This document, Section 7.5
+
+Description: – The server does not permit the user to send a message with this envelope From address [@!RFC5321].
+
+### forbiddenFrom
+
+JMAP Error Code: forbiddenFrom
+
+Intended use: common
+
+Change controller: IETF
+
+Reference: This document, sections 6.3 and 7.5
+
+Description: The server does not permit the user to send a message with the From header field [@!RFC5322] of the message to be sent.
+
+### forbiddenToSend
+
+JMAP Error Code: forbiddenToSend
+
+Intended use: common
+
+Change controller: IETF
+
+Reference: This document, Section 7.5
+
+Description: The user does not have permission to send at all right now.
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/identity.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/identity.mdown
new file mode 100644
index 0000000..be3f64b
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/identity.mdown
@@ -0,0 +1,85 @@
+# Identities
+
+An **Identity** object stores information about an email address or domain the user may send from. It has the following properties:
+
+- **id**: `Id` (immutable; server-set)
+  The id of the Identity.
+- **name**: `String` (default: "")
+  The "From" *name* the client SHOULD use when creating a new Email from this Identity.
+- **email**: `String` (immutable)
+  The "From" email address the client MUST use when creating a new Email from this Identity. If the *mailbox* part of the address (the section before the "@") is the single character `*` (e.g., `*@example.com`) then the client may use any valid address ending in that domain (e.g., `foo@example.com`).
+- **replyTo**: `EmailAddress[]|null` (default: null)
+  The Reply-To value the client SHOULD set when creating a new Email from this Identity.
+- **bcc**: `EmailAddress[]|null` (default: null)
+  The Bcc value the client SHOULD set when creating a new Email from this Identity.
+- **textSignature**: `String` (default: "")
+  A signature the client SHOULD insert into new plaintext messages that will be sent from this Identity. Clients MAY ignore this and/or combine this with a client-specific signature preference.
+- **htmlSignature**: `String` (default: "")
+  A signature the client SHOULD insert into new HTML messages that will be sent from this Identity. This text MUST be an HTML snippet to be inserted into the `<body></body>` section of the HTML. Clients MAY ignore this and/or combine this with a client-specific signature preference.
+- **mayDelete**: `Boolean` (server-set)
+  Is the user allowed to delete this Identity? Servers may wish to set this to `false` for the user's username or other default address. Attempts to destroy an Identity with `mayDelete: false` will be rejected with a standard `forbidden` SetError.
+
+See the "Addresses" header form description in the Email object for the definition of *EmailAddress*.
+
+Multiple identities with the same email address MAY exist, to allow for different settings the user wants to pick between (for example, with different names/signatures).
+
+The following JMAP methods are supported.
+
+## Identity/get
+
+This is a standard "/get" method as described in [@!RFC8620], Section 5.1. The *ids* argument may be `null` to fetch all at once.
+
+## Identity/changes
+
+This is a standard "/changes" method as described in [@!RFC8620], Section 5.2.
+
+## Identity/set
+
+This is a standard "/set" method as described in [@!RFC8620], Section 5.3. The following extra *SetError* types are defined:
+
+For **create**:
+
+- `forbiddenFrom`: The user is not allowed to send from the address given as
+  the *email* property of the Identity.
+
+## Example
+
+Request:
+
+    [ "Identity/get", {
+      "accountId": "acme"
+    }, "0" ]
+
+with response:
+
+    [ "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" ]
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/intro.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/intro.mdown
new file mode 100644
index 0000000..006ce4c
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/intro.mdown
@@ -0,0 +1,126 @@
+# Introduction
+
+The JSON Meta Application Protocol (JMAP) [@!RFC8620] is a generic protocol for synchronising data, such as mail, calendars, or contacts between a client and a server. It is optimised for mobile and web environments and aims to provide a consistent interface to different data types.
+
+This specification defines a data model for accessing a mail store over JMAP,
+allowing you to query, read, organise, and submit mail for sending.
+
+The data model is designed to allow a server to provide consistent access to
+the same data via IMAP [@?RFC3501] as well as JMAP. As in IMAP, a message
+must belong to a Mailbox; however, in JMAP, its id does not change if you move
+it between Mailboxes, and the server may allow it to belong to multiple
+Mailboxes simultaneously (often exposed in a user agent as labels rather than
+folders).
+
+As in IMAP, messages may also be assigned zero or more keywords: short arbitrary
+strings. These are primarily intended to store metadata to inform client
+display, such as unread status or whether a message has been replied to. An
+IANA registry allows common semantics to be shared between clients and extended
+easily in the future.
+
+A message and its replies are linked on the server by a common Thread id.
+Clients may fetch the list of messages with a particular Thread id to more
+easily present a threaded or conversational interface.
+
+Permissions for message access happen on a per-Mailbox basis. Servers may give
+the user restricted permissions for certain Mailboxes, for example, if another
+user's inbox has been shared as read-only with them.
+
+## Notational Conventions
+
+The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [@!RFC2119] [@!RFC8174] when, and only when, they appear in all capitals, as shown here.
+
+Type signatures, examples, and property descriptions in this document follow the conventions established in Section 1.1 of [@!RFC8620]. Data types defined in the core specification are also used in this document.
+
+Servers MUST support all properties specified for the new data types defined in this document.
+
+## Terminology
+
+This document uses the same terminology as in the core JMAP specification.
+
+The terms Mailbox, Thread, Email, SearchSnippet, EmailSubmission and VacationResponse (with that specific capitalisation) are used to refer to the data types defined in this document and instances of those data types.
+
+The term message refers to a document in Internet Message Format, as described in [@!RFC5322]. The Email data type represents messages in the mail store and associated metadata.
+
+## Additions to the Capabilities Object
+
+The capabilities object is returned as part of the JMAP Session object; see [@!RFC8620], Section 2.
+
+This document defines three additional capability URIs.
+
+### urn:ietf:params:jmap:mail
+
+This represents support for the Mailbox, Thread, Email, and SearchSnippet data types and associated API methods. The value of this property in the JMAP session *capabilities* property is an empty object.
+
+The value of this property in an account's *accountCapabilities* property is an object that MUST contain the following information on server capabilities and permissions for that account:
+
+- **maxMailboxesPerEmail**: `UnsignedInt|null`
+  The maximum number of Mailboxes (see Section 2) that can be can assigned to a single Email object (see Section 4). This MUST be an integer >= 1, or `null` for no limit (or rather, the limit is always the number of Mailboxes in the account).
+- **maxMailboxDepth**: `UnsignedInt|null`
+  The maximum depth of the Mailbox hierarchy (i.e., one more than the maximum number of ancestors a Mailbox may have), or `null` for no limit.
+- **maxSizeMailboxName**: `UnsignedInt`
+  The maximum length, in (UTF-8) octets, allowed for the name of a Mailbox. This MUST be at least 100, although it is recommended servers allow more.
+- **maxSizeAttachmentsPerEmail**: `UnsignedInt`
+  The maximum total size of attachments, in octets, allowed for a single Email object. A server MAY still reject the import or creation of an Email with a lower attachment size total (for example, if the body includes several megabytes of text, causing the size of the encoded MIME structure to be over some server-defined limit).
+
+    Note that this limit is for the sum of unencoded attachment sizes. Users are generally not knowledgeable about encoding overhead, etc., nor should they need to be, so marketing and help materials normally tell them the "max size attachments". This is the unencoded size they see on their hard drive, so this capability matches that and allows the client to consistently enforce what the user understands as the limit.
+
+    The server may separately have a limit for the total size of the message [@!RFC5322], created by combining the attachments (often base64 encoded) with the message headers and bodies. For example, suppose the server advertises `maxSizeAttachmentsPerEmail: 50000000` (50 MB). The enforced server limit may be for a message size of 70000000 octets. Even with base64 encoding and a 2 MB HTML body, 50 MB attachments would fit under this limit.
+
+- **emailQuerySortOptions**: `String[]`
+  A list of all the values the server supports for the "property" field of the Comparator object in an Email/query sort (see Section 4.4.2). This MAY include properties the client does not recognise (for example, custom properties specified in a vendor extension). Clients MUST ignore any unknown properties in the list.
+- **mayCreateTopLevelMailbox**: `Boolean`
+  If `true`, the user may create a Mailbox (see Section 2) in this account with
+  a `null` parentId. (Permission for creating a child of an existing Mailbox is given by the myRights property on that Mailbox.)
+
+### urn:ietf:params:jmap:submission
+
+This represents support for the Identity and EmailSubmission data types and associated API methods. The value of this property in the JMAP session *capabilities* property is an empty object.
+
+The value of this property in an account's *accountCapabilities* property is an object that MUST contain the following information on server capabilities and permissions for that account:
+
+- **maxDelayedSend**: `UnsignedInt`
+  The number in seconds of the maximum delay the server supports in sending
+  (see the EmailSubmission object description). This is `0` if the server does
+  not support delayed send.
+- **submissionExtensions**: `String[String[]]`
+  The set of SMTP submission extensions supported by the server, which the client may use when creating an EmailSubmission object (see Section 7). Each key in the object is the *ehlo-name*, and the value is a list of *ehlo-args*.
+
+    A JMAP implementation that talks to a submission server [@!RFC6409] SHOULD have a configuration setting that allows an administrator to modify the set of submission EHLO capabilities it may expose on this property. This allows a JMAP server to easily add access to a new submission extension without code changes. By default, the JMAP server should hide EHLO capabilities that have to do with the transport mechanism and thus are only relevant to the JMAP server (for example, PIPELINING, [...]
+
+    Examples of Submission extensions to include:
+
+    - FUTURERELEASE [@!RFC4865]
+    - SIZE [@!RFC1870]
+    - DSN [@!RFC3461]
+    - DELIVERYBY [@!RFC2852]
+    - MT-PRIORITY [@!RFC6710]
+
+    A JMAP server MAY advertise an extension and implement the semantics of that extension locally on the JMAP server even if a submission server used by JMAP doesn't implement it.
+
+    The full IANA registry of submission extensions can be found at
+    <https://www.iana.org/assignments/mail-parameters>.
+
+### urn:ietf:params:jmap:vacationresponse
+
+This represents support for the VacationResponse data type and associated API methods. The value of this property is an empty object in both the JMAP session *capabilities* property and an account's *accountCapabilities* property.
+
+## Data Type Support in Different Accounts
+
+The server MUST include the appropriate capability strings as keys in the *accountCapabilities* property of any account with which the user may use the data types represented by that URI. Supported data types may differ between accounts the user has access to. For example, in the user's personal account, they may have access to all three sets of data, but in a shared account, they may only have data for `urn:ietf:params:jmap:mail`. This means they can access Mailbox/Thread/Email data in  [...]
+
+## Push
+
+Servers MUST support the JMAP push mechanisms, as specified in [@!RFC8620] Section 7, to receive notifications when the state changes for any of the types defined in this specification.
+
+In addition, servers that implement the "urn:ietf:params:jmap:mail" capability MUST support pushing state changes for a type called "EmailDelivery". There are no methods to act on this type; it only exists as part of the push mechanism. The state string for this MUST change whenever a new Email is added to the store, but it SHOULD NOT change upon any other change to the Email objects, for example, if one is marked as read or deleted.
+
+Clients in battery-constrained environments may wish to delay fetching changes initiated by the user but fetch new Emails immediately so they can notify the user. To do this, they can register for pushes for the EmailDelivery type rather than the Email type (as defined in Section 4).
+
+### Example
+
+The client has registered for push notifications (see [@!RFC8620]) just for the `EmailDelivery` type. The user marks an Email as read on another device, causing the state string for the `Email` type to change; however, as nothing new was added to the store, the `EmailDelivery` state does not change and nothing is pushed to the client. A new message arrives in the user's inbox, again causing the `Email` state to change. This time, the `EmailDelivery` state also changes, and a StateChange  [...]
+
+## Ids
+
+If a JMAP Mail server also provides an IMAP interface to the data and supports IMAP Extension for Object Identifiers [@!RFC8474], the ids SHOULD be the same for Mailbox, Thread, and Email objects in JMAP.
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/mailbox.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/mailbox.mdown
new file mode 100644
index 0000000..d144ca6
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/mailbox.mdown
@@ -0,0 +1,290 @@
+# Mailboxes
+
+A Mailbox represents a named set of Emails. This is the primary mechanism for organising Emails within an account. It is analogous to a folder or a label in other systems. A Mailbox may perform a certain role in the system; see below for more details.
+
+For compatibility with IMAP, an Email MUST belong to one or more Mailboxes. The Email id does not change if the Email changes Mailboxes.
+
+A **Mailbox** object has the following properties:
+
+- **id**: `Id` (immutable; server-set)
+  The id of the Mailbox.
+- **name**: `String`
+  User-visible name for the Mailbox, e.g., "Inbox". This MUST be a Net-Unicode string [@!RFC5198] of at least 1 character in length, subject to the maximum size given in the capability object. There MUST NOT be two sibling Mailboxes with both the same parent and the same name. Servers MAY reject names that violate server policy (e.g., names containing a slash (/) or control characters).
+- **parentId**: `Id|null` (default: null)
+  The Mailbox id for the parent of this Mailbox, or `null` if this Mailbox is at the top level. Mailboxes form acyclic graphs (forests) directed by the child-to-parent relationship. There MUST NOT be a loop.
+- **role**: `String|null` (default: null)
+  Identifies Mailboxes that have a particular common purpose (e.g., the "inbox"), regardless of the *name* property (which may be localised).
+
+    This value is shared with IMAP (exposed in IMAP via the SPECIAL-USE extension [@!RFC6154]). However, unlike in IMAP, a Mailbox MUST only have a single role, and there MUST NOT be two Mailboxes in the same account with the same role. Servers providing IMAP access to the same data are encouraged to enforce these extra restrictions in IMAP as well. Otherwise, modifying the IMAP attributes to ensure compliance when exposing the data over JMAP is implementation dependent.
+
+    The value MUST be one of the Mailbox attribute names listed in the [IANA IMAP Mailbox Name Attributes](https://www.iana.org/assignments/imap-mailbox-name-attributes/imap-mailbox-name-attributes.xhtml) registry, as established in [@!RFC8457], converted to lowercase. New roles may be established here in the future.
+
+    An account is not required to have Mailboxes with any particular roles.
+
+- **sortOrder**: `UnsignedInt` (default: 0)
+  Defines the sort order of Mailboxes when presented in the client's UI, so it
+  is consistent between devices. The number MUST be an integer in the range
+  0 <= sortOrder < 2^31.
+
+    A Mailbox with a lower order should be displayed before a Mailbox with a higher order (that has the same parent) in any Mailbox listing in the client's UI. Mailboxes with equal order SHOULD be sorted in alphabetical order by name. The sorting should take into account locale-specific character order convention.
+
+- **totalEmails**: `UnsignedInt` (server-set)
+  The number of Emails in this Mailbox.
+- **unreadEmails**: `UnsignedInt` (server-set)
+  The number of Emails in this Mailbox that have neither the `$seen` keyword nor the `$draft` keyword.
+- **totalThreads**: `UnsignedInt` (server-set)
+  The number of Threads where at least one Email in the Thread is in this Mailbox.
+- **unreadThreads**: `UnsignedInt` (server-set)
+  An indication of the number of "unread" Threads in the Mailbox.
+
+    For compatibility with existing implementations, the way "unread Threads" is
+    determined is not mandated in this document. The simplest solution to implement is simply the number of Threads where at least one Email in the Thread is both in this Mailbox and has neither the `$seen` nor `$draft` keywords.
+
+    However, a quality implementation will return the number of unread items the user would see if they opened that Mailbox. A Thread is shown as unread if it contains any unread Emails that will be displayed when the Thread is opened. Therefore, `unreadThreads` should be the number of Threads where at least one Email in the Thread has neither the `$seen` nor the `$draft` keyword AND at least one Email in the Thread is in this Mailbox. Note that the unread Email does not need to be the o [...]
+
+    1. Emails that are **only** in the trash (and no other Mailbox) are ignored when calculating the `unreadThreads` count of other Mailboxes.
+    2. Emails that are **not** in the trash are ignored when calculating the `unreadThreads` count for the trash Mailbox.
+
+    The result of this is that Emails in the trash are treated as though they are in a separate Thread for the purposes of unread counts. It is expected that clients will hide Emails in the trash when viewing a Thread in another Mailbox, and vice versa. This allows you to delete a single Email to the trash out of a Thread.
+
+    For example, suppose you have an account where the entire contents is a single Thread with 2 Emails: an unread Email in the trash and a read Email in the inbox. The `unreadThreads` count would be `1` for the trash and `0` for the inbox.
+
+- **myRights**: `MailboxRights` (server-set)
+  The set of rights (Access Control Lists (ACLs)) the user has in relation to this Mailbox. These are backwards compatible with IMAP ACLs, as defined in [@!RFC4314]. A **MailboxRights** object has the following properties:
+
+    - **mayReadItems**: `Boolean`
+      If true, the user may use this Mailbox as part of a filter in an *Email/query* call, and the Mailbox may be included in the *mailboxIds* property of Email objects. Email objects may be fetched if they are in **at least one** Mailbox with this permission. If a sub-Mailbox is shared but not the parent Mailbox, this may be `false`. Corresponds to IMAP ACLs `lr` (if mapping from IMAP, both are required for this to be `true`).
+    - **mayAddItems**: `Boolean`
+      The user may add mail to this Mailbox (by either creating a new Email or moving an existing one). Corresponds to IMAP ACL `i`.
+    - **mayRemoveItems**: `Boolean`
+      The user may remove mail from this Mailbox (by either changing the Mailboxes of an Email or destroying the Email). Corresponds to IMAP ACLs `te` (if mapping from IMAP, both are required for this to be `true`).
+    - **maySetSeen**: `Boolean`
+      The user may add or remove the `$seen` keyword to/from an Email. If an Email belongs to multiple Mailboxes, the user may only modify `$seen` if they have this permission for **all** of the Mailboxes. Corresponds to IMAP ACL `s`.
+    - **maySetKeywords**: `Boolean`
+      The user may add or remove any keyword *other than* `$seen` to/from an Email. If an Email belongs to multiple Mailboxes, the user may only modify keywords if they have this permission for **all** of the Mailboxes. Corresponds to IMAP ACL `w`.
+    - **mayCreateChild**: `Boolean`
+      The user may create a Mailbox with this Mailbox as its parent. Corresponds to IMAP ACL `k`.
+    - **mayRename**: `Boolean`
+      The user may rename the Mailbox or make it a child of another Mailbox. Corresponds to IMAP ACL `x` (although this covers both rename and delete permissions).
+    - **mayDelete**: `Boolean`
+      The user may delete the Mailbox itself. Corresponds to IMAP ACL `x` (although this covers both rename and delete permissions).
+    - **maySubmit**: `Boolean`
+      Messages may be submitted directly to this Mailbox. Corresponds to IMAP ACL `p`.
+
+- **isSubscribed**: `Boolean`
+  Has the user indicated they wish to see this Mailbox in their client? This SHOULD default to `false` for Mailboxes in shared accounts the user has access to and `true` for any new Mailboxes created by the user themself. This MUST be stored separately per user where multiple users have access to a shared Mailbox.
+
+    A user may have permission to access a large number of shared accounts, or a shared account with a very large set of Mailboxes, but only be interested in the contents of a few of these. Clients may choose to only display Mailboxes where the `isSubscribed` property is set to `true`, and offer a separate UI to allow the user to see and subscribe/unsubscribe from the full set of Mailboxes. However, clients MAY choose to ignore this property, either entirely for ease of implementation or [...]
+
+    This property corresponds to IMAP [@?RFC3501] Mailbox subscriptions.
+
+For IMAP compatibility, an Email in both the trash and another Mailbox SHOULD be treated by the client as existing in both places (i.e., when emptying the trash, the client should just remove it from the trash Mailbox and leave it in the other Mailbox).
+
+The following JMAP methods are supported.
+
+## Mailbox/get
+
+This is a standard "/get" method as described in [@!RFC8620], Section 5.1. The *ids* argument may be `null` to fetch all at once.
+
+## Mailbox/changes
+
+This is a standard "/changes" method as described in [@!RFC8620], Section 5.2 but with one extra argument to the response:
+
+- **updatedProperties**: `String[]|null`
+  If only the "totalEmails", "unreadEmails", "totalThreads", and/or "unreadThreads" Mailbox properties have changed since the old state, this will be the list of properties that may have changed. If the server is unable to tell if only counts have changed, it MUST just be `null`.
+
+Since counts frequently change but other properties are generally only changed rarely, the server can help the client optimise data transfer by keeping track of changes to Email/Thread counts separate from other state changes. The *updatedProperties* array may be used directly via a back-reference in a subsequent Mailbox/get call in the same request, so only these properties are returned if nothing else has changed.
+
+## Mailbox/query
+
+This is a standard "/query" method as described in [@!RFC8620], Section 5.5, but with the following additional request argument:
+
+- **sortAsTree**: `Boolean` (default: false)
+  If `true`, when sorting the query results and comparing Mailboxes A and B:
+
+    - If A is an ancestor of B, it always comes first regardless of the *sort*
+      comparators. Similarly, if A is descendant of B, then B always comes
+      first.
+    - Otherwise, if A and B do not share a *parentId*, find the nearest
+      ancestors of each that do have the same *parentId* and compare the sort
+      properties on those Mailboxes instead.
+
+    The result of this is that the Mailboxes are sorted as a tree according to the parentId properties, with each set of children with a common parent sorted according to the standard sort comparators.
+- **filterAsTree**: `Boolean` (default: false)
+  If `true`, a Mailbox is only included in the query if all its ancestors are
+  also included in the query according to the filter.
+
+A **FilterCondition** object has the following properties, any of which may be omitted:
+
+- **parentId**: `Id|null`
+  The Mailbox *parentId* property must match the given value exactly.
+- **name**: `String`
+  The Mailbox *name* property contains the given string.
+- **role**: `String|null`
+  The Mailbox *role* property must match the given value exactly.
+- **hasAnyRole**: `Boolean`
+  If `true`, a Mailbox matches if it has any non-`null` value for its *role* property.
+- **isSubscribed**: `Boolean`
+  The `isSubscribed` property of the Mailbox must be identical to the value given to match the condition.
+
+A Mailbox object matches the FilterCondition if and only if all of the given conditions match. If zero properties are specified, it is automatically `true` for all objects.
+
+The following Mailbox properties MUST be supported for sorting:
+
+- `sortOrder`
+- `name`
+
+## Mailbox/queryChanges
+
+This is a standard "/queryChanges" method as described in [@!RFC8620], Section 5.6.
+
+## Mailbox/set
+
+This is a standard "/set" method as described in [@!RFC8620], Section 5.3, but with the following additional request argument:
+
+- **onDestroyRemoveEmails**: `Boolean` (default: false)
+  If `false`, any attempt to destroy a Mailbox that still has Emails in it will be rejected with a `mailboxHasEmail` SetError. If `true`, any Emails that were in the Mailbox will be removed from it, and if in no other Mailboxes, they will be destroyed when the Mailbox is destroyed.
+
+The following extra *SetError* types are defined:
+
+For **destroy**:
+
+- `mailboxHasChild`: The Mailbox still has at least one child Mailbox. The
+  client MUST remove these before it can delete the parent Mailbox.
+- `mailboxHasEmail`: The Mailbox has at least one Email assigned to it, and
+  the *onDestroyRemoveEmails* argument was `false`.
+
+## Example
+
+Fetching all Mailboxes in an account:
+
+    [[ "Mailbox/get", {
+      "accountId": "u33084183",
+      "ids": null
+    }, "0" ]]
+
+And the response:
+
+    [[ "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" ]]
+
+Now suppose an Email is marked read, and we get a push update that the Mailbox state has changed. You might fetch the updates like this:
+
+    [[ "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" ]]
+
+This fetches the list of ids for created/updated/destroyed Mailboxes, then using back-references, it fetches the data for just the created/updated Mailboxes in the same request. The response may look something like this:
+
+    [[ "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" ]]
+
+Here's an example where we try to rename one Mailbox and destroy another:
+
+    [[ "Mailbox/set", {
+      "accountId": "u33084183",
+      "ifInState": "78542",
+      "update": {
+        "MB674cc24095db49ce": {
+          "name": "Maybe important mail"
+        }
+      },
+      "destroy": [ "MB23cfa8094c0f41e6" ]
+    }, "0" ]]
+
+Suppose the rename succeeds, but we don't have permission to destroy the Mailbox we tried to destroy; we might get back:
+
+    [[ "Mailbox/set", {
+      "accountId": "u33084183",
+      "oldState": "78542",
+      "newState": "78549",
+      "updated": {
+          "MB674cc24095db49ce": null
+      },
+      "notDestroyed": {
+        "MB23cfa8094c0f41e6": {
+          "type": "forbidden"
+        }
+      }
+    }, "0" ]]
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/message.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/message.mdown
new file mode 100644
index 0000000..285fc80
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/message.mdown
@@ -0,0 +1,1315 @@
+# Emails
+
+An **Email** object is a representation of a message [@!RFC5322], which allows clients to avoid the complexities of MIME parsing, transfer encoding, and character encoding.
+
+## Properties of the Email Object
+
+Broadly, a message consists of two parts: a list of header fields and then a body. The Email data type provides a way to access the full structure or to use simplified properties and avoid some complexity if this is sufficient for the client application.
+
+While raw headers can be fetched and set, the vast majority of clients should use an appropriate parsed form for each of the header fields it wants to process, as this allows it to avoid the complexities of various encodings that are required in a valid message per RFC 5322.
+
+The body of a message is normally a MIME-encoded set of documents in a tree structure. This may be arbitrarily nested, but the majority of email clients present a flat model of a message body (normally plaintext or HTML) with a set of attachments. Flattening the MIME structure to form this model can be difficult and causes inconsistency between clients. Therefore, in addition to the *bodyStructure* property, which gives the full tree, the Email object contains 3 alternate properties with [...]
+
+- *textBody*/*htmlBody*: These provide a list of parts that should be
+  rendered sequentially as the "body" of the message. This is a list rather than a single part as messages may have headers and/or footers appended/prepended as separate parts when they are transmitted, and some clients send text and images intended to be displayed inline in the body (or even videos and sound clips) as multiple parts rather than a single HTML part with referenced images.
+
+  Because MIME allows for multiple representations of the same data (using `multipart/alternative`), there is a textBody property (which prefers a plaintext representation) and an htmlBody property (which prefers an HTML representation) to accommodate the two most common client requirements. The same part may appear in both lists where there is no alternative between the two.
+
+- *attachments*: This provides a list of parts that should be presented as
+  "attachments" to the message. Some images may be solely there for embedding within an HTML body part; clients may wish to not present these as attachments in the user interface if they are displaying the HTML with the embedded images directly. Some parts may also be in htmlBody/textBody; again, clients may wish to not present these as attachments in the user interface if rendered as part of the body.
+
+The *bodyValues* property allows for clients to fetch the value of text parts directly without having to do a second request for the blob and to have the server handle decoding the charset into unicode. This data is in a separate property rather than on the EmailBodyPart object to avoid duplication of large amounts of data, as the same part may be included twice if the client fetches more than one of bodyStructure, textBody, and htmlBody.
+
+In the following subsections, the common notational convention for wildcards has been adopted for content types, so `foo/*` means any content type that starts with `foo/`.
+
+Due to the number of properties involved, the set of *Email* properties is specified over the following four subsections. This is purely for readability; all properties are top-level peers.
+
+### Metadata
+
+These properties represent metadata about the message in the mail store and are not derived from parsing the message itself.
+
+- **id**: `Id` (immutable; server-set)
+  The id of the Email object. Note that this is the JMAP object id, NOT the Message-ID header field value of the message [@!RFC5322].
+- **blobId**: `Id` (immutable; server-set)
+  The id representing the raw octets of the message [@!RFC5322] for this Email. This may be used to download the raw original message or to attach it directly to another Email, etc.
+- **threadId**: `Id` (immutable; server-set)
+  The id of the Thread to which this Email belongs.
+- **mailboxIds**: `Id[Boolean]`
+  The set of Mailbox ids this Email belongs to. An Email in the mail store MUST belong to one or more Mailboxes at all times (until it is destroyed). The set is represented as an object, with each key being a *Mailbox id*. The value for each key in the object MUST be `true`.
+- **keywords**: `String[Boolean]` (default: \{\})
+  A set of keywords that apply to the Email. The set is represented as an object, with the keys being the *keywords*. The value for each key in the object MUST be `true`.
+
+    Keywords are shared with IMAP. The six system keywords from IMAP get special treatment. The following four keywords have their first character changed from `\` in IMAP to `$` in JMAP and have particular semantic meaning:
+
+    - `$draft`: The Email is a draft the user is composing.
+    - `$seen`: The Email has been read.
+    - `$flagged`: The Email has been flagged for urgent/special attention.
+    - `$answered`: The Email has been replied to.
+
+    The IMAP `\Recent` keyword is not exposed via JMAP. The IMAP `\Deleted` keyword is also not present: IMAP uses a delete+expunge model, which JMAP does not. Any message with the `\Deleted` keyword MUST NOT be visible via JMAP (and so are not counted in the "totalEmails", "unreadEmails", "totalThreads", and "unreadThreads" Mailbox properties).
+
+    Users may add arbitrary keywords to an Email. For compatibility with IMAP, a keyword is a case-insensitive string of 1–255 characters in the ASCII subset %x21–%x7e (excludes control chars and space), and it MUST NOT include any of these characters:
+
+        ( ) { ] % * " \
+
+    Because JSON is case sensitive, servers MUST return keywords in lowercase.
+
+    The [IMAP and JMAP Keywords](https://www.iana.org/assignments/imap-jmap-keywords/) registry as established in [@!RFC5788] assigns semantic meaning to some other keywords in common use. New keywords may be established here in the future. In particular, note:
+
+    - `$forwarded`: The Email has been forwarded.
+    - `$phishing`: The Email is highly likely to be phishing. Clients SHOULD warn users to take care when viewing this Email and disable links and attachments.
+    - `$junk`: The Email is definitely spam. Clients SHOULD set this flag when users report spam to help train automated spam-detection systems.
+    - `$notjunk`: The Email is definitely not spam. Clients SHOULD set this flag when users indicate an Email is legitimate, to help train automated spam-detection systems.
+
+- **size**: `UnsignedInt` (immutable; server-set)
+  The size, in octets, of the raw data for the message [@!RFC5322]  (as referenced by the *blobId*, i.e., the number of octets in the file the user would download).
+- **receivedAt**: `UTCDate` (immutable; default: time of creation on server)
+  The date the Email was received by the message store. This is the *internal date* in IMAP [@?RFC3501].
+
+### Header Fields Parsed Forms
+
+Header field properties are derived from the message header fields [@!RFC5322]  [@!RFC6532]. All header fields may be fetched in a raw form. Some header fields may also be fetched in a parsed form. The structured form that may be fetched depends on the header. The forms are defined in the subsections that follow.
+
+#### Raw
+
+Type: `String`
+
+The raw octets of the header field value from the first octet following the header field name terminating colon, up to but excluding the header field terminating CRLF. Any standards-compliant message MUST be either ASCII (RFC 5322) or UTF-8 (RFC 6532); however, other encodings exist in the wild. A server SHOULD replace any octet or octet run with the high bit set that violates UTF-8 syntax with the unicode replacement character (U+FFFD). Any NUL octet MUST be dropped.
+
+This form will typically have a leading space, as most generated messages
+insert a space after the colon that terminates the header field name.
+
+#### Text
+
+Type: `String`
+
+The header field value with:
+
+1. White space unfolded (as defined in [@!RFC5322], Section 2.2.3).
+2. The terminating CRLF at the end of the value removed.
+3. Any SP characters at the beginning of the value removed.
+4. Any syntactically correct encoded sections [@!RFC2047] with a known
+   character set decoded. Any NUL octets or control characters encoded per [@!RFC2047] are dropped from the decoded value. Any text that looks like syntax per [@!RFC2047] but violates placement or white space rules per [@!RFC2047] MUST NOT be decoded.
+5. The resulting unicode converted to Normalization Form C (NFC) form.
+
+If any decodings fail, the parser SHOULD insert a unicode replacement
+character (U+FFFD) and attempt to continue as much as possible.
+
+To prevent obviously nonsense behaviour, which can lead to interoperability issues, this form may only be fetched or set for the following header fields:
+
+* Subject
+* Comments
+* Keywords
+* List-Id
+* Any header field not defined in [@!RFC5322] or [@!RFC2369]
+
+#### Addresses
+
+Type: `EmailAddress[]`
+
+The header field is parsed as an *address-list* value, as specified in [@!RFC5322], Section 3.4, into the `EmailAddress[]` type. There is an EmailAddress item for each *mailbox* parsed from the *address-list*. Group and comment information is discarded.
+
+An **EmailAddress** object has the following properties:
+
+- **name**: `String|null`
+  The *display-name* of the *mailbox* [@!RFC5322]. If this is a *quoted-string*:
+
+    1. The surrounding DQUOTE characters are removed.
+    2. Any *quoted-pair* is decoded.
+    3. White space is unfolded, and then any leading and trailing white space
+       is removed.
+
+    If there is no *display-name* but there is a *comment* immediately following the *addr-spec*, the value of this SHOULD be used instead. Otherwise, this property is `null`.
+
+- **email**: `String`
+  The *addr-spec* of the *mailbox* [@!RFC5322].
+
+Any syntactically correct encoded sections [@!RFC2047] with a known encoding MUST be decoded, following the same rules as for the *Text* form.
+
+Parsing SHOULD be best effort in the face of invalid structure to
+accommodate invalid messages and semi-complete drafts. EmailAddress objects
+MAY have an *email* property that does not conform to the *addr-spec* form (for example, may not contain an @ symbol).
+
+For example, the following *address-list* string:
+
+    "  James Smythe" <ja...@example.com>, Friends:
+      jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?=
+      <jo...@example.com>;
+
+would be parsed as:
+
+    [
+      { "name": "James Smythe", "email": "james@example.com" },
+      { "name": null, "email": "jane@example.com" },
+      { "name": "John Smîth", "email": "john@example.com" }
+    ]
+
+To prevent obviously nonsense behaviour, which can lead to interoperability issues, this form may only be fetched or set for the following header fields:
+
+* From
+* Sender
+* Reply-To
+* To
+* Cc
+* Bcc
+* Resent-From
+* Resent-Sender
+* Resent-Reply-To
+* Resent-To
+* Resent-Cc
+* Resent-Bcc
+* Any header field not defined in [@!RFC5322] or [@!RFC2369]
+
+#### GroupedAddresses
+
+Type: `EmailAddressGroup[]`
+
+This is similar to the Addresses form but preserves group information. The header field is parsed as an *address-list* value, as specified in [@!RFC5322], Section 3.4, into the `GroupedAddresses[]` type. Consecutive *mailbox* values that are not part of a group are still collected under an EmailAddressGroup object to provide a uniform type.
+
+An **EmailAddressGroup** object has the following properties:
+
+- **name**: `String|null`
+  The *display-name* of the  *group* [@!RFC5322], or `null` if the addresses are not part of a group. If this is a *quoted-string*, it is processed the same as the *name* in the *EmailAddress* type.
+- **addresses**: `EmailAddress[]`
+  The *mailbox* values that belong to this group, represented as EmailAddress
+  objects.
+
+Any syntactically correct encoded sections [@!RFC2047] with a known encoding MUST be decoded, following the same rules as for the *Text* form.
+
+Parsing SHOULD be best effort in the face of invalid structure to
+accommodate invalid messages and semi-complete drafts.
+
+For example, the following *address-list* string:
+
+    "  James Smythe" <ja...@example.com>, Friends:
+      jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?=
+      <jo...@example.com>;
+
+would be parsed as:
+
+    [
+      { "name": null, "addresses": [
+        { "name": "James Smythe", "email": "james@example.com" }
+      ]},
+      { "name": "Friends", "addresses": [
+        { "name": null, "email": "jane@example.com" },
+        { "name": "John Smîth", "email": "john@example.com" }
+      ]}
+    ]
+
+To prevent obviously nonsense behaviour, which can lead to interoperability issues, this form may only be fetched or set for the same header fields as the *Addresses* form.
+
+#### MessageIds
+
+Type: `String[]|null`
+
+The header field is parsed as a list of *msg-id* values, as specified in [@!RFC5322], Section 3.6.4, into the `String[]` type. Comments and/or folding white space (CFWS) and surrounding angle brackets (`<>`) are removed. If parsing fails, the value is `null`.
+
+To prevent obviously nonsense behaviour, which can lead to interoperability issues, this form may only be fetched or set for the following header fields:
+
+* Message-ID
+* In-Reply-To
+* References
+* Resent-Message-ID
+* Any header field not defined in [@!RFC5322] or [@!RFC2369]
+
+#### Date
+
+Type: `Date|null`
+
+The header field is parsed as a *date-time* value, as specified in [@!RFC5322], Section 3.3, into the `Date` type. If parsing fails, the value is `null`.
+
+To prevent obviously nonsense behaviour, which can lead to interoperability issues, this form may only be fetched or set for the following header fields:
+
+* Date
+* Resent-Date
+* Any header field not defined in [@!RFC5322] or [@!RFC2369]
+
+#### URLs
+
+Type: `String[]|null`
+
+The header field is parsed as a list of URLs, as described in [@!RFC2369], into the `String[]` type. Values do not include the surrounding angle brackets or any comments in the header with the URLs. If parsing fails, the value is `null`.
+
+To prevent obviously nonsense behaviour, which can lead to interoperability issues, this form may only be fetched or set for the following header fields:
+
+* List-Help
+* List-Unsubscribe
+* List-Subscribe
+* List-Post
+* List-Owner
+* List-Archive
+* Any header field not defined in [@!RFC5322] or [@!RFC2369]
+
+### Header Fields Properties
+
+The following low-level **Email** property is specified for complete access to the header data of the message:
+
+- **headers**: `EmailHeader[]` (immutable)
+  This is a list of all header fields [@!RFC5322], in the same order they appear in the message. An **EmailHeader** object has the following properties:
+
+    - **name**: `String`
+      The header *field name* as defined in [@!RFC5322], with the same capitalization that it has in the message.
+    - **value**: `String`
+      The header *field value* as defined in [@!RFC5322], in *Raw* form.
+
+In addition, the client may request/send properties representing individual header fields of the form:
+
+    header:{header-field-name}
+
+Where `{header-field-name}` means any series of one or more printable ASCII characters (i.e., characters that have values between 33 and 126, inclusive), except for colon (:). The property may also have the following suffixes:
+
+  - **:as{header-form}**
+    This means the value is in a parsed form, where `{header-form}` is one of the parsed-form names specified above. If not given, the value is in *Raw* form.
+
+  - **:all**
+    This means the value is an array, with the items corresponding to each instance of the header field, in the order they appear in the message. If this suffix is not used, the result is the value of the **last** instance of the header field (i.e., identical to the **last** item in the array if :all is used), or `null` if none.
+
+If both suffixes are used, they MUST be specified in the order above. Header field names are matched case insensitively. The value is typed according to the requested form or to an array of that type if :all is used. If no header fields exist in the message with the requested name, the value is `null` if fetching a single instance or an empty array if requesting :all.
+
+As a simple example, if the client requests a property called `header:subject`, this means find the *last* header field in the message named "subject" (matched case insensitively) and return the value in *Raw* form, or `null` if no header field of this name is found.
+
+For a more complex example, consider the client requesting a property called `header:Resent-To:asAddresses:all`. This means:
+
+1. Find *all* header fields named Resent-To (matched case insensitively).
+2. For each instance, parse the header field value in the *Addresses* form.
+3. The result is of type `EmailAddress[][]` — each item in the array
+   corresponds to the parsed value (which is itself an array) of the Resent-To
+   header field instance.
+
+The following convenience properties are also specified for the **Email** object:
+
+- **messageId**: `String[]|null` (immutable)
+  The value is identical to the value of *header:Message-ID:asMessageIds*. For messages conforming to RFC 5322 this will be an array with a single entry.
+- **inReplyTo**: `String[]|null` (immutable)
+  The value is identical to the value of *header:In-Reply-To:asMessageIds*.
+- **references**: `String[]|null` (immutable)
+  The value is identical to the value of *header:References:asMessageIds*.
+- **sender**: `EmailAddress[]|null` (immutable)
+  The value is identical to the value of *header:Sender:asAddresses*.
+- **from**: `EmailAddress[]|null` (immutable)
+  The value is identical to the value of *header:From:asAddresses*.
+- **to**: `EmailAddress[]|null` (immutable)
+  The value is identical to the value of *header:To:asAddresses*.
+- **cc**: `EmailAddress[]|null` (immutable)
+  The value is identical to the value of *header:Cc:asAddresses*.
+- **bcc**:  `EmailAddress[]|null` (immutable)
+  The value is identical to the value of *header:Bcc:asAddresses*.
+- **replyTo**: `EmailAddress[]|null` (immutable)
+  The value is identical to the value of *header:Reply-To:asAddresses*.
+- **subject**: `String|null` (immutable)
+  The value is identical to the value of *header:Subject:asText*.
+- **sentAt**: `Date|null` (immutable; default on creation: current server time)
+  The value is identical to the value of *header:Date:asDate*.
+
+### Body Parts
+
+These properties are derived from the message body [@!RFC5322] and its MIME entities [@RFC2045].
+
+An **EmailBodyPart** object has the following properties:
+
+- **partId**: `String|null`
+  Identifies this part uniquely within the Email. This is scoped to the *emailId* and has no meaning outside of the JMAP Email object representation. This is `null` if, and only if, the part is of type `multipart/*`.
+- **blobId**: `Id|null`
+  The id representing the raw octets of the contents of the part, after decoding any known *Content-Transfer-Encoding* (as defined in [@!RFC2045]), or `null` if, and only if, the part is of type `multipart/*`. Note that two parts may be transfer-encoded differently but have the same blob id if their decoded octets are identical and the server is using a secure hash of the data for the blob id. If the transfer encoding is unknown, it is treated as though it had no transfer encoding.
+- **size**: `UnsignedInt`
+  The size, in octets, of the raw data after content transfer decoding (as referenced by the *blobId*, i.e., the number of octets in the file the user would download).
+- **headers**: `EmailHeader[]`
+  This is a list of all header fields in the part, in the order they appear in the message. The values are in *Raw* form.
+- **name**: `String|null`
+  This is the decoded *filename* parameter of the *Content-Disposition* header field per [@!RFC2231], or (for compatibility with existing systems) if not present, then it's the decoded *name* parameter of the *Content-Type* header field per [@!RFC2047].
+- **type**: `String`
+  The value of the *Content-Type* header field of the part, if present; otherwise, the implicit type as per the MIME standard (`text/plain` or `message/rfc822` if inside a `multipart/digest`). CFWS is removed and any parameters are stripped.
+- **charset**: `String|null`
+  The value of the charset parameter of the *Content-Type* header field, if present, or `null` if the header field is present but not of type `text/*`. If there is no *Content-Type* header field, or it exists and is of type `text/*` but has no charset parameter, this is the implicit charset as per the MIME standard: `us-ascii`.
+- **disposition**: `String|null`
+  The value of the *Content-Disposition* header field of the part, if present; otherwise, it's `null`. CFWS is removed and any parameters are stripped.
+- **cid**: `String|null`
+  The value of the *Content-Id* header field of the part, if present; otherwise it's `null`. CFWS and surrounding angle brackets (`<>`) are removed. This may be used to reference the content from within a `text/html` body part [HTML](https://www.w3.org/TR/html52/) using the `cid:` protocol, as defined in [@!RFC2392].
+- **language**: `String[]|null`
+  The list of language tags, as defined in [@!RFC3282], in the *Content-Language* header field of the part, if present.
+- **location**: `String|null`
+  The URI, as defined in [@!RFC2557], in the *Content-Location* header field of the part, if present.
+- **subParts**: `EmailBodyPart[]|null`
+  If the type is `multipart/*`, this contains the body parts of each child.
+
+In addition, the client may request/send EmailBodyPart properties representing individual header fields, following the same syntax and semantics as for the Email object, e.g., `header:Content-Type`.
+
+The following **Email** properties are specified for access to the body data of the message:
+
+- **bodyStructure**: `EmailBodyPart` (immutable)
+  This is the full MIME structure of the message body, without recursing into `message/rfc822` or `message/global` parts. Note that EmailBodyParts may have subParts if they are of type `multipart/*`.
+- **bodyValues**: `String[EmailBodyValue]` (immutable)
+  This is a map of *partId* to an **EmailBodyValue** object for none, some, or all `text/*` parts. Which parts are included and whether the value is truncated is determined by various arguments to *Email/get* and *Email/parse*.
+
+    An **EmailBodyValue** object has the following properties:
+
+    * **value**: `String`
+      The value of the body part after decoding *Content-Transfer-Encoding* and
+      the *Content-Type* charset, if both known to the server, and with any CRLF replaced with a single LF. The server MAY use heuristics to determine the charset to use for decoding if the charset is unknown, no charset is given, or it believes the charset given is incorrect. Decoding is best effort; the server SHOULD insert the unicode replacement character (U+FFFD) and continue when a malformed section is encountered.
+
+        Note that due to the charset decoding and line ending normalisation, the length of this string will probably not be exactly the same as the *size* property on the corresponding EmailBodyPart.
+
+    * **isEncodingProblem**: `Boolean` (default: false)
+      This is `true` if malformed sections were found while decoding the charset, or the charset was unknown, or the content-transfer-encoding was unknown.
+    * **isTruncated**: `Boolean` (default: false)
+      This is `true` if the *value* has been truncated.
+
+    See the Security Considerations section for issues related to truncation
+    and heuristic determination of the content-type and charset.
+
+- **textBody**: `EmailBodyPart[]` (immutable)
+  A list of `text/plain`, `text/html`, `image/*`, `audio/*`, and/or `video/*` parts to display (sequentially) as the message body, with a preference for `text/plain` when alternative versions are available.
+- **htmlBody**: `EmailBodyPart[]` (immutable)
+  A list of `text/plain`, `text/html`, `image/*`, `audio/*`, and/or `video/*` parts to display (sequentially) as the message body, with a preference for `text/html` when alternative versions are available.
+- **attachments**: `EmailBodyPart[]` (immutable)
+  A list, traversing depth-first, of all parts in *bodyStructure* that satisfy either of the following conditions:
+
+    - not of type `multipart/*` and not included in *textBody* or *htmlBody*
+    - of type `image/*`, `audio/*`, or `video/*` and not in both *textBody* and
+     *htmlBody*
+
+    None of these parts include subParts, including `message/*` types. Attached messages may be fetched using the Email/parse method and the blobId.
+
+    Note that a `text/html` body part [HTML](https://www.w3.org/TR/html52/) may reference image parts in attachments by using `cid:` links to reference the *Content-Id*, as defined in [@!RFC2392], or by referencing the *Content-Location*.
+- **hasAttachment**: `Boolean` (immutable; server-set)
+  This is `true` if there are one or more parts in the message that a client UI should offer as downloadable. A server SHOULD set hasAttachment to `true` if the *attachments* list contains at least one item that does not have `Content-Disposition: inline`. The server MAY ignore parts in this list that are processed automatically in some way or are referenced as embedded images in one of the `text/html` parts of the message.
+
+    The server MAY set hasAttachment based on implementation-defined or site-configurable heuristics.
+
+- **preview**: `String` (immutable; server-set)
+  A plaintext fragment of the message body. This is intended to be shown as a preview line when listing messages in the mail store and may be truncated when shown. The server may choose which part of the message to include in the preview; skipping quoted sections and salutations and collapsing white space can result in a more useful preview.
+
+    This MUST NOT be more than 256 characters in length.
+
+    As this is derived from the message content by the server, and the algorithm for doing so could change over time, fetching this for an Email a second time MAY return a different result. However, the previous value is not considered incorrect, and the change SHOULD NOT cause the Email object to be considered as changed by the server.
+
+The exact algorithm for decomposing bodyStructure into textBody, htmlBody, and attachments part lists is not mandated, as this is a quality-of-service implementation issue and likely to require workarounds for malformed content discovered over time. However, the following algorithm (expressed here in JavaScript) is suggested as a starting point, based on real-world experience:
+
+    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 );
+
+For instance, consider a message with both text and HTML versions that has
+gone through a list software manager that attaches a header and footer. It might have a MIME structure something like:
+
+    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
+
+In this case, the above algorithm would decompose this to:
+
+    textBody => [ A, B, C, D, K ]
+    htmlBody => [ A, E, K ]
+    attachments => [ C, F, G, H, J ]
+
+
+## Email/get
+
+This is a standard "/get" method as described in [@!RFC8620], Section 5.1, with the following additional request arguments:
+
+- **bodyProperties**: `String[]`
+  A list of properties to fetch for each EmailBodyPart returned. If omitted, this defaults to:
+
+        [ "partId", "blobId", "size", "name", "type", "charset",
+          "disposition", "cid", "language", "location" ]
+
+- **fetchTextBodyValues**: `Boolean` (default: false)
+  If `true`, the *bodyValues* property includes any `text/*` part in the `textBody` property.
+- **fetchHTMLBodyValues**: `Boolean` (default: false)
+  If `true`, the *bodyValues* property includes any `text/*` part in the `htmlBody` property.
+- **fetchAllBodyValues**: `Boolean` (default: false)
+  If `true`, the *bodyValues* property includes any `text/*` part in the `bodyStructure` property.
+- **maxBodyValueBytes**: `UnsignedInt` (default: 0)
+  If greater than zero, the *value* property of any EmailBodyValue object returned in *bodyValues* MUST be truncated if necessary so it does not exceed this number of octets in size. If `0` (the default), no truncation occurs.
+
+    The server MUST ensure the truncation results in valid UTF-8 and does not occur mid-codepoint. If the part is of type `text/html`, the server SHOULD NOT truncate inside an HTML tag, e.g., in the middle of `<a href="https://example.com">`. There is no requirement for the truncated form to be a balanced tree or valid HTML (indeed, the original source may well be neither of these things).
+
+If the standard *properties* argument is omitted or `null`, the following default MUST be used instead of "all" properties:
+
+    [ "id", "blobId", "threadId", "mailboxIds", "keywords", "size",
+    "receivedAt", "messageId", "inReplyTo", "references", "sender", "from",
+    "to", "cc", "bcc", "replyTo", "subject", "sentAt", "hasAttachment",
+    "preview", "bodyValues", "textBody", "htmlBody", "attachments" ]
+
+The following properties are expected to be fast to fetch in a quality implementation:
+
+- id
+- blobId
+- threadId
+- mailboxIds
+- keywords
+- size
+- receivedAt
+- messageId
+- inReplyTo
+- sender
+- from
+- to
+- cc
+- bcc
+- replyTo
+- subject
+- sentAt
+- hasAttachment
+- preview
+
+Clients SHOULD take care when fetching any other properties, as there may be significantly longer latency in fetching and returning the data.
+
+As specified above, parsed forms of headers may only be used on appropriate header fields. Attempting to fetch a form that is forbidden (e.g., `header:From:asDate`) MUST result in the method call being rejected with an `invalidArguments` error.
+
+Where a specific header field is requested as a property, the capitalization of the property name in the response MUST be identical to that used in the request.
+
+### Example
+
+Request:
+
+    [[ "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" ]]
+
+and response:
+
+    [[ "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" ]]
+
+
+## Email/changes
+
+This is a standard "/changes" method as described in [@!RFC8620], Section 5.2. If generating intermediate states for a large set of changes, it is recommended that newer changes be returned first, as these are generally of more interest to users.
+
+## Email/query
+
+This is a standard "/query" method as described in [@!RFC8620], Section 5.5, but with the following additional request arguments:
+
+- **collapseThreads**: `Boolean` (default: false)
+  If `true`, Emails in the same Thread as a previous Email in the list (given the filter and sort order) will be removed from the list. This means only one Email at most will be included in the list for any given Thread.
+
+In quality implementations, the query "total" property is expected to be fast to calculate when the filter consists solely of a single `inMailbox` property, as it is the same as the totalEmails or totalThreads properties (depending on whether collapseThreads is true) of the associated Mailbox object.
+
+### Filtering
+
+A **FilterCondition** object has the following properties, any of which may be omitted:
+
+- **inMailbox**: `Id`
+  A Mailbox id. An Email must be in this Mailbox to match the condition.
+- **inMailboxOtherThan**: `Id[]`
+  A list of Mailbox ids. An Email must be in at least one Mailbox not in this list to match the condition. This is to allow messages solely in trash/spam to be easily excluded from a search.
+- **before**: `UTCDate`
+  The *receivedAt* date-time of the Email must be before this date-time to match the condition.
+- **after**: `UTCDate`
+  The *receivedAt* date-time of the Email must be the same or after this date-time to match the condition.
+- **minSize**: `UnsignedInt`
+  The *size* property of the Email must be equal to or greater than this number to match the condition.
+- **maxSize**: `UnsignedInt`
+  The *size* property of the Email must be less than this number to match the condition.
+- **allInThreadHaveKeyword**: `String`
+  All Emails (including this one) in the same Thread as this Email must have the given keyword to match the condition.
+- **someInThreadHaveKeyword**: `String`
+  At least one Email (possibly this one) in the same Thread as this Email must have the given keyword to match the condition.
+- **noneInThreadHaveKeyword**: `String`
+  All Emails (including this one) in the same Thread as this Email must **not** have the given keyword to match the condition.
+- **hasKeyword**: `String`
+  This Email must have the given keyword to match the condition.
+- **notKeyword**: `String`
+  This Email must not have the given keyword to match the condition.
+- **hasAttachment**: `Boolean`
+  The `hasAttachment` property of the Email must be identical to the value given to match the condition.
+- **text**: `String`
+  Looks for the text in Emails. The server MUST look up text in the *From*, *To*, *Cc*, *Bcc*, and *Subject* header fields of the message and SHOULD look inside any `text/*` or other body parts that may be converted to text by the server. The server MAY extend the search to any additional textual property.
+- **from**: `String`
+  Looks for the text in the *From* header field of the message.
+- **to**: `String`
+  Looks for the text in the *To* header field of the message.
+- **cc**: `String`
+  Looks for the text in the *Cc* header field of the message.
+- **bcc**: `String`
+  Looks for the text in the *Bcc* header field of the message.
+- **subject**: `String`
+  Looks for the text in the *Subject* header field of the message.
+- **body**: `String`
+  Looks for the text in one of the body parts of the message. The server MAY exclude MIME body parts with content media types other than `text/*` and `message/*` from consideration in search matching. Care should be taken to match based on the text content actually presented to an end user by viewers for that media type or otherwise identified as appropriate for search indexing. Matching document metadata uninteresting to an end user (e.g., markup tag and attribute names) is undesirable.
+- **header**: `String[]`
+  The array MUST contain either one or two elements. The first element is the name of the header field to match against. The second (optional) element is the text to look for in the header field value. If not supplied, the message matches simply if it *has* a header field of the given name.
+
+If zero properties are specified on the FilterCondition, the condition MUST always evaluate to `true`. If multiple properties are specified, ALL must apply for the condition to be `true` (it is equivalent to splitting the object into one-property conditions and making them all the child of an AND filter operator).
+
+The exact semantics for matching `String` fields is **deliberately not defined** to allow for flexibility in indexing implementation, subject to the following:
+
+- Any syntactically correct encoded sections [@!RFC2047] of header fields with a known encoding SHOULD be decoded before attempting to match text.
+- When searching inside a `text/html` body part, any text considered markup rather than content SHOULD be ignored, including HTML tags and most attributes, anything inside the `<head>` tag, Cascading Style Sheets (CSS) and JavaScript. Attribute content intended for presentation to the user such as "alt" and "title" SHOULD be considered in the search.
+- Text SHOULD be matched in a case-insensitive manner.
+- Text contained in either (but matched) single (') or double (") quotes SHOULD be treated as a **phrase search**; that is, a match is required for that exact word or sequence of words, excluding the surrounding quotation marks.
+
+    Within a phrase, to match one of the following characters you MUST escape it by prefixing it with a backslash (\\):
+
+        ' " \
+
+- Outside of a phrase, white space SHOULD be treated as dividing separate tokens that may be searched for separately but MUST all be present for the Email to match the filter.
+- Tokens (not part of a phrase) MAY be matched on a whole-word basis using stemming (for example, a text search for "bus" would match "buses" but not "business").
+
+### Sorting
+
+The following value for the *property* field on the Comparator object MUST be supported for sorting:
+
+- **receivedAt** - The *receivedAt* date as returned in the Email object.
+
+The following values for the *property* field on the Comparator object SHOULD be supported for sorting. When specifying a "hasKeyword", "allInThreadHaveKeyword", or "someInThreadHaveKeyword" sort, the Comparator object MUST also have a *keyword* property.
+
+- **size** - The *size* as returned in the Email object.
+- **from** – This is taken to be either the *name* property or if `null`/empty, the *email* property of the **first** EmailAddress object in the Email's *from* property. If still none, consider the value to be the empty string.
+- **to** - This is taken to be either the *name* property or if `null`/empty, the *email* property of the **first** EmailAddress object in the Email's *to* property. If still none, consider the value to be the empty string.
+- **subject** - This is taken to be the base subject of the message, as defined in Section 2.1 of [@!RFC5256].
+- **sentAt** - The *sentAt* property on the Email object.
+- **hasKeyword** - This value MUST be considered `true` if the Email has the keyword given as an additional *keyword* property on the *Comparator* object, or `false` otherwise.
+- **allInThreadHaveKeyword** - This value MUST be considered `true` for the Email if **all** of the Emails in the same Thread have the keyword given as an additional *keyword* property on the *Comparator* object.
+- **someInThreadHaveKeyword** - This value MUST be considered `true` for the Email if **any** of the Emails in the same Thread have the keyword given as an additional *keyword* property on the *Comparator* object.
+
+The server MAY support sorting based on other properties as well. A client can discover which properties are supported by inspecting the account's *capabilities* object (see Section 1.3).
+
+Example sort:
+
+    [{
+      "property": "someInThreadHaveKeyword",
+      "keyword": "$flagged",
+      "isAscending": false
+    }, {
+      "property": "subject",
+      "collation": "i;ascii-casemap"
+    }, {
+      "property": "receivedAt",
+      "isAscending": false
+    }]
+
+This would sort Emails in flagged Threads first (the Thread is considered flagged if any Email within it is flagged), in subject order second, and then from newest first for messages with the same subject. If two Emails have identical values for all three properties, then the order is server dependent but must be stable.
+
+### Thread Collapsing
+
+When *collapseThreads* is `true`, then after filtering and sorting the Email list, the list is further winnowed by removing any Emails for a Thread id that has already been seen (when passing through the list sequentially). A Thread will therefore only appear **once** in the result, at the position of the first Email in the list that belongs to the Thread (given the current sort/filter).
+
+## Email/queryChanges
+
+This is a standard "/queryChanges" method as described in [@!RFC8620], Section 5.6, with the following additional request argument:
+
+- **collapseThreads**: `Boolean` (default: false)
+  The *collapseThreads* argument that was used with *Email/query*.
+
+## Email/set
+
+This is a standard "/set" method as described in [@!RFC8620], Section 5.3. The *Email/set* method encompasses:
+
+- Creating a draft
+- Changing the keywords of an Email (e.g., unread/flagged status)
+- Adding/removing an Email to/from Mailboxes (moving a message)
+- Deleting Emails
+
+The format of the keywords/mailboxIds properties means that when updating an Email, you can either replace the entire set of keywords/Mailboxes (by setting the full value of the property) or add/remove individual ones using the JMAP patch syntax (see [@!RFC8620], Section 5.3 for the specification and Section 5.7 for an example).
+
+Due to the format of the Email object, when creating an Email there are a number of ways to specify the same information. To ensure that the message [@!RFC5322] to create is unambiguous, the following constraints apply to Email objects submitted for creation:
+
+- The *headers* property MUST NOT be given on either the top-level Email or an
+  EmailBodyPart — the client must set each header field as an individual
+  property.
+- There MUST NOT be two properties that represent the same header field
+  (e.g., `header:from` and `from`) within the Email or particular EmailBodyPart.
+- Header fields MUST NOT be specified in parsed forms that are forbidden for
+  that particular field.
+- Header fields beginning with `Content-` MUST NOT be specified on the Email
+  object, only on EmailBodyPart objects.
+- If a bodyStructure property is given, there MUST NOT be textBody, htmlBody, or
+  attachments properties.
+- If given, the bodyStructure EmailBodyPart MUST NOT contain a property
+  representing a header field that is already defined on the top-level Email
+  object.
+- If given, textBody MUST contain exactly one body part and it MUST be of type
+  `text/plain`.
+- If given, htmlBody MUST contain exactly one body part and it MUST be of type
+  `text/html`.
+- Within an EmailBodyPart:
+  - The client may specify a partId OR a blobId, but not both. If a partId is
+    given, this partId MUST be present in the bodyValues property.
+  - The charset property MUST be omitted if a partId is given (the part's
+    content is included in bodyValues, and the server may choose any appropriate
+    encoding).
+  - The size property MUST be omitted if a partId is given. If a blobId is
+    given, it may be included but is ignored by the server (the size is
+    actually calculated from the blob content itself).
+  - A `Content-Transfer-Encoding` header field MUST NOT be given.
+- Within an EmailBodyValue object, isEncodingProblem and isTruncated MUST be
+  either `false` or omitted.
+
+Creation attempts that violate any of this SHOULD be rejected with an `invalidProperties` error; however, a server MAY choose to modify the Email (e.g., choose between conflicting headers, use a different content-encoding, etc.) to comply with its requirements instead.
+
+The server MAY also choose to set additional headers. If not included, the server MUST generate and set a `Message-ID` header field in conformance with [@!RFC5322], Section 3.6.4, and a `Date` header field in conformance with Section 3.6.1.
+
+The final message generated may be invalid per RFC 5322. For example, if it is a half-finished draft, the To header field may have a value that does not conform to the required syntax for this header. The message will be checked for strict conformance when submitted for sending (see the EmailSubmission object description).
+
+Destroying an Email removes it from all Mailboxes to which it belonged. To  just delete an Email to trash, simply change the `mailboxIds` property, so it is now in the Mailbox with a *role* property equal to `trash`, and remove all other Mailbox ids.
+
+When emptying the trash, clients SHOULD NOT destroy Emails that are also in a Mailbox other than trash. For those Emails, they SHOULD just remove the trash Mailbox from the Email.
+
+For successfully created Email objects, the *created* response contains the *id*, *blobId*, *threadId*, and *size* properties of the object.
+
+The following extra *SetError* types are defined:
+
+For **create**:
+
+- `blobNotFound`: At least one blob id given for an EmailBodyPart doesn't
+  exist. An extra *notFound* property of type `Id[]` MUST be included in the SetError object containing every *blobId* referenced by an EmailBodyPart that could not be found on the server.
+
+For **create** and **update**:
+
+- `tooManyKeywords`: The change to the Email's keywords would exceed a
+  server-defined maximum.
+- `tooManyMailboxes`: The change to the set of Mailboxes that this Email is in would exceed a server-defined maximum.
+
+## Email/copy
+
+This is a standard "/copy" method as described in [@!RFC8620], Section 5.4, except only the *mailboxIds*, *keywords*, and *receivedAt* properties may be set during the copy. This method cannot modify the message represented by the Email.
+
+The server MAY forbid two Email objects with identical message content [@!RFC5322], or even just with the same Message-ID [@!RFC5322], to coexist within an account; if the target account already has the Email, the copy will be rejected with a standard `alreadyExists` error.
+
+For successfully copied Email objects, the *created* response contains the *id*, *blobId*, *threadId*, and *size* properties of the new object.
+
+
+## Email/import
+
+The *Email/import* method adds messages [@!RFC5322] to the set of Emails in an account. The server MUST support messages with Email Address Internationalization (EAI) headers [@!RFC6532]. The messages must first be uploaded as blobs using the standard upload mechanism. The method takes the following arguments:
+
+- **accountId**: `Id`
+  The id of the account to use.
+- **ifInState**: `String|null`
+  This is a state string as returned by the *Email/get* method. If supplied, the string must match the current state of the account referenced by the accountId; otherwise, the method will be aborted and a `stateMismatch` error returned. If `null`, any changes will be applied to the current state.
+- **emails**: `Id[EmailImport]`
+  A map of creation id (client specified) to EmailImport objects.
+
+An **EmailImport** object has the following properties:
+
+- **blobId**: `Id`
+  The id of the blob containing the raw message [@!RFC5322].
+- **mailboxIds**: `Id[Boolean]`
+  The ids of the Mailboxes to assign this Email to. At least one Mailbox MUST be given.
+- **keywords**: `String[Boolean]` (default: \{\})
+  The keywords to apply to the Email.
+- **receivedAt**: `UTCDate` (default: time of most recent Received header, or time of import on server if none)
+  The *receivedAt* date to set on the Email.
+
+Each Email to import is considered an atomic unit that may succeed or fail individually. Importing successfully creates a new Email object from the data referenced by the blobId and applies the given Mailboxes, keywords, and receivedAt date.
+
+The server MAY forbid two Email objects with the same exact content [@!RFC5322], or even just with the same Message-ID [@!RFC5322], to coexist within an account. In this case, it MUST reject attempts to import an Email considered to be a duplicate with an `alreadyExists` SetError. An *existingId* property of type `Id` MUST be included on the SetError object with the id of the existing Email. If duplicates are allowed, the newly created Email object MUST have a separate id and independent [...]
+
+If the *blobId*, *mailboxIds*, or *keywords* properties are invalid (e.g., missing, wrong type, id not found), the server MUST reject the import with an `invalidProperties` SetError.
+
+If the Email cannot be imported because it would take the account over quota, the import should be rejected with an `overQuota` SetError.
+
+If the blob referenced is not a valid message [@!RFC5322], the server MAY modify the message to fix errors (such as removing NUL octets or fixing invalid headers). If it does this, the *blobId* on the response MUST represent the new representation and therefore be different to the *blobId* on the EmailImport object. Alternatively, the server MAY reject the import with an `invalidEmail` SetError.
+
+The response has the following arguments:
+
+- **accountId**: `Id`
+  The id of the account used for this call.
+- **oldState**: `String|null`
+  The state string that would have been returned by *Email/get* on this account before making the requested changes, or `null` if the server doesn't know what the previous state string was.
+- **newState**: `String`
+  The state string that will now be returned by *Email/get* on this account.
+- **created**: `Id[Email]|null`
+  A map of the creation id to an object containing the *id*, *blobId*, *threadId*, and *size* properties for each successfully imported Email, or `null` if none.
+- **notCreated**: `Id[SetError]|null`
+  A map of the creation id to a SetError object for each Email that failed to be created, or `null` if all successful. The possible errors are defined above.
+
+The following additional errors may be returned instead of the *Email/import* response:
+
+`stateMismatch`: An `ifInState` argument was supplied, and it does not match the current state.
+
+## Email/parse
+
+This method allows you to parse blobs as messages [@!RFC5322] to get Email objects. The server MUST support messages with EAI headers [@!RFC6532]. This can be used to parse and display attached messages without having to import them as top-level Email objects in the mail store in their own right.
+
+The following metadata properties on the Email objects will be `null` if requested:
+
+- id
+- mailboxIds
+- keywords
+- receivedAt
+
+The *threadId* property of the Email MAY be present if the server can calculate which Thread the Email would be assigned to were it to be imported. Otherwise, this too is `null` if fetched.
+
+The *Email/parse* method takes the following arguments:
+
+- **accountId**: `Id`
+  The id of the account to use.
+- **blobIds**: `Id[]`
+  The ids of the blobs to parse.
+- **properties**: `String[]`
+  If supplied, only the properties listed in the array are returned for each Email object. If omitted, defaults to:
+
+      [ "messageId", "inReplyTo", "references", "sender", "from", "to", "cc", "bcc", "replyTo", "subject", "sentAt", "hasAttachment", "preview", "bodyValues", "textBody", "htmlBody", "attachments" ]
+
+- **bodyProperties**: `String[]`
+  A list of properties to fetch for each EmailBodyPart returned. If omitted, defaults to the same value as the Email/get "bodyProperties" default argument.
+- **fetchTextBodyValues**: `Boolean` (default: false)
+  If `true`, the *bodyValues* property includes any `text/*` part in the `textBody` property.
+- **fetchHTMLBodyValues**: `Boolean` (default: false)
+  If `true`, the *bodyValues* property includes any `text/*` part in the `htmlBody` property.
+- **fetchAllBodyValues**: `Boolean` (default: false)
+  If `true`, the *bodyValues* property includes any `text/*` part in the `bodyStructure` property.
+- **maxBodyValueBytes**: `UnsignedInt` (default: 0)
+  If greater than zero, the *value* property of any EmailBodyValue object returned in *bodyValues* MUST be truncated if necessary so it does not exceed this number of octets in size. If `0` (the default), no truncation occurs.
+
+    The server MUST ensure the truncation results in valid UTF-8 and does not occur mid-codepoint. If the part is of type `text/html`, the server SHOULD NOT truncate inside an HTML tag, e.g., in the middle of `<a href="https://example.com">`. There is no requirement for the truncated form to be a balanced tree or valid HTML (indeed, the original source may well be neither of these things).
+
+The response has the following arguments:
+
+- **accountId**: `Id`
+  The id of the account used for the call.
+- **parsed**: `Id[Email]|null`
+  A map of blob id to parsed Email representation for each successfully parsed blob, or `null` if none.
+- **notParsable**: `Id[]|null`
+  A list of ids given that corresponded to blobs that could not be parsed as Emails, or `null` if none.
+- **notFound**: `Id[]|null`
+  A list of blob ids given that could not be found, or `null` if none.
+
+As specified above, parsed forms of headers may only be used on appropriate header fields. Attempting to fetch a form that is forbidden (e.g., `header:From:asDate`) MUST result in the method call being rejected with an `invalidArguments` error.
+
+Where a specific header field is requested as a property, the capitalization of the property name in the response MUST be identical to that used in the request.
+
+
+## Examples
+
+A client logs in for the first time. It first fetches the set of Mailboxes. Now it will display the inbox to the user, which we will presume has Mailbox id "fb666a55". The inbox may be (very!) large, but the user's screen is only so big, so the client can just load the Threads it needs to fill the screen and then load in more only when the user scrolls. The client sends this request:
+
+    [[ "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" ]]
+
+Let's break down the 4 method calls to see what they're doing:
+
+"0": This asks the server for the ids of the first 30 Email objects in the inbox, sorted newest first, ignoring Emails from the same Thread as a newer Email in the Mailbox (i.e., it is the first 30 unique Threads).
+
+"1": Now we use a back-reference to fetch the Thread ids for each of these Email ids.
+
+"2": Another back-reference fetches the Thread object for each of these Thread ids.
+
+"3": Finally, we fetch the information we need to display the Mailbox listing (but no more!) for every Email in each of these 30 Threads. The client may aggregate this data for display, for example, by showing the Thread as "flagged" if any of the Emails in it has the `$flagged` keyword.
+
+The response from the server may look something like this:
+
+    [[ "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" ]]
+
+Now, on another device, the user marks the first Email as unread, sending this API request:
+
+    [[ "Email/set", {
+      "accountId": "ue150411c",
+      "update": {
+        "Ma783e5cdf5f2deffbc97930a": {
+          "keywords/$seen": null
+        }
+      }
+    }, "0" ]]
+
+The server applies this and sends the success response:
+
+    [[ "Email/set", {
+      "accountId": "ue150411c",
+      "oldState": "780605",
+      "newState": "780606",
+      "updated": {
+        "Ma783e5cdf5f2deffbc97930a": null
+      },
+      ...
+    }, "0" ]]
+
+The user also deletes a few Emails, and then a new message arrives.
+
+Back on our original machine, we receive a push update that the state string for Email is now "780800". As this does not match the client's current state, it issues a request for the changes:
+
+    [[ "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" ]]
+
+The response:
+
+    [[ "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" ]]
+
+The client can update its local cache of the query results by removing "M9bd17497e2a99cb345fc1d0a" and then splicing in "Me8de6c9f6de198239b982ea2" at position 0. As it does not have the data for this new Email, it will then fetch it (it also could have done this in the same request using back-references).
+
+It knows something has changed about "Ma783e5cdf5f2deffbc97930a", so it will refetch the Mailbox ids and keywords (the only mutable properties) for this Email too.
+
+The user starts composing a new Email. The email is plaintext and the client knows the email in in English so adds this metadata to the body part. The user saves a draft while the composition is still in progress. The client sends:
+
+    [[ "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" ]]
+
+The server creates the message and sends the success response:
+
+    [[ "Email/set", {
+      "accountId": "ue150411c",
+      "oldState": "780823",
+      "newState": "780839",
+      "created": {
+        "k192": {
+          "id": "Mf40b5f831efa7233b9eb1c7f",
+          "blobId": "Gf40b5f831efa7233b9eb1c7f8f97d84eeeee64f7",
+          "threadId": "Td957e72e89f516dc",
+          "size": 359
+        }
+      },
+      ...
+    }, "0" ]]
+
+The message created on the server looks something like this:
+
+    Message-Id: <bb...@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" <jo...@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
+
+The user adds a recipient and converts the message to HTML so they can add formatting, then saves an updated draft:
+
+    [[ "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" ]]
+
+The server creates the new draft, deletes the old one, and sends the success response:
+
+    [[ "Email/set", {
+      "accountId": "ue150411c",
+      "oldState": "780839",
+      "newState": "780842",
+      "created": {
+        "k1546": {
+          "id": "Md45b47b4877521042cec0938",
+          "blobId": "Ge8de6c9f6de198239b982ea214e0f3a704e4af74",
+          "threadId": "Td957e72e89f516dc",
+          "size": 11721
+        }
+      },
+      "destroyed": [ "Mf40b5f831efa7233b9eb1c7f" ],
+      ...
+    }, "0" ]]
+
+The client moves this draft to a different account. The only way to do this is via the `Email/copy` method. It MUST set a new mailboxIds property, since the current value will not be valid Mailbox ids in the destination account:
+
+    [[ "Email/copy", {
+      "fromAccountId": "ue150411c",
+      "accountId": "u6c6c41ac",
+      "create": {
+        "k45": {
+          "id": "Md45b47b4877521042cec0938",
+          "mailboxIds": {
+            "75a4c956": true
+          }
+        }
+      },
+      "onSuccessDestroyOriginal": true
+    }, "0" ]]
+
+The server successfully copies the Email and deletes the original. Due to the implicit call to "Email/set", there are two responses to the single method call, both with the same method call 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" ]]
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
new file mode 100644
index 0000000..71f8718
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
@@ -0,0 +1,300 @@
+# Email Submission
+
+An **EmailSubmission** object represents the submission of an Email for delivery to one or more recipients. It has the following properties:
+
+- **id**: `Id` (immutable; server-set)
+  The id of the EmailSubmission.
+- **identityId**: `Id` (immutable)
+  The id of the Identity to associate with this submission.
+- **emailId**: `Id` (immutable)
+  The id of the Email to send. The Email being sent does not have to be a draft, for example, when "redirecting" an existing Email to a different address.
+- **threadId**: `Id` (immutable; server-set)
+  The Thread id of the Email to send. This is set by the server to the *threadId* property of the Email referenced by the *emailId*.
+- **envelope**: `Envelope|null` (immutable)
+  Information for use when sending via SMTP.
+
+    An **Envelope** object has the following properties:
+
+    - **mailFrom**: `Address`
+      The email address to use as the return address in the SMTP submission, plus any parameters to pass with the MAIL FROM address. The JMAP server MAY allow the address to be the empty string.
+
+        When a JMAP server performs an SMTP message submission, it MAY use the
+        same id string for the ENVID parameter [@!RFC3461] and the
+        EmailSubmission object id. Servers that do this MAY replace a
+        client-provided value for ENVID with a server-provided value.
+
+    - **rcptTo**: `Address[]`
+      The email addresses to send the message to, and any RCPT TO parameters to pass with the recipient.
+
+    An **Address** object has the following properties:
+
+    - **email**: `String`
+      The email address being represented by the object. This is a "Mailbox" as used in the Reverse-path or Forward-path of the MAIL FROM or RCPT TO command in [@!RFC5321].
+    - **parameters**: `Object|null`
+      Any parameters to send with the email address (either mail-parameter or rcpt-parameter as appropriate, as specified in [@!RFC5321]). If supplied, each key in the object is a parameter name, and the value is either the parameter value (type `String`) or `null` if the parameter does not take a value. For both name and value, any xtext or unitext encodings are removed (see [@!RFC3461] and [@!RFC6533]) and JSON string encoding is applied.
+
+    If the *envelope* property is `null` or omitted on creation, the server MUST generate this from the referenced Email as follows:
+
+    - **mailFrom**: The email address in the *Sender* header field, if present;
+      otherwise, it's the email address in the *From* header field, if present. In either case, no parameters are added.
+
+        If multiple addresses are present in one of these header fields, or there is more than one *Sender*/*From* header field, the server SHOULD reject the EmailSubmission as invalid; otherwise, it MUST take the first address in the last *Sender*/*From* header field.
+
+        If the address found from this is not allowed by the Identity associated with this submission, the *email* property from the Identity MUST be used instead.
+
+    - **rcptTo**: The deduplicated set of email addresses from the *To*, *Cc*,
+      and *Bcc* header fields, if present, with no parameters for any of them.
+
+- **sendAt**: `UTCDate` (immutable; server-set)
+  The date the submission was/will be released for delivery.
+
+    If the client successfully used FUTURERELEASE [@!RFC4865] with the submission, this MUST be the time when the server will release the message; otherwise, it MUST be the time the EmailSubmission was created.
+
+- **undoStatus**: `String`
+  This represents whether the submission may be canceled. This is server set on create and MUST be one of the following values:
+
+    - `pending`: It may be possible to cancel this submission.
+    - `final`: The message has been relayed to at least one recipient in a
+      manner that cannot be recalled. It is no longer possible to cancel this
+      submission.
+    - `canceled`: The submission was canceled and will not be delivered
+      to any recipient.
+
+    On systems that do not support unsending, the value of this property will always be `final`. On systems that do support canceling submission, it will start as `pending` and MAY transition to `final` when the server knows it definitely cannot recall the message, but it MAY just remain `pending`. If in pending state, a client can attempt to cancel the submission by setting this property to `canceled`; if the update succeeds, the submission was successfully canceled, and the message has [...]
+
+- **deliveryStatus**: `String[DeliveryStatus]|null` (server-set)
+  This represents the delivery status for each of the submission's recipients, if known. This property MAY not be supported by all servers, in which case it will remain `null`. Servers that support it SHOULD update the EmailSubmission object each time the status of any of the recipients changes, even if some recipients are still being retried.
+
+    This value is a map from the email address of each recipient to a *DeliveryStatus* object.
+
+    A **DeliveryStatus** object has the following properties:
+
+    - **smtpReply**: `String`
+      The SMTP reply string returned for this recipient when the server last tried to relay the message, or in a later Delivery Status Notification (DSN, as defined in [@!RFC3464]) response for the message. This SHOULD be the response to the RCPT TO stage, unless this was accepted and the message as a whole was rejected at the end of the DATA stage, in which case the DATA stage reply SHOULD be used instead.
+
+        Multi-line SMTP responses should be concatenated to a single string as follows:
+
+        - The hyphen following the SMTP code on all but the last line is
+          replaced with a space.
+        - Any prefix in common with the first line is stripped from lines after
+          the first.
+        - CRLF is replaced by a space.
+
+        For example:
+
+            550-5.7.1 Our system has detected that this message is
+            550 5.7.1 likely spam.
+
+        would become:
+
+            550 5.7.1 Our system has detected that this message is likely spam.
+
+        For messages relayed via an alternative to SMTP, the server MAY generate a synthetic string representing the status instead. If it does this, the string MUST be of the following form:
+
+        - A 3-digit SMTP reply code, as defined in [@!RFC5321], Section 4.2.3.
+        - Then a single space character.
+        - Then an SMTP Enhanced Mail System Status Code as defined in
+          [@!RFC3463], with a registry defined in [@!RFC5248].
+        - Then a single space character.
+        - Then an implementation-specific information string with a
+          human-readable explanation of the response.
+
+    - **delivered**: `String`
+      Represents whether the message has been successfully delivered to the recipient. This MUST be one of the following values:
+
+        - `queued`: The message is in a local mail queue and status will change
+          once it exits the local mail queues. The *smtpReply* property may still change.
+        - `yes`: The message was successfully delivered to the mail store of the
+          recipient. The *smtpReply* property is final.
+        - `no`: Delivery to the recipient permanently failed. The *smtpReply*
+          property is final.
+        - `unknown`: The final delivery status is unknown, (e.g., it was relayed
+          to an external machine and no further information is available). The *smtpReply* property may still change if a DSN arrives.
+
+        Note that successful relaying to an external SMTP server SHOULD NOT be taken as an indication that the message has successfully reached the final mail store. In this case though, the server may receive a DSN response, if requested.
+
+        If a DSN is received for the recipient with Action equal to "delivered", as per [@!RFC3464], Section 2.3.3, then the *delivered* property SHOULD be set to `yes`; if the Action equals "failed", the property SHOULD be set to `no`. Receipt of any other DSN SHOULD NOT affect this property.
+
+        The server MAY also set this property based on other feedback channels.
+
+    - **displayed**: `String`
+      Represents whether the message has been displayed to the recipient. This MUST be one of the following values:
+
+        - `unknown`: The display status is unknown. This is the initial value.
+        - `yes`: The recipient's system claims the message content has been
+          displayed to the recipient. Note that there is no guarantee that the recipient has noticed, read, or understood the content.
+
+        If a Message Disposition Notification (MDN) is received for this recipient with Disposition-Type (as per [@!RFC8098], Section 3.2.6.2) equal to "displayed", this property SHOULD be set to `yes`.
+
+        The server MAY also set this property based on other feedback channels.
+
+- **dsnBlobIds**: `Id[]` (server-set)
+  A list of blob ids for DSNs [@!RFC3464] received for this submission, in order of receipt, oldest first. The blob is the whole MIME message (with a top-level content-type of `multipart/report`), as received.
+- **mdnBlobIds**: `Id[]` (server-set)
+  A list of blob ids for MDNs [@!RFC8098] received for this submission, in order of receipt, oldest first. The blob is the whole MIME message (with a top-level content-type of `multipart/report`), as received.
+
+JMAP servers MAY choose not to expose DSN and MDN responses as Email objects if they correlate to an EmailSubmission object. It SHOULD only do this if it exposes them in the *dsnBlobIds* and *mdnblobIds* fields instead, and it expects the user to be using clients capable of fetching and displaying delivery status via the EmailSubmission object.
+
+For efficiency, a server MAY destroy EmailSubmission objects at any time after the message is successfully sent or after it has finished retrying to send the message. For very basic SMTP proxies, this MAY be immediately after creation, as it has no way to assign a real id and return the information again if fetched later.
+
+The following JMAP methods are supported.
+
+## EmailSubmission/get
+
+This is a standard "/get" method as described in [@!RFC8620], Section 5.1.
+
+## EmailSubmission/changes
+
+This is a standard "/changes" method as described in [@!RFC8620], Section 5.2.
+
+## EmailSubmission/query
+
+This is a standard "/query" method as described in [@!RFC8620], Section 5.5.
+
+A **FilterCondition** object has the following properties, any of which may be omitted:
+
+- **identityIds**: `Id[]`
+  The EmailSubmission *identityId* property must be in this list to match the
+  condition.
+- **emailIds**: `Id[]`
+  The EmailSubmission *emailId* property must be in this list to match the
+  condition.
+- **threadIds**: `Id[]`
+  The EmailSubmission *threadId* property must be in this list to match the
+  condition.
+- **undoStatus**: `String`
+  The EmailSubmission *undoStatus* property must be identical to the value given to match the condition.
+- **before**: `UTCDate`
+  The *sendAt* property of the EmailSubmission object must be before this date-time to match the condition.
+- **after**: `UTCDate`
+  The *sendAt* property of the EmailSubmission object must be the same as or after this date-time to match the condition.
+
+An EmailSubmission object matches the FilterCondition if and only if all of the given conditions match. If zero properties are specified, it is automatically `true` for all objects.
+
+The following EmailSubmission properties MUST be supported for sorting:
+
+- `emailId`
+- `threadId`
+- `sentAt`
+
+## EmailSubmission/queryChanges
+
+This is a standard "/queryChanges" method as described in [@!RFC8620], Section 5.6.
+
+## EmailSubmission/set
+
+This is a standard "/set" method as described in [@!RFC8620], Section 5.3 with the following two additional request arguments:
+
+- **onSuccessUpdateEmail**: `Id[PatchObject]|null`
+  A map of *EmailSubmission id* to an object containing properties to update on the Email object referenced by the EmailSubmission if the create/update/destroy succeeds. (For references to EmailSubmissions created in the same "/set" invocation, this is equivalent to a creation-reference, so the id will be the creation id prefixed with a `#`.)
+- **onSuccessDestroyEmail**: `Id[]|null`
+  A list of *EmailSubmission ids* for which the Email with the corresponding emailId should be destroyed if the create/update/destroy succeeds. (For references to EmailSubmission creations, this is equivalent to a creation-reference so the id will be the creation id prefixed with a `#`.)
+
+After all create/update/destroy items in the *EmailSubmission/set* invocation  have been processed, a single implicit *Email/set* call MUST be made to perform any changes requested in these two arguments. The response to this MUST be returned after the *EmailSubmission/set* response.
+
+An Email is sent by creating an EmailSubmission object. When processing each create, the server must check that the message is valid, and the user has sufficient authorisation to send it. If the creation succeeds, the message will be sent to the recipients given in the envelope *rcptTo* parameter. The server MUST remove any *Bcc* header field present on the message during delivery. The server MAY add or remove other header fields from the submitted message or make further alterations in  [...]
+
+If the referenced Email is destroyed at any point after the EmailSubmission object is created, this MUST NOT change the behaviour of the submission (i.e., it does not cancel a future send). The *emailId* and *threadId* properties of the EmailSubmission object remain, but trying to fetch them (with a standard Email/get call) will return a `notFound` error if the corresponding objects have been destroyed.
+
+Similarly, destroying an EmailSubmission object MUST NOT affect the deliveries it represents. It purely removes the record of the submission. The server MAY automatically destroy EmailSubmission objects after some time or in response to other triggers, and MAY forbid the client from manually destroying EmailSubmission objects.
+
+If the message to be sent is larger than the server supports sending, a standard `tooLarge` SetError MUST be returned. A *maxSize* `UnsignedInt` property MUST be present on the SetError specifying the maximum size of an message that may be sent, in octets.
+
+If the Email or Identity id given cannot be found, the submission creation is rejected with a standard `invalidProperties` SetError.
+
+The following extra *SetError* types are defined:
+
+For **create**:
+
+- `invalidEmail` - The Email to be sent is invalid in some way. The
+  SetError SHOULD contain a property called *properties* of type `String[]` that lists **all** the properties of the Email that were invalid.
+- `tooManyRecipients` - The envelope (supplied or generated) has more
+  recipients than the server allows. A *maxRecipients* `UnsignedInt` property MUST also be present on the SetError specifying the maximum number of allowed recipients.
+- `noRecipients` – The envelope (supplied or generated) does not have any
+  rcptTo email addresses.
+- `invalidRecipients` – The *rcptTo* property of the envelope (supplied or
+  generated) contains at least one rcptTo value which is not a valid email address for sending to. An *invalidRecipients* `String[]` property MUST also be present on the SetError, which is a list of the invalid addresses.
+- `forbiddenMailFrom` – The server does not permit the user to send a message
+  with this envelope From address [@!RFC5321].
+- `forbiddenFrom` – The server does not permit the user to send a message
+  with the From header field [@!RFC5322] of the message to be sent.
+- `forbiddenToSend` – The user does not have permission to send at all right
+  now for some reason. A *description* `String` property MAY be present on the SetError object to display to the user why they are not permitted.
+
+For **update**:
+
+- `cannotUnsend` – The client attempted to update the *undoStatus* of a valid
+  EmailSubmission object from `pending` to `canceled`, but the message cannot be unsent.
+
+
+### Example
+
+The following example presumes a draft of the Email to be sent has already been saved, and its Email id is "M7f6ed5bcfd7e2604d1753f6c". This call then sends the Email immediately, and if successful, removes the `$draft` flag and moves it from the drafts folder (which has Mailbox id "7cb4e8ee-df87-4757-b9c4-2ea1ca41b38e") to the sent folder (which we presume has 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" ]]
+
+A successful response might look like this. Note that there are two responses due to the implicit Email/set call, but both have the same method call id as they are due to the same call in the request:
+
+    [[ "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" ]]
+
+Suppose instead an admin has removed sending rights for the user, so the submission is rejected with a "forbiddenToSend" error. The description argument of the error is intended for display to the user, so it should be localised appropriately. Let's suppose the request was sent with an Accept-Language header like this:
+
+    Accept-Language: de;q=0.9,en;q=0.8
+
+The server should attempt to choose the best localisation from those it has available based on the Accept-Language header, as described in [@!RFC8620], Section 3.8. If the server has English, French, and German translations, it would choose German as the preferred language and return a response like this:
+
+    [[ "EmailSubmission/set", {
+      "accountId": "ue411d190",
+      "oldState": "012421s6-8nrq-4ps4-n0p4-9330r951ns21",
+      "newState": "012421s6-8nrq-4ps4-n0p4-9330r951ns21",
+      "notCreated": {
+        "k1490": {
+          "type": "forbiddenToSend",
+          "description": "Verzeihung, wegen verdächtiger Aktivitäten Ihres Benutzerkontos haben wir den Versand von Nachrichten gesperrt. Bitte wenden Sie sich für Hilfe an unser Support Team."
+        }
+      }
+    }, "0" ]]
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/searchsnippet.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/searchsnippet.mdown
new file mode 100644
index 0000000..7d83411
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/searchsnippet.mdown
@@ -0,0 +1,94 @@
+# Search Snippets
+
+When doing a search on a `String` property, the client may wish to show the relevant section of the body that matches the search as a preview and to highlight any matching terms in both this and the subject of the Email. Search snippets represent this data.
+
+A **SearchSnippet** object has the following properties:
+
+- **emailId**: `Id`
+  The Email id the snippet applies to.
+- **subject**: `String|null`
+  If text from the filter matches the subject, this is the subject of the Email with the following transformations:
+
+    1. Any instance of the following three characters MUST be replaced by an appropriate HTML entity: & (ampersand), < (less-than sign), and > (greater-than sign) [HTML](https://www.w3.org/TR/html52/). Other characters MAY also be replaced with an HTML entity form.
+
+    2. The matching words/phrases from the filter are wrapped in HTML `<mark></mark>` tags.
+
+    If the subject does not match text from the filter, this property is `null`.
+- **preview**: `String|null`
+  If text from the filter matches the plaintext or HTML body, this is the relevant section of the body (converted to plaintext if originally HTML), with the same transformations as the *subject* property. It MUST NOT be bigger than 255 octets in size. If the body does not contain a match for the text from the filter, this property is `null`.
+
+What is a relevant section of the body for preview is server defined. If the server is unable to determine search snippets, it MUST return `null` for both the *subject* and *preview* properties.
+
+Note that unlike most data types, a SearchSnippet DOES NOT have a property called `id`.
+
+The following JMAP method is supported.
+
+## SearchSnippet/get
+
+To fetch search snippets, make a call to `SearchSnippet/get`. It takes the following arguments:
+
+- **accountId**: `Id`
+  The id of the account to use.
+- **filter**: `FilterOperator|FilterCondition|null`
+  The same filter as passed to Email/query; see the description of this method in Section 4.4 for details.
+- **emailIds**: `Id[]`
+  The ids of the Emails to fetch snippets for.
+
+The response has the following arguments:
+
+- **accountId**: `Id`
+  The id of the account used for the call.
+- **list**: `SearchSnippet[]`
+  An array of SearchSnippet objects for the requested Email ids. This may not be in the same order as the ids that were in the request.
+- **notFound**: `Id[]|null`
+  An array of Email ids requested that could not be found, or `null` if all
+  ids were found.
+
+As the search snippets are derived from the message content and the algorithm for doing so could change over time, fetching the same snippets a second time MAY return a different result. However, the previous value is not considered incorrect, so there is no state string or update mechanism needed.
+
+The following additional errors may be returned instead of the *SearchSnippet/get* response:
+
+`requestTooLarge`: The number of *emailIds* requested by the client exceeds the maximum number the server is willing to process in a single method call.
+
+`unsupportedFilter`: The server is unable to process the given *filter* for any reason.
+
+## Example
+
+Here, we did an Email/query to search for any Email in the account containing the word "foo"; now, we are fetching the search snippets for some of the ids that were returned in the results:
+
+    [[ "SearchSnippet/get", {
+      "accountId": "ue150411c",
+      "filter": {
+        "text": "foo"
+      },
+      "emailIds": [
+        "M44200ec123de277c0c1ce69c",
+        "M7bcbcb0b58d7729686e83d99",
+        "M28d12783a0969584b6deaac0",
+        ...
+      ]
+    }, "0" ]]
+
+Example response:
+
+    [[ "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 &lt;1 widget rather than the complete..."
+      },
+      ...
+      ],
+      "notFound": null
+    }, "0" ]]
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/securityconsiderations.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/securityconsiderations.mdown
new file mode 100644
index 0000000..7173351
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/securityconsiderations.mdown
@@ -0,0 +1,96 @@
+# Security Considerations
+
+All security considerations of JMAP ([@!RFC8620]) apply to this specification. Additional considerations specific to the data types and functionality introduced by this document are described in the following subsections.
+
+## EmailBodyPart Value
+
+Service providers typically perform security filtering on incoming messages, and it's important that the detection of content-type and charset for the security filter aligns with the heuristics performed by JMAP servers. Servers that apply heuristics to determine the content-type or charset for an *EmailBodyValue* SHOULD document the heuristics and provide a mechanism to turn them off in the event they are misaligned with the security filter used at a particular mail host.
+
+Automatic conversion of charsets that allow hidden channels for ASCII text, such as UTF-7, have been problematic for security filters in the past, so server implementations can mitigate this risk by having such conversions  off-by-default and/or separately configurable.
+
+To allow the client to restrict the volume of data it can receive in response to a request, a maximum length may be requested for the data returned for a textual body part. However, truncating the data may change the semantic meaning, for example, truncating a URL changes its location. Servers that scan for links to malicious sites should take care to either ensure truncation is not at a semantically significant point or rescan the truncated value for malicious content before returning it.
+
+## HTML Email Display
+
+HTML message bodies provide richer formatting for messages but present a number of security challenges, especially when embedded in a webmail context in combination with interface HTML. Clients that render HTML messages should carefully consider the potential risks, including:
+
+* Embedded JavaScript can rewrite the message to change its content on
+  subsequent opening, allowing users to be mislead. In webmail systems, if run in the same origin as the interface, it can access and exfiltrate all private data accessible to the user, including all other messages and potentially contacts, calendar events, settings, and credentials. It can also rewrite the interface to undetectably phish passwords. A compromise is likely to be persistent, not just for the duration of page load, due to exfiltration of session credentials or installation  [...]
+* HTML documents may load content directly from the Internet, rather than just
+  referencing attached resources. For example, you may have an `<img>` tag with an external `src` attribute. This may leak to the sender when a message is opened, as well as the IP address of the recipient. Cookies may also be sent and set by the server, allowing tracking between different messages and even website visits and advertising profiles.
+* In webmail systems, CSS can break the layout or create phishing
+  vulnerabilities. For example, the use of `position:fixed` can allow a message to draw content outside of its normal bounds, potentially clickjacking a real interface element.
+* If in a webmail context and not inside a separate frame, any styles defined in
+  CSS rules will apply to interface elements as well if the selector matches, allowing the interface to be modified. Similarly, any interface styles that match elements in the message will alter their appearance, potentially breaking the layout of the message.
+* The link text in HTML has no necessary correlation with the actual target of
+  the link, which can be used to make phishing attacks more convincing.
+* Links opened from a message or embedded external content may leak private info
+  in the `Referer` header sent by default in most systems.
+* Forms can be used to mimic login boxes, providing a potent phishing vector if
+  allowed to submit directly from the message display.
+
+There are a number of ways clients can mitigate these issues, and a defence-in-depth approach that uses a combination of techniques will provide the strongest security.
+
+* HTML can be filtered before rendering, stripping potentially malicious
+  content. Sanitising HTML correctly is tricky, and implementors are strongly recommended to use a well-tested library with a carefully vetted whitelist-only approach. New features with unexpected security characteristics may be added to HTML rendering engines in the future; a blacklist approach is likely to result in security issues.
+
+  Subtle differences in parsing of HTML can introduce security flaws: to filter with 100% accuracy, you need to use the same parser that the HTML rendering engine will use.
+
+* Encapsulating the message in an `<iframe sandbox>`, as defined in
+  [HTML](https://www.w3.org/TR/html52/), Section 4.7.6, can help mitigate a
+  number of risks. This will:
+
+  * Disable JavaScript.
+  * Disable form submission.
+  * Prevent drawing outside of its bounds or conflicts between message CSS and interface CSS.
+  * Establish a unique anonymous origin, separate to the containing origin.
+
+* A strong [Content Security Policy](https://www.w3.org/TR/CSP3/) can, among
+  other things, block JavaScript and loading of external content should it manage to evade the filter.
+
+* The leakage of information in the Referer header can be mitigated with the
+  use of a [referrer policy](https://www.w3.org/TR/referrer-policy/).
+
+* A `crossorigin=anonymous` attribute on tags that load remote content can
+  prevent cookies from being sent.
+
+* If adding `target=_blank` to open links in new tabs, also add `rel=noopener`
+  to ensure the page that opens cannot change the URL in the original tab to
+  redirect the user to a phishing site.
+
+As highly complex software components, HTML rendering engines increase the attack surface of a client considerably, especially when being used to process untrusted, potentially malicious content. Serious bugs have been found in image decoders, JavaScript engines, and HTML parsers in the past, which could lead to full system compromise. Clients using an engine should ensure they get the latest version and continue to incorporate any security patches released by the vendor.
+
+## Multiple Part Display
+
+Messages may consist of multiple parts to be displayed sequentially as a body. Clients MUST render each part in isolation and MUST NOT concatenate the raw text values to render. Doing so may change the overall semantics of the message. If the client or server is decrypting a Pretty Good Privacy (PGP) or S/MIME encrypted part, concatenating with other parts may leak the decrypted text to an attacker, as described in [EFAIL](https://www.usenix.org/system/files/conference/usenixsecurity18/s [...]
+
+## Email Submission
+
+SMTP submission servers [@!RFC6409] use a number of mechanisms to mitigate damage caused by compromised user accounts and end-user systems including rate limiting, anti-virus/anti-spam milters (mail filters), and other technologies. The technologies work better when they have more information about the client connection. If JMAP email submission is implemented as a proxy to an SMTP submission server, it is useful to communicate this information from the JMAP proxy to the submission serve [...]
+
+JMAP servers that proxy to an SMTP submission server SHOULD allow use of the *submissions* port [@!RFC8314]. Implementation of a mechanism similar to SMTP XCLIENT is strongly encouraged. While Simple Authentication and Security Layer (SASL) PLAIN over TLS [@!RFC4616] is presently the mandatory-to-implement mechanism for interoperability with SMTP submission servers [@!RFC4954], a JMAP submission proxy SHOULD implement and prefer a stronger mechanism for this use case such as TLS client c [...]
+
+In the event the JMAP server directly relays mail to SMTP servers in other administrative domains, implementation of the de facto [milter](http://www.postfix.org/MILTER_README.html) protocol is strongly encouraged to integrate with third-party products that address security issues including anti-virus/anti-spam, reputation protection, compliance archiving, and data loss prevention. Proxying to a local SMTP submission server may be a simpler way to provide such security services.
+
+
+## Partial Account Access
+
+A user may only have permission to access a subset of the data that exists in an account. To avoid leaking unauthorised information, in such a situation, the server MUST treat any data the user does not have permission to access the same as if it did not exist.
+
+For example, suppose user A has an account with two Mailboxes, inbox and sent, but only shares the inbox with user B. In this case, when user B fetches Mailboxes for this account, the server MUST behave as though the sent Mailbox did not exist. Similarly, when querying or fetching Email objects, it MUST treat any messages that just belong to the sent Mailbox as though they did not exist. Fetching Thread objects MUST only return ids for Email objects the user has permission to access; if  [...]
+
+If the server forbids a single account from having two identical messages, or two messages with the same `Message-Id` header field, a user with write access can use the error returned by trying to create/import such a message to detect whether it already exists in an inaccessible portion of the account.
+
+## Permission to Send from an Address
+
+In recent years, the email ecosystem has moved towards associating trust with the From address in the message [@!RFC5322], particularly with schemes such as Domain-based Message Authentication, Reporting, and Conformance (DMARC) ([@?RFC7489]).
+
+The set of Identity objects (see Section 6) in an account lets the client know which email addresses the user has permission to send from. Each email submission is associated with an Identity, and servers SHOULD reject submissions where the `From` header field of the message does not correspond to the associated Identity.
+
+The server MAY allow an exception to send an exact copy of an existing message received into the mail store to another address (otherwise known as "redirecting" or "bouncing"), although it is RECOMMENDED the server limit this to destinations the user has verified they also control.
+
+If the user attempts to create a new Identity object, the server MUST reject it with the appropriate error if the user does not have permission to use that email address to send from.
+
+The SMTP MAIL FROM address [@!RFC5321] is often confused with the From message header field [@!RFC5322]. The user generally only ever sees the address in the message header field, and this is the primary one to enforce. However, the server MUST also enforce appropriate restrictions on the MAIL FROM address [@!RFC5321] to stop the user from flooding a third-party address with bounces and non-delivery notices.
+
+The JMAP submission model provides separate errors for impermissible addresses in either context.
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/thread.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/thread.mdown
new file mode 100644
index 0000000..13b9589
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/thread.mdown
@@ -0,0 +1,56 @@
+# Threads
+
+Replies are grouped together with the original message to form a Thread. In JMAP, a Thread is simply a flat list of Emails, ordered by date. Every Email MUST belong to a Thread, even if it is the only Email in the Thread.
+
+The exact algorithm for determining whether two Emails belong to the same Thread is not mandated in this spec to allow for compatibility with different existing systems. For new implementations, it is suggested that two messages belong in the same Thread if both of the following conditions apply:
+
+  1. An identical message id [@!RFC5322] appears in both messages in any of the
+     Message-Id, In-Reply-To, and References header fields.
+  2. After stripping automatically added prefixes such as "Fwd:", "Re:",
+     "[List-Tag]", etc., and ignoring white space, the subjects are the same. This avoids the situation where a person replies to an old message as a convenient way of finding the right recipient to send to but changes the subject and starts a new conversation.
+
+If messages are delivered out of order for some reason, a user may have two Emails in the same Thread but without headers that associate them with each other. The arrival of a third Email may provide the missing references to join them all together into a single Thread. Since the *threadId* of an Email is immutable, if the server wishes to merge the Threads, it MUST handle this by deleting and reinserting (with a new Email id) the Emails that change *threadId*.
+
+A **Thread** object has the following properties:
+
+- **id**: `Id` (immutable; server-set)
+  The id of the Thread.
+- **emailIds**: `Id[]` (server-set)
+  The ids of the Emails in the Thread, sorted by the *receivedAt* date of the Email, oldest first. If two Emails have an identical date, the sort is server dependent but MUST be stable (sorting by id is recommended).
+
+The following JMAP methods are supported.
+
+## Thread/get
+
+This is a standard "/get" method as described in [@!RFC8620], Section 5.1.
+
+### Example
+
+Request:
+
+    [[ "Thread/get", {
+      "accountId": "acme",
+      "ids": ["f123u4", "f41u44"]
+    }, "#1" ]]
+
+with response:
+
+    [[ "Thread/get", {
+      "accountId": "acme",
+      "state": "f6a7e214",
+      "list": [
+        {
+          "id": "f123u4",
+          "emailIds": [ "eaa623", "f782cbb"]
+        },
+        {
+          "id": "f41u44",
+          "emailIds": [ "82cf7bb" ]
+        }
+      ],
+      "notFound": []
+    }, "#1" ]]
+
+## Thread/changes
+
+This is a standard "/changes" method as described in [@!RFC8620], Section 5.2.
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/vacationresponse.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/vacationresponse.mdown
new file mode 100644
index 0000000..8639ea2
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/vacationresponse.mdown
@@ -0,0 +1,34 @@
+# Vacation Response
+
+A vacation response sends an automatic reply when a message is delivered to the mail store, informing the original sender that their message may not be read for some time.
+
+Automated message sending can produce undesirable behaviour. To avoid this, implementors MUST follow the recommendations set forth in [@!RFC3834].
+
+The **VacationResponse** object represents the state of vacation-response-related settings for an account. It has the following properties:
+
+- **id**: `Id` (immutable; server-set)
+  The id of the object. There is only ever one VacationResponse object, and its id is `"singleton"`.
+- **isEnabled**: `Boolean`
+  Should a vacation response be sent if a message arrives between the *fromDate* and *toDate*?
+- **fromDate**: `UTCDate|null`
+  If *isEnabled* is `true`, messages that arrive on or after this date-time (but before the *toDate* if defined) should receive the user's vacation response. If `null`, the vacation response is effective immediately.
+- **toDate**: `UTCDate|null`
+  If *isEnabled* is `true`, messages that arrive before this date-time (but on or after the *fromDate* if defined) should receive the user's vacation response. If `null`, the vacation response is effective indefinitely.
+- **subject**: `String|null`
+  The subject that will be used by the message sent in response to messages when the vacation response is enabled. If `null`, an appropriate subject SHOULD be set by the server.
+- **textBody**: `String|null`
+  The plaintext body to send in response to messages when the vacation response is enabled. If this is `null`, the server SHOULD generate a plaintext body part from the *htmlBody* when sending vacation responses but MAY choose to send the response as HTML only. If both *textBody* and *htmlBody* are `null`, an appropriate default body SHOULD be generated for responses by the server.
+- **htmlBody**: `String|null`
+  The HTML body to send in response to messages when the vacation response is enabled. If this is `null`, the server MAY choose to generate an HTML body part from the *textBody* when sending vacation responses or MAY choose to send the response as plaintext only.
+
+The following JMAP methods are supported.
+
+## VacationResponse/get
+
+This is a standard "/get" method as described in [@!RFC8620], Section 5.1.
+
+There MUST only be exactly one VacationResponse object in an account. It MUST have the id "singleton".
+
+## VacationResponse/set
+
+This is a standard "/set" method as described in [@!RFC8620], Section 5.3.


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 02/08: JAMES-3319 Add a test ensuring deduplication blobStore do not accidentaly delete similar blobs

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 9356dac51a836b13973b1499ffb0194b927186f2
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Jul 28 11:43:21 2020 +0700

    JAMES-3319 Add a test ensuring deduplication blobStore do not accidentaly delete similar blobs
---
 .../queue/rabbitmq/RabbitMQMailQueueTest.java      | 64 +++++++++++++++++++---
 1 file changed, 57 insertions(+), 7 deletions(-)

diff --git a/server/queue/queue-rabbitmq/src/test/java/org/apache/james/queue/rabbitmq/RabbitMQMailQueueTest.java b/server/queue/queue-rabbitmq/src/test/java/org/apache/james/queue/rabbitmq/RabbitMQMailQueueTest.java
index 2da8199..0ae5ebc 100644
--- a/server/queue/queue-rabbitmq/src/test/java/org/apache/james/queue/rabbitmq/RabbitMQMailQueueTest.java
+++ b/server/queue/queue-rabbitmq/src/test/java/org/apache/james/queue/rabbitmq/RabbitMQMailQueueTest.java
@@ -56,6 +56,7 @@ import org.apache.james.blob.cassandra.BlobTables;
 import org.apache.james.blob.cassandra.CassandraBlobModule;
 import org.apache.james.blob.cassandra.CassandraBlobStoreFactory;
 import org.apache.james.blob.mail.MimeMessageStore;
+import org.apache.james.core.builder.MimeMessageBuilder;
 import org.apache.james.eventsourcing.eventstore.cassandra.CassandraEventStoreModule;
 import org.apache.james.metrics.api.Gauge;
 import org.apache.james.queue.api.MailQueue;
@@ -131,7 +132,9 @@ class RabbitMQMailQueueTest {
                 metricTestSystem,
                 RabbitMQMailQueueConfiguration.builder()
                     .sizeMetricsEnabled(true)
-                    .build());
+                    .build(),
+                CassandraBlobStoreFactory.forTesting(cassandra.getConf())
+                    .passthrough());
         }
 
         @Override
@@ -766,7 +769,9 @@ class RabbitMQMailQueueTest {
                 metricTestSystem,
                 RabbitMQMailQueueConfiguration.builder()
                     .sizeMetricsEnabled(false)
-                    .build());
+                    .build(),
+                CassandraBlobStoreFactory.forTesting(cassandra.getConf())
+                    .passthrough());
         }
 
         @Test
@@ -776,11 +781,56 @@ class RabbitMQMailQueueTest {
         }
     }
 
-    private void setUp(CassandraCluster cassandra,
-                       MailQueueMetricExtension.MailQueueMetricTestSystem metricTestSystem,
-                       RabbitMQMailQueueConfiguration configuration) throws Exception {
-        BlobStore blobStore = CassandraBlobStoreFactory.forTesting(cassandra.getConf())
-            .passthrough();
+    @Nested
+    class DeDuplicationTest {
+        @RegisterExtension
+        MailQueueMetricExtension mailQueueMetricExtension = new MailQueueMetricExtension();
+
+        @BeforeEach
+        void setup(CassandraCluster cassandra, MailQueueMetricExtension.MailQueueMetricTestSystem metricTestSystem) throws Exception {
+            setUp(cassandra,
+                metricTestSystem,
+                RabbitMQMailQueueConfiguration.builder()
+                    .sizeMetricsEnabled(true)
+                    .build(),
+                CassandraBlobStoreFactory.forTesting(cassandra.getConf())
+                    .deduplication());
+        }
+
+        @Test
+        void dequeueShouldStillRetrieveAllBlobsWhenIdenticalContentAndDeduplication() throws Exception {
+            Flux<MailQueue.MailQueueItem> dequeueFlux = Flux.from(mailQueue.deQueue());
+            String identicalContent = "identical content";
+            String identicalSubject = "identical subject";
+
+            mailQueue.enQueue(defaultMail()
+                .name("myMail1")
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setSubject(identicalSubject)
+                    .setText(identicalContent))
+                .build());
+            mailQueue.enQueue(defaultMail()
+                .name("myMail2")
+                .mimeMessage(MimeMessageBuilder.mimeMessageBuilder()
+                    .setSubject(identicalSubject)
+                    .setText(identicalContent))
+                .build());
+
+            List<MailQueue.MailQueueItem> items = dequeueFlux.take(2)
+                .concatMap(mailQueueItem -> Mono.fromCallable(() -> {
+                    mailQueueItem.done(true);
+                    return mailQueueItem;
+                }))
+                .collectList()
+                .block(Duration.ofSeconds(10));
+
+            assertThat(items)
+                .allSatisfy(Throwing.consumer(item -> assertThat(item.getMail().getMessage().getContent())
+                    .isEqualTo(identicalContent)));
+        }
+    }
+
+    private void setUp(CassandraCluster cassandra, MailQueueMetricExtension.MailQueueMetricTestSystem metricTestSystem, RabbitMQMailQueueConfiguration configuration, BlobStore blobStore) throws Exception {
         MimeMessageStore.Factory mimeMessageStoreFactory = MimeMessageStore.factory(blobStore);
         clock = new UpdatableTickingClock(IN_SLICE_1);
 


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 06/08: JAMES-3099 Annotate JMAP RFC-8621 mail specifications

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit c9ccd9f0a8b29fdd2da45e52abe342a450e5c74c
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Tue Jul 28 16:34:08 2020 +0700

    JAMES-3099 Annotate JMAP RFC-8621 mail specifications
---
 .../doc/specs/spec/mail/identity.mdown             |  4 ++++
 .../jmap-rfc-8621/doc/specs/spec/mail/intro.mdown  | 28 ++++++++++++++++++++++
 .../doc/specs/spec/mail/mailbox.mdown              | 20 ++++++++++++++++
 .../doc/specs/spec/mail/message.mdown              |  4 ++++
 .../doc/specs/spec/mail/messagesubmission.mdown    |  4 ++++
 .../doc/specs/spec/mail/searchsnippet.mdown        |  4 ++++
 .../jmap-rfc-8621/doc/specs/spec/mail/thread.mdown |  4 ++++
 .../doc/specs/spec/mail/vacationresponse.mdown     |  4 ++++
 8 files changed, 72 insertions(+)

diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/identity.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/identity.mdown
index be3f64b..c1b8431 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/identity.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/identity.mdown
@@ -1,5 +1,9 @@
 # Identities
 
+<aside class="warning">
+  Not implemented yet
+</aside>
+
 An **Identity** object stores information about an email address or domain the user may send from. It has the following properties:
 
 - **id**: `Id` (immutable; server-set)
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/intro.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/intro.mdown
index 006ce4c..05691e3 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/intro.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/intro.mdown
@@ -50,17 +50,33 @@ This document defines three additional capability URIs.
 
 ### urn:ietf:params:jmap:mail
 
+<aside class="notice">
+  Implemented
+</aside>
+
 This represents support for the Mailbox, Thread, Email, and SearchSnippet data types and associated API methods. The value of this property in the JMAP session *capabilities* property is an empty object.
 
 The value of this property in an account's *accountCapabilities* property is an object that MUST contain the following information on server capabilities and permissions for that account:
 
 - **maxMailboxesPerEmail**: `UnsignedInt|null`
+<aside class="warning">
+  Not enforced yet
+</aside>
   The maximum number of Mailboxes (see Section 2) that can be can assigned to a single Email object (see Section 4). This MUST be an integer >= 1, or `null` for no limit (or rather, the limit is always the number of Mailboxes in the account).
 - **maxMailboxDepth**: `UnsignedInt|null`
+<aside class="warning">
+  Not enforced yet
+</aside>
   The maximum depth of the Mailbox hierarchy (i.e., one more than the maximum number of ancestors a Mailbox may have), or `null` for no limit.
 - **maxSizeMailboxName**: `UnsignedInt`
+<aside class="warning">
+  Not enforced yet
+</aside>
   The maximum length, in (UTF-8) octets, allowed for the name of a Mailbox. This MUST be at least 100, although it is recommended servers allow more.
 - **maxSizeAttachmentsPerEmail**: `UnsignedInt`
+<aside class="warning">
+  Not enforced yet
+</aside>
   The maximum total size of attachments, in octets, allowed for a single Email object. A server MAY still reject the import or creation of an Email with a lower attachment size total (for example, if the body includes several megabytes of text, causing the size of the encoded MIME structure to be over some server-defined limit).
 
     Note that this limit is for the sum of unencoded attachment sizes. Users are generally not knowledgeable about encoding overhead, etc., nor should they need to be, so marketing and help materials normally tell them the "max size attachments". This is the unencoded size they see on their hard drive, so this capability matches that and allows the client to consistently enforce what the user understands as the limit.
@@ -75,6 +91,10 @@ The value of this property in an account's *accountCapabilities* property is an
 
 ### urn:ietf:params:jmap:submission
 
+<aside class="warning">
+  Not implemented yet
+</aside>
+
 This represents support for the Identity and EmailSubmission data types and associated API methods. The value of this property in the JMAP session *capabilities* property is an empty object.
 
 The value of this property in an account's *accountCapabilities* property is an object that MUST contain the following information on server capabilities and permissions for that account:
@@ -103,6 +123,10 @@ The value of this property in an account's *accountCapabilities* property is an
 
 ### urn:ietf:params:jmap:vacationresponse
 
+<aside class="warning">
+  Not implemented yet
+</aside>
+
 This represents support for the VacationResponse data type and associated API methods. The value of this property is an empty object in both the JMAP session *capabilities* property and an account's *accountCapabilities* property.
 
 ## Data Type Support in Different Accounts
@@ -111,6 +135,10 @@ The server MUST include the appropriate capability strings as keys in the *accou
 
 ## Push
 
+<aside class="warning">
+  Not implemented yet
+</aside>
+
 Servers MUST support the JMAP push mechanisms, as specified in [@!RFC8620] Section 7, to receive notifications when the state changes for any of the types defined in this specification.
 
 In addition, servers that implement the "urn:ietf:params:jmap:mail" capability MUST support pushing state changes for a type called "EmailDelivery". There are no methods to act on this type; it only exists as part of the push mechanism. The state string for this MUST change whenever a new Email is added to the store, but it SHOULD NOT change upon any other change to the Email objects, for example, if one is marked as read or deleted.
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/mailbox.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/mailbox.mdown
index d144ca6..b92eee3 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/mailbox.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/mailbox.mdown
@@ -5,6 +5,9 @@ A Mailbox represents a named set of Emails. This is the primary mechanism for or
 For compatibility with IMAP, an Email MUST belong to one or more Mailboxes. The Email id does not change if the Email changes Mailboxes.
 
 A **Mailbox** object has the following properties:
+<aside class="notice">
+  Implemented
+</aside>
 
 - **id**: `Id` (immutable; server-set)
   The id of the Mailbox.
@@ -13,6 +16,10 @@ A **Mailbox** object has the following properties:
 - **parentId**: `Id|null` (default: null)
   The Mailbox id for the parent of this Mailbox, or `null` if this Mailbox is at the top level. Mailboxes form acyclic graphs (forests) directed by the child-to-parent relationship. There MUST NOT be a loop.
 - **role**: `String|null` (default: null)
+  <aside class="warning">
+    Partially implemented. Role are based on the name of the mailbox. An evolution would be to use a mailbox annotation to hold its
+    value, removing the link with the name, and allow the user to define locale based system mailboxes. This value cannot be updated.
+  </aside>
   Identifies Mailboxes that have a particular common purpose (e.g., the "inbox"), regardless of the *name* property (which may be localised).
 
     This value is shared with IMAP (exposed in IMAP via the SPECIAL-USE extension [@!RFC6154]). However, unlike in IMAP, a Mailbox MUST only have a single role, and there MUST NOT be two Mailboxes in the same account with the same role. Servers providing IMAP access to the same data are encouraged to enforce these extra restrictions in IMAP as well. Otherwise, modifying the IMAP attributes to ensure compliance when exposing the data over JMAP is implementation dependent.
@@ -22,6 +29,9 @@ A **Mailbox** object has the following properties:
     An account is not required to have Mailboxes with any particular roles.
 
 - **sortOrder**: `UnsignedInt` (default: 0)
+  <aside class="warning">
+    Partially implemented. Based on the role. This value cannot be updated.
+  </aside>
   Defines the sort order of Mailboxes when presented in the client's UI, so it
   is consistent between devices. The number MUST be an integer in the range
   0 <= sortOrder < 2^31.
@@ -33,8 +43,14 @@ A **Mailbox** object has the following properties:
 - **unreadEmails**: `UnsignedInt` (server-set)
   The number of Emails in this Mailbox that have neither the `$seen` keyword nor the `$draft` keyword.
 - **totalThreads**: `UnsignedInt` (server-set)
+  <aside class="warning">
+    Threads are not implemented. So far, each email is its own thread, so totalThreads will have the same value than totalEmails
+  </aside>
   The number of Threads where at least one Email in the Thread is in this Mailbox.
 - **unreadThreads**: `UnsignedInt` (server-set)
+  <aside class="warning">
+    Threads are not implemented. So far, each email is its own thread, so unreadThreads will have the same value than unreadEmails
+  </aside>
   An indication of the number of "unread" Threads in the Mailbox.
 
     For compatibility with existing implementations, the way "unread Threads" is
@@ -84,6 +100,10 @@ The following JMAP methods are supported.
 
 ## Mailbox/get
 
+<aside class="notice">
+  Implemented
+</aside>
+
 This is a standard "/get" method as described in [@!RFC8620], Section 5.1. The *ids* argument may be `null` to fetch all at once.
 
 ## Mailbox/changes
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/message.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/message.mdown
index 285fc80..126f421 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/message.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/message.mdown
@@ -1,5 +1,9 @@
 # Emails
 
+<aside class="warning">
+  Not implemented yet
+</aside>
+
 An **Email** object is a representation of a message [@!RFC5322], which allows clients to avoid the complexities of MIME parsing, transfer encoding, and character encoding.
 
 ## Properties of the Email Object
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
index 71f8718..2609aeb 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
@@ -1,5 +1,9 @@
 # Email Submission
 
+<aside class="warning">
+  Not implemented yet
+</aside>
+
 An **EmailSubmission** object represents the submission of an Email for delivery to one or more recipients. It has the following properties:
 
 - **id**: `Id` (immutable; server-set)
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/searchsnippet.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/searchsnippet.mdown
index 7d83411..299a625 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/searchsnippet.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/searchsnippet.mdown
@@ -1,5 +1,9 @@
 # Search Snippets
 
+<aside class="warning">
+  Not implemented
+</aside>
+
 When doing a search on a `String` property, the client may wish to show the relevant section of the body that matches the search as a preview and to highlight any matching terms in both this and the subject of the Email. Search snippets represent this data.
 
 A **SearchSnippet** object has the following properties:
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/thread.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/thread.mdown
index 13b9589..f32556f 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/thread.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/thread.mdown
@@ -1,5 +1,9 @@
 # Threads
 
+<aside class="warning">
+  Not implemented yet
+</aside>
+
 Replies are grouped together with the original message to form a Thread. In JMAP, a Thread is simply a flat list of Emails, ordered by date. Every Email MUST belong to a Thread, even if it is the only Email in the Thread.
 
 The exact algorithm for determining whether two Emails belong to the same Thread is not mandated in this spec to allow for compatibility with different existing systems. For new implementations, it is suggested that two messages belong in the same Thread if both of the following conditions apply:
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/vacationresponse.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/vacationresponse.mdown
index 8639ea2..bc425f2 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/vacationresponse.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/vacationresponse.mdown
@@ -1,5 +1,9 @@
 # Vacation Response
 
+<aside class="warning">
+  Not implemented yet
+</aside>
+
 A vacation response sends an automatic reply when a message is delivered to the mail store, informing the original sender that their message may not be read for some time.
 
 Automated message sending can produce undesirable behaviour. To avoid this, implementors MUST follow the recommendations set forth in [@!RFC3834].


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 08/08: JAMES-3099 Add a little explanation in the README about annotations

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 30a99e381aac890c4b2efb4a8171c20ab6e06acf
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Tue Jul 28 17:02:27 2020 +0700

    JAMES-3099 Add a little explanation in the README about annotations
---
 server/protocols/jmap-rfc-8621/doc/README.md | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/server/protocols/jmap-rfc-8621/doc/README.md b/server/protocols/jmap-rfc-8621/doc/README.md
index 32ac876..39cba9a 100644
--- a/server/protocols/jmap-rfc-8621/doc/README.md
+++ b/server/protocols/jmap-rfc-8621/doc/README.md
@@ -8,4 +8,10 @@ Officially finalized specifications so far regarding JMAP are:
 * [The core protocol](https://jmap.io/spec-core.html) [[RFC 8620](https://tools.ietf.org/html/rfc8620)]
 * [JMAP Mail](https://jmap.io/spec-mail.html) [[RFC 8621](https://tools.ietf.org/html/rfc8621)]
 
-Annotations aim at tracking implementation progress in James project.
\ No newline at end of file
+Annotations aim at tracking implementation progress in James project.
+
+Annotations are usually represented in the documentation by an `aside` tag. That tag can have two classes:
+
+* `notice` More like an informative annotation, usually used to mark that a feature has been implemented
+* `warning` To get the developer's attention, usually to say that maybe a point or detail of an implemented feature has 
+not been implemented, or partially
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 05/08: JAMES-3099 Add a subsection in config.jmap about JMAP versions in James

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 05d92671d34681a31803173e42c4c632d7f99a63
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Tue Jul 28 14:52:59 2020 +0700

    JAMES-3099 Add a subsection in config.jmap about JMAP versions in James
---
 src/site/xdoc/server/config-jmap.xml | 23 ++++++++++++++++++++++-
 1 file changed, 22 insertions(+), 1 deletion(-)

diff --git a/src/site/xdoc/server/config-jmap.xml b/src/site/xdoc/server/config-jmap.xml
index 5639aa0..b1d28ec 100644
--- a/src/site/xdoc/server/config-jmap.xml
+++ b/src/site/xdoc/server/config-jmap.xml
@@ -59,11 +59,32 @@
                 </dl>
 
             </subsection>
+
             <subsection name="Wire tapping">
-                <p>Enabling <b>TRACE</b> on <b>org.apache.james.jmap.wire</b>enables reactor-netty wiretap, logging of
+                <p>Enabling <b>TRACE</b> on <b>org.apache.james.jmap.wire</b> enables reactor-netty wiretap, logging of
                 all incoming and outgoing requests, outgoing requests. This will log also potentially sensible information
                 like authentication credentials.</p>
             </subsection>
+
+            <subsection name="JMAP-draft vs JMAP-RFC-8621">
+                <p>James had been supporting an implementation based on an early specification of JMAP, what we call here
+                <em>JMAP-draft</em> version. But the protocol went under a lot of changes until its finalization as an
+                official RFC.</p>
+
+                <p>The finalized version of JMAP regarding the core specifications [<a href="https://tools.ietf.org/html/rfc8620">RFC-8620</a>]
+                and the mail specifications [<a href="https://tools.ietf.org/html/rfc8621">RFC-8621</a>] are being
+                currently implemented in James (<em>JMAP-RFC-8621</em> version). It's supposed to replace at term the
+                <em>JMAP-draft</em> version.</p>
+
+                <p>Meanwhile, both versions will be available. The version by default will be <em>JMAP-draft</em> during
+                the time it takes to implement the new version. If you want to use a specific version for a request,
+                you will need to add an extra <em>jmapVersion</em> field in your <b>Accept</b> header of your JMAP request:</p>
+
+                <ul>
+                    <li><b>JMAP-draft</b>: <em>Accept: application/json; jmapVersion=draft</em></li>
+                    <li><b>JMAP-RFC-8621</b>: <em>Accept: application/json; jmapVersion=rfc-8621</em></li>
+                </ul>
+            </subsection>
         </section>
 
     </body>


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 03/08: JAMES-3319 Mitigate CassandraMailQueueView deletion concurrency

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 2676b9c80085e0a5af98df0cedc399daf99054e1
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Jul 28 15:11:07 2020 +0700

    JAMES-3319 Mitigate CassandraMailQueueView deletion concurrency
---
 .../james/queue/rabbitmq/view/cassandra/CassandraMailQueueView.java   | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/view/cassandra/CassandraMailQueueView.java b/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/view/cassandra/CassandraMailQueueView.java
index 4a9bad8..754294f 100644
--- a/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/view/cassandra/CassandraMailQueueView.java
+++ b/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/view/cassandra/CassandraMailQueueView.java
@@ -73,6 +73,8 @@ public class CassandraMailQueueView implements MailQueueView<CassandraMailQueueB
         }
     }
 
+    private static final int DELETION_CONCURRENCY = 8;
+
     private final CassandraMailQueueMailStore storeHelper;
     private final CassandraMailQueueBrowser cassandraMailQueueBrowser;
     private final CassandraMailQueueMailDelete cassandraMailQueueMailDelete;
@@ -144,7 +146,7 @@ public class CassandraMailQueueView implements MailQueueView<CassandraMailQueueB
             .map(EnqueuedItemWithSlicingContext::getEnqueuedItem)
             .filter(deleteCondition::shouldBeDeleted)
             .flatMap(mailReference -> cassandraMailQueueMailDelete.considerDeleted(mailReference.getEnqueueId(), mailQueueName)
-                .then(Mono.from(mimeMessageStore.delete(mailReference.getPartsId()))))
+                .then(Mono.from(mimeMessageStore.delete(mailReference.getPartsId()))), DELETION_CONCURRENCY)
             .count()
             .doOnNext(ignored -> cassandraMailQueueMailDelete.updateBrowseStart(mailQueueName))
             .subscribeOn(Schedulers.elastic())


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 01/08: JAMES-3319 Effective blob deletion for RabbitMQMailQueue

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit d69b48f1276910d8e3cdf13e677fcafc818b65f7
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Jul 27 16:14:03 2020 +0700

    JAMES-3319 Effective blob deletion for RabbitMQMailQueue
---
 .../org/apache/james/queue/rabbitmq/Dequeuer.java  |  2 +-
 .../apache/james/queue/rabbitmq/MailLoader.java    |  5 +-
 .../james/queue/rabbitmq/MailWithEnqueueId.java    |  9 ++-
 .../queue/rabbitmq/view/api/DeleteCondition.java   | 13 +++-
 .../view/cassandra/CassandraMailQueueView.java     | 25 +++++--
 .../queue/rabbitmq/RabbitMQMailQueueTest.java      | 77 ++++++++++++++++++++++
 .../rabbitmq/view/api/DeleteConditionTest.java     |  6 +-
 .../CassandraMailQueueViewTestFactory.java         |  4 +-
 8 files changed, 123 insertions(+), 18 deletions(-)

diff --git a/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/Dequeuer.java b/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/Dequeuer.java
index b477335..0241c46 100644
--- a/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/Dequeuer.java
+++ b/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/Dequeuer.java
@@ -132,7 +132,7 @@ class Dequeuer implements Closeable {
             if (success) {
                 dequeueMetric.increment();
                 response.ack();
-                mailQueueView.delete(DeleteCondition.withEnqueueId(mailWithEnqueueId.getEnqueueId()));
+                mailQueueView.delete(DeleteCondition.withEnqueueId(mailWithEnqueueId.getEnqueueId(), mailWithEnqueueId.getBlobIds()));
             } else {
                 response.nack(REQUEUE);
             }
diff --git a/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/MailLoader.java b/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/MailLoader.java
index b724e60..ffe765b 100644
--- a/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/MailLoader.java
+++ b/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/MailLoader.java
@@ -47,7 +47,10 @@ class MailLoader {
     Mono<MailWithEnqueueId> load(MailReferenceDTO dto) {
         return Mono.fromCallable(() -> dto.toMailReference(blobIdFactory))
             .flatMap(mailReference -> buildMail(mailReference)
-                .map(mail -> new MailWithEnqueueId(mailReference.getEnqueueId(), mail)));
+                .map(mail -> new MailWithEnqueueId(
+                    mailReference.getEnqueueId(),
+                    mail,
+                    mailReference.getPartsId())));
     }
 
     private Mono<Mail> buildMail(MailReference mailReference) {
diff --git a/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/MailWithEnqueueId.java b/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/MailWithEnqueueId.java
index f256fbf..a8edc9c 100644
--- a/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/MailWithEnqueueId.java
+++ b/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/MailWithEnqueueId.java
@@ -19,15 +19,18 @@
 
 package org.apache.james.queue.rabbitmq;
 
+import org.apache.james.blob.mail.MimeMessagePartsId;
 import org.apache.mailet.Mail;
 
 public class MailWithEnqueueId {
     private final EnqueueId enqueueId;
     private final Mail mail;
+    private final MimeMessagePartsId blobIds;
 
-    MailWithEnqueueId(EnqueueId enqueueId, Mail mail) {
+    MailWithEnqueueId(EnqueueId enqueueId, Mail mail, MimeMessagePartsId blobIds) {
         this.enqueueId = enqueueId;
         this.mail = mail;
+        this.blobIds = blobIds;
     }
 
     public EnqueueId getEnqueueId() {
@@ -37,4 +40,8 @@ public class MailWithEnqueueId {
     public Mail getMail() {
         return mail;
     }
+
+    public MimeMessagePartsId getBlobIds() {
+        return blobIds;
+    }
 }
diff --git a/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/view/api/DeleteCondition.java b/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/view/api/DeleteCondition.java
index 12aba7b..d0e8d04 100644
--- a/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/view/api/DeleteCondition.java
+++ b/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/view/api/DeleteCondition.java
@@ -22,6 +22,7 @@ package org.apache.james.queue.rabbitmq.view.api;
 import java.util.Objects;
 
 import org.apache.commons.lang3.NotImplementedException;
+import org.apache.james.blob.mail.MimeMessagePartsId;
 import org.apache.james.queue.api.ManageableMailQueue;
 import org.apache.james.queue.rabbitmq.EnqueueId;
 import org.apache.james.queue.rabbitmq.EnqueuedItem;
@@ -114,15 +115,21 @@ public interface DeleteCondition {
 
     class WithEnqueueId implements DeleteCondition {
         private final EnqueueId enqueueId;
+        private final MimeMessagePartsId blobIds;
 
-        WithEnqueueId(EnqueueId enqueueId) {
+        WithEnqueueId(EnqueueId enqueueId, MimeMessagePartsId blobIds) {
             this.enqueueId = enqueueId;
+            this.blobIds = blobIds;
         }
 
         public EnqueueId getEnqueueId() {
             return enqueueId;
         }
 
+        public MimeMessagePartsId getBlobIds() {
+            return blobIds;
+        }
+
         @Override
         public boolean shouldBeDeleted(EnqueuedItem enqueuedItem) {
             Preconditions.checkNotNull(enqueuedItem);
@@ -191,9 +198,9 @@ public interface DeleteCondition {
         return new WithName(value);
     }
 
-    static WithEnqueueId withEnqueueId(EnqueueId value) {
+    static WithEnqueueId withEnqueueId(EnqueueId value, MimeMessagePartsId blobIds) {
         Preconditions.checkNotNull(value);
-        return new WithEnqueueId(value);
+        return new WithEnqueueId(value, blobIds);
     }
 
     static DeleteCondition all() {
diff --git a/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/view/cassandra/CassandraMailQueueView.java b/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/view/cassandra/CassandraMailQueueView.java
index 72b26a3..4a9bad8 100644
--- a/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/view/cassandra/CassandraMailQueueView.java
+++ b/server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/view/cassandra/CassandraMailQueueView.java
@@ -24,7 +24,11 @@ import static org.apache.james.util.FunctionalUtils.negate;
 import java.time.Instant;
 
 import javax.inject.Inject;
+import javax.mail.internet.MimeMessage;
 
+import org.apache.james.blob.api.Store;
+import org.apache.james.blob.mail.MimeMessagePartsId;
+import org.apache.james.blob.mail.MimeMessageStore;
 import org.apache.james.queue.api.ManageableMailQueue;
 import org.apache.james.queue.rabbitmq.EnqueueId;
 import org.apache.james.queue.rabbitmq.EnqueuedItem;
@@ -45,40 +49,46 @@ public class CassandraMailQueueView implements MailQueueView<CassandraMailQueueB
         private final CassandraMailQueueMailStore storeHelper;
         private final CassandraMailQueueBrowser cassandraMailQueueBrowser;
         private final CassandraMailQueueMailDelete cassandraMailQueueMailDelete;
+        private final MimeMessageStore.Factory mimeMessageStoreFactory;
 
         @Inject
         public Factory(CassandraMailQueueMailStore storeHelper,
                        CassandraMailQueueBrowser cassandraMailQueueBrowser,
                        CassandraMailQueueMailDelete cassandraMailQueueMailDelete,
                        EventsourcingConfigurationManagement eventsourcingConfigurationManagement,
+                       MimeMessageStore.Factory mimeMessageStoreFactory,
                        CassandraMailQueueViewConfiguration configuration) {
             this.storeHelper = storeHelper;
             this.cassandraMailQueueBrowser = cassandraMailQueueBrowser;
             this.cassandraMailQueueMailDelete = cassandraMailQueueMailDelete;
+            this.mimeMessageStoreFactory = mimeMessageStoreFactory;
 
             eventsourcingConfigurationManagement.registerConfiguration(configuration);
         }
 
         @Override
         public MailQueueView create(MailQueueName mailQueueName) {
-            return new CassandraMailQueueView(storeHelper, mailQueueName, cassandraMailQueueBrowser, cassandraMailQueueMailDelete);
+            return new CassandraMailQueueView(storeHelper, mailQueueName, cassandraMailQueueBrowser, cassandraMailQueueMailDelete,
+                mimeMessageStoreFactory.mimeMessageStore());
         }
     }
 
     private final CassandraMailQueueMailStore storeHelper;
     private final CassandraMailQueueBrowser cassandraMailQueueBrowser;
     private final CassandraMailQueueMailDelete cassandraMailQueueMailDelete;
+    private final Store<MimeMessage, MimeMessagePartsId> mimeMessageStore;
 
     private final MailQueueName mailQueueName;
 
     CassandraMailQueueView(CassandraMailQueueMailStore storeHelper,
                            MailQueueName mailQueueName,
                            CassandraMailQueueBrowser cassandraMailQueueBrowser,
-                           CassandraMailQueueMailDelete cassandraMailQueueMailDelete) {
+                           CassandraMailQueueMailDelete cassandraMailQueueMailDelete, Store<MimeMessage, MimeMessagePartsId> mimeMessageStore) {
         this.mailQueueName = mailQueueName;
         this.storeHelper = storeHelper;
         this.cassandraMailQueueBrowser = cassandraMailQueueBrowser;
         this.cassandraMailQueueMailDelete = cassandraMailQueueMailDelete;
+        this.mimeMessageStore = mimeMessageStore;
     }
 
     @Override
@@ -123,7 +133,7 @@ public class CassandraMailQueueView implements MailQueueView<CassandraMailQueueB
     public long delete(DeleteCondition deleteCondition) {
         if (deleteCondition instanceof DeleteCondition.WithEnqueueId) {
             DeleteCondition.WithEnqueueId enqueueIdCondition = (DeleteCondition.WithEnqueueId) deleteCondition;
-            delete(enqueueIdCondition.getEnqueueId()).block();
+            delete(enqueueIdCondition.getEnqueueId(), enqueueIdCondition.getBlobIds()).block();
             return 1L;
         }
         return browseThenDelete(deleteCondition);
@@ -133,15 +143,18 @@ public class CassandraMailQueueView implements MailQueueView<CassandraMailQueueB
         return cassandraMailQueueBrowser.browseReferences(mailQueueName)
             .map(EnqueuedItemWithSlicingContext::getEnqueuedItem)
             .filter(deleteCondition::shouldBeDeleted)
-            .flatMap(mailReference -> cassandraMailQueueMailDelete.considerDeleted(mailReference.getEnqueueId(), mailQueueName))
+            .flatMap(mailReference -> cassandraMailQueueMailDelete.considerDeleted(mailReference.getEnqueueId(), mailQueueName)
+                .then(Mono.from(mimeMessageStore.delete(mailReference.getPartsId()))))
             .count()
             .doOnNext(ignored -> cassandraMailQueueMailDelete.updateBrowseStart(mailQueueName))
             .subscribeOn(Schedulers.elastic())
             .block();
     }
 
-    private Mono<Void> delete(EnqueueId enqueueId) {
-        return cassandraMailQueueMailDelete.considerDeleted(enqueueId, mailQueueName);
+    private Mono<Void> delete(EnqueueId enqueueId,
+                              MimeMessagePartsId blobIds) {
+        return cassandraMailQueueMailDelete.considerDeleted(enqueueId, mailQueueName)
+            .then(Mono.from(mimeMessageStore.delete(blobIds)));
     }
 
     @Override
diff --git a/server/queue/queue-rabbitmq/src/test/java/org/apache/james/queue/rabbitmq/RabbitMQMailQueueTest.java b/server/queue/queue-rabbitmq/src/test/java/org/apache/james/queue/rabbitmq/RabbitMQMailQueueTest.java
index a8b044d..2da8199 100644
--- a/server/queue/queue-rabbitmq/src/test/java/org/apache/james/queue/rabbitmq/RabbitMQMailQueueTest.java
+++ b/server/queue/queue-rabbitmq/src/test/java/org/apache/james/queue/rabbitmq/RabbitMQMailQueueTest.java
@@ -19,12 +19,16 @@
 
 package org.apache.james.queue.rabbitmq;
 
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
 import static java.time.temporal.ChronoUnit.HOURS;
 import static org.apache.james.backends.cassandra.Scenario.Builder.executeNormally;
 import static org.apache.james.backends.cassandra.Scenario.Builder.fail;
 import static org.apache.james.backends.cassandra.Scenario.Builder.returnEmpty;
 import static org.apache.james.backends.rabbitmq.Constants.EMPTY_ROUTING_KEY;
 import static org.apache.james.queue.api.Mails.defaultMail;
+import static org.apache.james.queue.api.Mails.defaultMailNoRecipient;
+import static org.apache.mailet.base.MailAddressFixture.RECIPIENT1;
+import static org.apache.mailet.base.MailAddressFixture.SENDER;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.mockito.ArgumentMatchers.any;
@@ -48,6 +52,7 @@ import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionModule
 import org.apache.james.backends.rabbitmq.RabbitMQExtension;
 import org.apache.james.blob.api.BlobStore;
 import org.apache.james.blob.api.HashBlobId;
+import org.apache.james.blob.cassandra.BlobTables;
 import org.apache.james.blob.cassandra.CassandraBlobModule;
 import org.apache.james.blob.cassandra.CassandraBlobStoreFactory;
 import org.apache.james.blob.mail.MimeMessageStore;
@@ -175,6 +180,78 @@ class RabbitMQMailQueueTest {
         }
 
         @Test
+        void dequeueShouldDeleteBlobs(CassandraCluster cassandra) throws Exception {
+            String name1 = "myMail1";
+            Flux<MailQueue.MailQueueItem> dequeueFlux = Flux.from(getMailQueue().deQueue());
+            getMailQueue().enQueue(defaultMail()
+                .name(name1)
+                .build());
+
+            dequeueFlux.take(1)
+                .flatMap(mailQueueItem -> Mono.fromCallable(() -> {
+                    mailQueueItem.done(true);
+                    return mailQueueItem;
+                })).blockLast(Duration.ofSeconds(10));
+
+            assertThat(cassandra.getConf().execute(select().from(BlobTables.DefaultBucketBlobTable.TABLE_NAME)))
+                .isEmpty();
+        }
+
+        @Test
+        void clearShouldDeleteBlobs(CassandraCluster cassandra) throws Exception {
+            String name1 = "myMail1";
+            getMailQueue().enQueue(defaultMail()
+                .name(name1)
+                .build());
+
+            getManageableMailQueue().clear();
+
+            assertThat(cassandra.getConf().execute(select().from(BlobTables.DefaultBucketBlobTable.TABLE_NAME)))
+                .isEmpty();
+        }
+
+        @Test
+        void removeByNameShouldDeleteBlobs(CassandraCluster cassandra) throws Exception {
+            String name1 = "myMail1";
+            getMailQueue().enQueue(defaultMail()
+                .name(name1)
+                .build());
+
+            getManageableMailQueue().remove(ManageableMailQueue.Type.Name, name1);
+
+            assertThat(cassandra.getConf().execute(select().from(BlobTables.DefaultBucketBlobTable.TABLE_NAME)))
+                .isEmpty();
+        }
+
+        @Test
+        void removeByRecipientShouldDeleteBlobs(CassandraCluster cassandra) throws Exception {
+            String name1 = "myMail1";
+            getMailQueue().enQueue(defaultMailNoRecipient()
+                .name(name1)
+                .recipient(RECIPIENT1)
+                .build());
+
+            getManageableMailQueue().remove(ManageableMailQueue.Type.Recipient, RECIPIENT1.asString());
+
+            assertThat(cassandra.getConf().execute(select().from(BlobTables.DefaultBucketBlobTable.TABLE_NAME)))
+                .isEmpty();
+        }
+
+        @Test
+        void removeBySenderShouldDeleteBlobs(CassandraCluster cassandra) throws Exception {
+            String name1 = "myMail1";
+            getMailQueue().enQueue(defaultMail()
+                .name(name1)
+                .sender(SENDER)
+                .build());
+
+            getManageableMailQueue().remove(ManageableMailQueue.Type.Sender, SENDER.asString());
+
+            assertThat(cassandra.getConf().execute(select().from(BlobTables.DefaultBucketBlobTable.TABLE_NAME)))
+                .isEmpty();
+        }
+
+        @Test
         void browseAndDequeueShouldCombineWellWhenDifferentSlices() throws Exception {
             ManageableMailQueue mailQueue = getManageableMailQueue();
             int emailCount = 5;
diff --git a/server/queue/queue-rabbitmq/src/test/java/org/apache/james/queue/rabbitmq/view/api/DeleteConditionTest.java b/server/queue/queue-rabbitmq/src/test/java/org/apache/james/queue/rabbitmq/view/api/DeleteConditionTest.java
index ddcd5da..2b800ed 100644
--- a/server/queue/queue-rabbitmq/src/test/java/org/apache/james/queue/rabbitmq/view/api/DeleteConditionTest.java
+++ b/server/queue/queue-rabbitmq/src/test/java/org/apache/james/queue/rabbitmq/view/api/DeleteConditionTest.java
@@ -80,7 +80,7 @@ class DeleteConditionTest {
         @Test
         void withSenderShouldThrowOnNullCondition() {
             assertThatThrownBy(() ->
-                DeleteCondition.withEnqueueId(null))
+                DeleteCondition.withEnqueueId(null, null))
                 .isInstanceOf(NullPointerException.class);
         }
 
@@ -97,7 +97,7 @@ class DeleteConditionTest {
                 .mimeMessagePartsId(MESSAGE_PARTS_ID)
                 .build();
 
-            assertThat(DeleteCondition.withEnqueueId(ENQUEUE_ID_1).shouldBeDeleted(enqueuedItem))
+            assertThat(DeleteCondition.withEnqueueId(ENQUEUE_ID_1, MESSAGE_PARTS_ID).shouldBeDeleted(enqueuedItem))
                 .isTrue();
         }
 
@@ -114,7 +114,7 @@ class DeleteConditionTest {
                 .mimeMessagePartsId(MESSAGE_PARTS_ID)
                 .build();
 
-            assertThat(DeleteCondition.withEnqueueId(ENQUEUE_ID_1).shouldBeDeleted(enqueuedItem))
+            assertThat(DeleteCondition.withEnqueueId(ENQUEUE_ID_1, MESSAGE_PARTS_ID).shouldBeDeleted(enqueuedItem))
                 .isFalse();
         }
     }
diff --git a/server/queue/queue-rabbitmq/src/test/java/org/apache/james/queue/rabbitmq/view/cassandra/CassandraMailQueueViewTestFactory.java b/server/queue/queue-rabbitmq/src/test/java/org/apache/james/queue/rabbitmq/view/cassandra/CassandraMailQueueViewTestFactory.java
index 1caf7aa..0d611a8 100644
--- a/server/queue/queue-rabbitmq/src/test/java/org/apache/james/queue/rabbitmq/view/cassandra/CassandraMailQueueViewTestFactory.java
+++ b/server/queue/queue-rabbitmq/src/test/java/org/apache/james/queue/rabbitmq/view/cassandra/CassandraMailQueueViewTestFactory.java
@@ -25,12 +25,9 @@ import java.util.Optional;
 import org.apache.james.backends.cassandra.init.configuration.CassandraConsistenciesConfiguration;
 import org.apache.james.blob.api.HashBlobId;
 import org.apache.james.blob.mail.MimeMessageStore;
-import org.apache.james.eventsourcing.Event;
 import org.apache.james.eventsourcing.eventstore.cassandra.CassandraEventStore;
 import org.apache.james.eventsourcing.eventstore.cassandra.EventStoreDao;
 import org.apache.james.eventsourcing.eventstore.cassandra.JsonEventSerializer;
-import org.apache.james.eventsourcing.eventstore.cassandra.dto.EventDTO;
-import org.apache.james.eventsourcing.eventstore.cassandra.dto.EventDTOModule;
 import org.apache.james.queue.rabbitmq.MailQueueName;
 import org.apache.james.queue.rabbitmq.view.cassandra.configuration.CassandraMailQueueViewConfiguration;
 import org.apache.james.queue.rabbitmq.view.cassandra.configuration.CassandraMailQueueViewConfigurationModule;
@@ -66,6 +63,7 @@ public class CassandraMailQueueViewTestFactory {
             cassandraMailQueueBrowser,
             cassandraMailQueueMailDelete,
             eventsourcingConfigurationManagement,
+            mimeMessageStoreFactory,
             configuration);
     }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 07/08: JAMES-3099 Annotate JMAP RFC-8620 core specifications

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit c297b6e7003c87a5fd3a35879dcde62f2e221398
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Tue Jul 28 16:57:41 2020 +0700

    JAMES-3099 Annotate JMAP RFC-8620 core specifications
---
 .../jmap-rfc-8621/doc/specs/spec/jmap/api.mdown    | 26 ++++++++++++++
 .../jmap-rfc-8621/doc/specs/spec/jmap/binary.mdown |  4 +++
 .../jmap-rfc-8621/doc/specs/spec/jmap/intro.mdown  |  8 +++++
 .../doc/specs/spec/jmap/session.mdown              | 40 ++++++++++++++++++++++
 4 files changed, 78 insertions(+)

diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/api.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/api.mdown
index 4a69f90..b902cc1 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/api.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/api.mdown
@@ -10,6 +10,10 @@ The request MUST be of type `application/json` and consist of a single JSON-enco
 
 ## The Invocation Data Type
 
+<aside class="notice">
+  Implemented
+</aside>
+
 Method calls and responses are represented by the **Invocation** data type. This is a tuple, represented as a JSON array containing three elements:
 
 1. A `String` **name** of the method to call or of the response.
@@ -20,6 +24,10 @@ Method calls and responses are represented by the **Invocation** data type. This
 
 ## The Request Object
 
+<aside class="notice">
+  Implemented
+</aside>
+
 A **Request** object has the following properties:
 
 - **using**: `String[]`
@@ -59,6 +67,10 @@ Future specifications MAY add further properties to the Request object to extend
 
 ## The Response Object
 
+<aside class="notice">
+  Implemented
+</aside>
+
 A **Response** object has the following properties:
 
 - **methodResponses**: `Invocation[]`
@@ -70,6 +82,9 @@ A **Response** object has the following properties:
   additional ones added for newly created records.
 - **sessionState**: `String` The current value of the "state" string on the
   Session object, as described in Section 2. Clients may use this to detect if this object has changed and needs to be refetched.
+  <aside class="warning">
+    Not implemented
+  </aside>
 
 Unless otherwise specified, if the method call completed successfully, its response name is the same as the method name in the request.
 
@@ -113,6 +128,10 @@ Finally, methods that make changes to the server state often act upon a number o
 
 ### Request-Level Errors
 
+<aside class="notice">
+  Implemented
+</aside>
+
 When an HTTP error response is returned to the client, the server
 SHOULD return a JSON "problem details" object as the response body,
 as per [@!RFC7807].
@@ -126,6 +145,9 @@ The following problem types are defined:
 - `urn:ietf:params:jmap:error:notRequest`
   The request parsed as JSON but did not match the type signature of the Request object.
 - `urn:ietf:params:jmap:error:limit`
+  <aside class="warning">
+    Not implemented
+  </aside>
   The request was not processed as it would have exceeded one of the **request**
   limits defined on the capability object, such as maxSizeRequest,
   maxCallsInRequest, or maxConcurrentRequests. A "limit" property MUST also be
@@ -397,6 +419,10 @@ Method calls within a single request MUST be executed in order. However, method
 
 # The Core/echo Method
 
+<aside class="notice">
+  Implemented
+</aside>
+
 The *Core/echo* method returns exactly the same arguments as it is given. It is useful for testing if you have a valid authenticated connection to a JMAP API endpoint.
 
 ## Example
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/binary.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/binary.mdown
index 9574f08..fdbd130 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/binary.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/binary.mdown
@@ -1,5 +1,9 @@
 # Binary Data
 
+<aside class="warning">
+  Not implemented yet
+</aside>
+
 Binary data is referenced by a *blobId* in JMAP and uploaded/downloaded separately to the core API. The blobId solely represents the raw bytes of data, not any associated metadata such as a file name or content type. Such metadata is stored alongside the blobId in the object referencing it. The data represented by a blobId is immutable.
 
 Any blobId that exists within an account may be used when creating/updating another object in that account. For example, an Email type may have a blobId that represents the object in Internet Message Format [@!RFC5322]. A client could create a new Email object with an attachment and use this blobId, in effect attaching the old message to the new one. Similarly, it could attach any existing attachment of an old message without having to download and upload it again.
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/intro.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/intro.mdown
index 2ba63a8..8efde9c 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/intro.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/intro.mdown
@@ -45,6 +45,10 @@ signature. These have the following meanings:
 
 ## The Id Data Type
 
+<aside class="notice">
+  Implemented
+</aside>
+
 All record ids are assigned by the server and are immutable.
 
 Where `Id` is given as a data type, it means a `String` of at least 1 and a maximum of 255 octets in size, and it MUST only contain characters from the "URL and Filename Safe" base64 alphabet, as defined in Section 5 of [@!RFC4648], excluding the pad character (`=`). This means the allowed characters are the ASCII alphanumeric characters (`A-Za-z0-9`), hyphen (`-`), and underscore (`_`).
@@ -67,6 +71,10 @@ alphabetical character.
 
 ## The Int and UnsignedInt Data Types
 
+<aside class="notice">
+  Implemented
+</aside>
+
 Where `Int` is given as a data type, it means an integer in the range -2^53+1 <= value <= 2^53-1, the safe range for integers stored in a floating-point double, represented as a JSON `Number`.
 
 Where `UnsignedInt` is given as a data type, it means an `Int` where the value MUST be in the range 0 <= value <= 2^53-1.
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/session.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/session.mdown
index d813e0d..1ff1ba5 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/session.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/jmap/session.mdown
@@ -1,5 +1,9 @@
 # The JMAP Session Resource
 
+<aside class="notice">
+  Implemented
+</aside>
+
 You need two things to connect to a JMAP server:
 
 1. The URL for the JMAP Session resource. This may be requested directly from
@@ -10,27 +14,54 @@ You need two things to connect to a JMAP server:
 A successful authenticated GET request to the JMAP Session resource MUST return a JSON-encoded **Session** object, giving details about the data and capabilities the server can provide to the client given those credentials. It has the following properties:
 
 - **capabilities**: `String[Object]`
+<aside class="notice">
+  Implemented
+</aside>
   An object specifying the capabilities of this server. Each key is a URI for a capability supported by the server. The value for each of these keys is an object with further information about the server's capabilities in relation to that capability.
 
     The client MUST ignore any properties it does not understand.
 
     The capabilities object MUST include a property called `urn:ietf:params:jmap:core`. The value of this property is an object that MUST contain the following information on server capabilities (suggested minimum values for limits are supplied that allow clients to make efficient use of the network):
+  <aside class="notice">
+    Implemented
+  </aside>
 
     - **maxSizeUpload**: `UnsignedInt`
+    <aside class="warning">
+      Not enforced yet
+    </aside>
       The maximum file size, in octets, that the server will accept for a single file upload (for any purpose). Suggested minimum: 50,000,000.
     - **maxConcurrentUpload**: `UnsignedInt`
+    <aside class="warning">
+      Not enforced yet
+    </aside>
       The maximum number of concurrent requests the server will accept to the upload endpoint.  Suggested minimum: 4.
     - **maxSizeRequest**: `UnsignedInt`
+    <aside class="warning">
+      Not enforced yet
+    </aside>
       The maximum size, in octets, that the server will accept for a single
       request to the API endpoint. Suggested minimum: 10,000,000.
     - **maxConcurrentRequests**: `UnsignedInt`
+    <aside class="warning">
+      Not enforced yet
+    </aside>
       The maximum number of concurrent requests the server will accept to
       the API endpoint. Suggested minimum: 4.
     - **maxCallsInRequest**: `UnsignedInt`
+    <aside class="warning">
+      Not enforced yet
+    </aside>
       The maximum number of method calls the server will accept in a single request to the API endpoint.  Suggested minimum: 16.
     - **maxObjectsInGet**: `UnsignedInt`
+    <aside class="warning">
+      Not enforced yet
+    </aside>
       The maximum number of objects that the client may request in a single `/get` type method call. Suggested minimum: 500.
     - **maxObjectsInSet**: `UnsignedInt`
+    <aside class="warning">
+      Not enforced yet
+    </aside>
       The maximum number of objects the client may send to create, update, or destroy in a single `/set` type method call. This is the combined total, e.g., if the maximum is 10, you could not create 7 objects and destroy 6, as this would be 13 actions, which exceeds the limit. Suggested minimum: 500.
     - **collationAlgorithms**: `String[]`
       A list of identifiers for algorithms registered in the collation registry, as defined in [@!RFC4790], that the server supports for sorting when querying records.
@@ -40,6 +71,9 @@ A successful authenticated GET request to the JMAP Session resource MUST return
     Servers MAY advertise vendor-specific JMAP extensions, as described in Section 1.8. To avoid conflict, an identifier for a vendor-specific extension MUST be a URL with a domain owned by the vendor. Clients MUST opt in to any capability it wishes to use (see Section 3.3).
 
 - **accounts**: `Id[Account]`
+<aside class="notice">
+  Implemented
+</aside>
   A map of an **account id** to an Account object for each account (see Section 1.6.2) the user has access to. An **Account** object has the following properties:
 
     - **name**: `String`
@@ -74,8 +108,14 @@ A successful authenticated GET request to the JMAP Session resource MUST return
          Errors", Section 3.6.2).
 
 - **primaryAccounts**: `String[Id]`
+<aside class="notice">
+  Implemented
+</aside>
   A map of capability URIs (as found in *accountCapabilities*) to the account id that is considered to be the user's main or default account for data pertaining to that capability. If no account being returned belongs to the user, or in any other way there is no appropriate way to determine a default account, there MAY be no entry for a particular URI, even though that capability is supported by the server (and in the capabilities object). `urn:ietf:params:jmap:core` SHOULD NOT be present.
 - **username**: `String`
+<aside class="notice">
+  Implemented
+</aside>
   The username associated with the given credentials, or the empty string if none.
 - **apiUrl**: `String`
   The URL to use for JMAP API requests.


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org