4.4. Email/query
This is a standard "/query" method as described in [RFC8620], Section 5.5 but with the following additional request arguments: o 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.
4.4.1. Filtering
A *FilterCondition* object has the following properties, any of which may be omitted: o inMailbox: "Id" A Mailbox id. An Email must be in this Mailbox to match the condition. o 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. o before: "UTCDate" The "receivedAt" date-time of the Email must be before this date- time to match the condition. o after: "UTCDate" The "receivedAt" date-time of the Email must be the same or after this date-time to match the condition. o minSize: "UnsignedInt" The "size" property of the Email must be equal to or greater than this number to match the condition. o maxSize: "UnsignedInt" The "size" property of the Email must be less than this number to match the condition. o allInThreadHaveKeyword: "String" All Emails (including this one) in the same Thread as this Email must have the given keyword to match the condition. o 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.
o noneInThreadHaveKeyword: "String" All Emails (including this one) in the same Thread as this Email must *not* have the given keyword to match the condition. o hasKeyword: "String" This Email must have the given keyword to match the condition. o notKeyword: "String" This Email must not have the given keyword to match the condition. o hasAttachment: "Boolean" The "hasAttachment" property of the Email must be identical to the value given to match the condition. o 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. o from: "String" Looks for the text in the From header field of the message. o to: "String" Looks for the text in the To header field of the message. o cc: "String" Looks for the text in the Cc header field of the message. o bcc: "String" Looks for the text in the Bcc header field of the message. o subject: "String" Looks for the text in the Subject header field of the message.
o 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. o 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: o Any syntactically correct encoded sections [RFC2047] of header fields with a known encoding SHOULD be decoded before attempting to match text. o 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. o Text SHOULD be matched in a case-insensitive manner. o 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 (\): ' " \ o 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. o 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").4.4.2. Sorting
The following value for the "property" field on the Comparator object MUST be supported for sorting: o "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. o "size" - The "size" as returned in the Email object. o "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. o "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. o "subject" - This is taken to be the base subject of the message, as defined in Section 2.1 of [RFC5256]. o "sentAt" - The "sentAt" property on the Email object. o "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.
o "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. o "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.4.4.3. 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).
4.5. Email/queryChanges
This is a standard "/queryChanges" method as described in [RFC8620], Section 5.6 with the following additional request argument: o collapseThreads: "Boolean" (default: false) The "collapseThreads" argument that was used with "Email/query".4.6. Email/set
This is a standard "/set" method as described in [RFC8620], Section 5.3. The "Email/set" method encompasses: o Creating a draft o Changing the keywords of an Email (e.g., unread/flagged status) o Adding/removing an Email to/from Mailboxes (moving a message) o 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: o 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. o There MUST NOT be two properties that represent the same header field (e.g., "header:from" and "from") within the Email or particular EmailBodyPart. o Header fields MUST NOT be specified in parsed forms that are forbidden for that particular field. o Header fields beginning with "Content-" MUST NOT be specified on the Email object, only on EmailBodyPart objects.
o If a "bodyStructure" property is given, there MUST NOT be "textBody", "htmlBody", or "attachments" properties. o If given, the "bodyStructure" EmailBodyPart MUST NOT contain a property representing a header field that is already defined on the top-level Email object. o If given, textBody MUST contain exactly one body part and it MUST be of type "text/plain". o If given, htmlBody MUST contain exactly one body part and it MUST be of type "text/html". o 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. o 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": o "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": o "tooManyKeywords": The change to the Email's keywords would exceed a server-defined maximum. o "tooManyMailboxes": The change to the set of Mailboxes that this Email is in would exceed a server-defined maximum.4.7. 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.
4.8. 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: o accountId: "Id" The id of the account to use. o 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. o emails: "Id[EmailImport]" A map of creation id (client specified) to EmailImport objects. An *EmailImport* object has the following properties: o blobId: "Id" The id of the blob containing the raw message [RFC5322]. o mailboxIds: "Id[Boolean]" The ids of the Mailboxes to assign this Email to. At least one Mailbox MUST be given. o keywords: "String[Boolean]" (default: {}) The keywords to apply to the Email. o 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 mutable properties to the existing object. 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: o accountId: "Id" The id of the account used for this call. o 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. o newState: "String" The state string that will now be returned by "Email/get" on this account. o 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.
o 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.4.9. 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: o id o mailboxIds o keywords o 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: o accountId: "Id" The id of the account to use. o blobIds: "Id[]" The ids of the blobs to parse.
o 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" ] o 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. o fetchTextBodyValues: "Boolean" (default: false) If true, the "bodyValues" property includes any "text/*" part in the "textBody" property. o fetchHTMLBodyValues: "Boolean" (default: false) If true, the "bodyValues" property includes any "text/*" part in the "htmlBody" property. o fetchAllBodyValues: "Boolean" (default: false) If true, the "bodyValues" property includes any "text/*" part in the "bodyStructure" property. o 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: o accountId: "Id" The id of the account used for the call. o parsed: "Id[Email]|null" A map of blob id to parsed Email representation for each successfully parsed blob, or null if none. o notParsable: "Id[]|null" A list of ids given that corresponded to blobs that could not be parsed as Emails, or null if none. o 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.4.10. 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 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: <bbce0ae9-58be-4b24-ac82-deb840d58016@sloti7d1t02> User-Agent: Cyrus-JMAP/3.1.6-736-gdfb8e44 Mime-Version: 1.0 Date: Tue, 10 Jul 2018 11:03:11 +1000 From: "Joe Bloggs" <joe@example.com> Subject: World domination Content-Language: en Content-Type: text/plain I have the most brilliant plan. Let me tell you all about it. What we do is, we 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" ]]