Skip to main content
Mail is the RFC 8621 surface, requested with urn:ietf:params:jmap:mail (plus submission and vacationresponse for sending and auto-reply). This page lists what OxiMail serves and where it goes beyond the RFC. The mechanics shared by every method — batching, back-references, /changes, push — are on JMAP Core.

Objects and methods

OxiMail-specific method extensions on the mail surface: Email/send (one-call submission convenience), Email/cancel (cancel a scheduled/undo-window send), and Email/wake (snooze expiry). Reindexing is an admin concern, not a client one.

The body tree

Email/get derives everything it says about a message’s body from one depth-first walk of the real MIME tree. That is what makes partId mean the same thing in every property of the same response: the id you read in bodyStructure is the id you find on the attachment, and the key you look up in bodyValues.
bodyValues is empty unless you ask for it. RFC 8621 §4.2 makes the content conditional on fetchTextBodyValues, fetchHTMLBodyValues or fetchAllBodyValues, and OxiMail now holds that letter — the values used to be populated whether or not the arguments were present. A client that never set a fetch* argument and read bodyValues anyway will read an empty map. Set the argument. Email/get and Email/parse run the same selection, so the rule is identical on both.
Per-part headers are how you read Content-Type parameters. type is normatively bare — text/calendar and nothing more — so the parameters that carry the meaning are only reachable by asking for the headers of the part: bodyProperties: ["partId", "type", "headers"]. This is the RFC-blessed way to tell a genuine invitation (method=REQUEST) from a plain .ics someone attached, without downloading the blob. Two deviations are deliberate and worth knowing:
  • The default projection strips per-part headers. The default shape stays a subset of the normative §4.2 property list; ask for headers explicitly and you get them.
  • partId and subParts survive an explicit projection inside the tree. A tree without its branches is not a tree, so omitting subParts from bodyProperties reads as a mistake rather than as a wish to flatten, and the server keeps them.
Two shapes carry no bodyStructure at all, by construction: a message the parser refused and fell back to reading flat (it never announces a structure it did not parse), and a native draft, whose canonical v2 shape is body/html.

Behaviours worth knowing

  • notFound is never empty when it should not be. Every unparseable or unknown id in a /get lands in notFound explicitly.
  • updated values are null. Per RFC 8620 §5.3, check membership (id in updated), not truthiness of the value.
  • hasAttachment means attachments. The flag is computed from the MIME structure at ingestion, not from text extraction.
  • An attached message is one attachment, not its contents. attachments is drawn from this message’s body structure (RFC 8621 §4.1.7), so a message/rfc822 part is the attachment and its own parts are not advertised alongside it. Forwarding a mail that itself carried contract.pdf lists the .eml, not contract.pdf — the reader is never told they received files the sender never attached. The encapsulated parts remain reachable: parse the .eml blob with Email/parse and their blobs download normally (every part is stored, nested ones included — what the wire list answers and what is downloadable are two different questions).
  • Thread/get and Thread/changes need only the mail capability. RFC 8621 §3 makes that pair mandatory, so those two methods carry both capabilities — urn:ietf:params:jmap:mail as primary, v2:threads as additional — and a client that declares nothing but core + mail is served. The richer thread verbs (query, queryChanges, set, archive, merge, split) stay v2 extensions. The RFC’s {id, emailIds} shape is a strict subset of what the object serves: select it with properties.
  • Anchor pagination is exact. anchor + anchorOffset + position in Email/query follow RFC 8620 §5.5 to the letter, including negative positions.
  • Drafts can be rewritten. A body update on a $draft message works through the standard destroy+create cycle; the server does not reject draft edits.
  • Send-to-self delivers. onSuccessUpdateEmail runs after delivery, and deduplication never happens at the message level — mailing yourself works.
  • Scheduled send is first-class. An EmailSubmission with a future send time is queued and cancellable (Email/cancel) until it leaves; disabled accounts’ scheduled sends are cancelled at the source.
  • Strictness is a feature. Unknown accountIdaccountNotFound; a capability missing from usingunknownCapability; a bad back-reference → invalidResultReference. The server is the reference implementation and does not have a lenient mode.
Email/query supports the RFC 8621 filter tree (inMailbox, from/to/subject/text, hasKeyword, hasAttachment, dates, sizes) with AND/OR/NOT combinators, and full-text conditions run against the Tantivy index — searchable the moment SMTP answered 250 OK (accepted means searchable). SearchSnippet/get returns highlighted fragments for result lists.

Keywords and flags

JMAP keywords ($seen, $flagged, $draft, $answered, $junk, custom) are the same state IMAP flags read and write — one store, two protocols. The spam pipeline files with $junk; user Labels are a separate v2 object (JMAP v2).

Sending

The submission pipeline validates the from against the account’s identities (envelope and header — forging a colleague’s address fails with forbiddenFrom), stores the message encrypted, and hands it to the delivery queue. onSuccessUpdateEmail/onSuccessDestroyEmail run after delivery is accepted, in the same request’s response.

Read receipts (MDN)

Message Disposition Notifications are the RFC 9007 surface, requested with urn:ietf:params:jmap:mdn. Both directions are served. Reading the receipts you were sent. Inbound MDNs are linked onto the sent message they answer at SMTP ingest, paired on Original-Message-ID and exposed as Email.mdnBlobIds. MDN/parse turns such a blob into the structured MDN object — disposition, originalMessageId, finalRecipient, reportingUA, originalRecipient, mdnGateway, error, extensionFields — so a client renders “your message was displayed on …” without reimplementing RFC 8098 itself. Verdicts are per blob, never a method-level collapse: each id lands in parsed, notParsable (it parses as mail but is not an MDN, or does not parse at all — RFC 9007 draws no finer line), or notFound. Empty groups are served as null, which is the RFC’s own shape. Sending one. MDN/send composes an RFC 8098 receipt for a received message and hands it to the outbound queue, with disposition and optional subject, textBody, reportingUA, finalRecipient (which must be an address this account may send as, forbiddenFrom otherwise), includeOriginalMessage, and extensionFields. Two properties of that method are worth knowing before you build on it:
  • Nothing sends an MDN automatically. Emission is an explicit user gesture, and this method is the gesture. The receipt goes out with a null envelope sender (RFC 8098 §3) and does not generate follow-up traffic if it fails.
  • “Already sent” is not client-erasable. RFC 9007 anchors the anti-duplicate check on the $mdnsent keyword, and OxiMail holds the RFC letter — the onSuccessUpdateEmail patch must set keywords/$mdnsent: true, refused by name otherwise, so an RFC-literal third-party client works unmodified. But it checks a server-side audit timestamp alongside the keyword, written atomically with the patch. Clearing $mdnsent does not re-arm the send.
For the classic-protocol view of the same data, see legacy protocols; for v2-only mail extensions (labels, snooze, saved messages), see JMAP v2.