Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

JMAP feasibility — 2026-09

Scope: a research artifact assessing what it would take to add a JMAP server surface (RFC 8620 core, RFC 8621 mail, and the eight published extension RFCs) to this stack. It records what the specs demand, what the storage kernel can answer today, where the design collides with sealing mail to the recipient, how widely JMAP is actually deployed, and what a build would cost. The recommendations are inputs to a future planning decision, not decisions — nothing on this page changes code, and no adoption is settled by its appearing here. It is the structural sibling of RFC-updates recon — 2026-08 and the two dependency audits.

Method: the spec inventory came from jmap.io/spec/index.html and was cross-checked against the IETF datatracker for the two documents still in draft. Every requirement quoted below was read from the RFC text itself (rfc-editor.org/rfc/rfc8620.txt, rfc8621.txt), not from a summary. Every claim about this repo traces to a trait definition, migration or route read on 2026-09-01; the two absence claims that carry the most weight were established through scripts/verify-absent.sh with a passing positive control rather than a bare grep, per the repo’s verification discipline. Cost language is ordinal — no wave was planned and no scheduling analysis was run.

The verdict in three sentences

JMAP is technically feasible here and the HTTP substrate is a genuinely good fit, but roughly half the work is not JMAP at all — it is backend capability the storage kernel has never needed, principally an account-scoped change log with tombstones. One part is not an engineering cost at all: JMAP assumes the server can parse every message, which for sealed-at-rest accounts is only true inside a live session holding a reading secret. That is a question about what SithBit is, and it has to be answered before any schema is designed.

The specification inventory

Ten JMAP documents are published as RFCs. Two more are still Internet-Drafts as of 2026-09-01, verified against the datatracker rather than trusting jmap.io’s own labelling.

DocumentCapability URNStatus
RFC 8620 — JMAP coreurn:ietf:params:jmap:corePublished
RFC 8621 — JMAP for Mailurn:ietf:params:jmap:mailPublished
RFC 8887 — JMAP over WebSocketurn:ietf:params:jmap:websocketPublished
RFC 9007 — MDN handlingurn:ietf:params:jmap:mdnPublished
RFC 9219 — S/MIME signature verificationurn:ietf:params:jmap:smimeverifyPublished
RFC 9404 — Blob managementurn:ietf:params:jmap:blobPublished
RFC 9425 — Quotasurn:ietf:params:jmap:quotaPublished
RFC 9610 — Contacts (with RFC 9553 JSContact)urn:ietf:params:jmap:contactsPublished
RFC 9661 — Sieve scripts managementurn:ietf:params:jmap:sievePublished
RFC 9670 — Sharingurn:ietf:params:jmap:principalsPublished
JMAP for Calendarsnot yet assigneddraft-ietf-jmap-calendars-28
JSCalendar 2.0n/a (data format)draft-ietf-calext-jscalendarbis-18

An eleventh published RFC belongs in any adoption discussion even though jmap.io does not list it, because it is an IMAP document: RFC 9698, the JMAPACCESS extension for IMAP (Standards Track, January 2025, authored at ICANN and Fastmail). It lets an IMAP server advertise that the same messages are reachable over JMAP with the same credentials — “intended for clients that want to migrate gradually to JMAP or use JMAP extensions within an IMAP client”. It matters here twice: it is the cheapest possible first step for a stack that already ships an IMAP server, and its existence is itself an adoption signal (see below).

How widely JMAP is actually used

Worth stating plainly, because it bears directly on whether the work is worth doing. JMAP is a real, finished, actively-extended IETF standard with a healthy implementation ecosystem — and it has almost no presence at the two places that would make it a compatibility requirement: large mail providers and mainstream mail clients.

Providers. Fastmail is the origin and the flagship: it offers full JMAP access alongside IMAP/POP/SMTP, and its people author most of the specs. Beyond it, deployment is essentially self-hosted. No hyperscale provider offers JMAP — Gmail, Outlook.com and Yahoo are IMAP-only for third-party access, and Proton’s JMAP request remains an open item on its public feedback forum, still being asked about in 2026.

Servers. The ecosystem here is genuinely healthy. jmap.io lists seven server implementations, of which the substantial ones are Stalwart (Rust, JMAP-native rather than bolted on), Apache James (Java, JMAP in the 3.x series), Cyrus IMAP (JMAP in its 3.x series), and atmail / Group-Office / shipmail / tmail-backend. Two proxies also exist in both directions — a JMAP server fronting an IMAP store, and an IMAP-to-JMAP proxy — which is a fair indicator of where the demand actually sits.

Clients. This is the weak link, and it is the one that matters for the interoperability argument. jmap.io lists seventeen clients, and the list is almost entirely FOSS and niche: aerc, meli, Ltt.rs, Sterna Mail, Aria, Boogie, Cypht, Bulwark, Twake, Mailtemi, Pimsync. Thunderbird, Apple Mail, Outlook and the Gmail apps are all absent. Thunderbird has JMAP on its public roadmaps — iOS exploration reported around 80% complete, Android in exploration, desktop planned after Exchange — but in every case IMAP is being implemented first and JMAP is explicitly deferred; Thundermail (the Pro service) is stated to support it from launch. Fifteen client libraries and SDKs exist across Go, Java, TypeScript, Rust, Python and Perl, so building a JMAP client is easy. That is not the same as users already having one.

The honest read. JMAP’s install base today is Fastmail plus self-hosters, and RFC 9698 exists precisely because the migration path everyone expects is gradual and IMAP-anchored. For SithBit the practical consequence is that adding JMAP would not in the near term let a user point Thunderbird or Apple Mail at a mailbox and have it work better — those clients would still use IMAP. What it would buy is a standards-shaped replacement for the bespoke /v1/mail REST API that the webmail PWA, the Outlook add-in and the Thunderbird and Chrome extensions all ride today, and a future-proofed position for when the mainstream clients do land their JMAP support. Whether that is worth the cost below is a product judgement, not a technical one.

What a conforming server owes

JMAP is JSON over HTTPS with essentially no ABNF; the difficulty is entirely in the data-model contract, and it lands in three places.

The Session resource (RFC 8620 §2) has a fixed shape: capabilities, accounts, primaryAccounts, username, apiUrl, downloadUrl, uploadUrl, eventSourceUrl, state. Three of those are URI Templates whose variable lists are MUST-carry — downloadUrl must contain accountId, blobId, type and name; eventSourceUrl must contain types, closeafter and ping.

The API endpoint takes a batch of methodCalls triplets and returns methodResponses. Back-references (#-prefixed arguments resolved by JSON Pointer, plus JMAP’s * flatten rule) let call N consume call N−1’s actual result — which forces the dispatcher to be a strictly sequential interpreter over a growing response buffer, never a parallel fan-out.

Six method archetypes apply to every data type: Foo/get, Foo/changes, Foo/set, Foo/copy, Foo/query, Foo/queryChanges. The sharp edges are in the archetypes rather than the types. Foo/set is atomic per record and explicitly not per call, so a batch half-commits into notCreated/notUpdated/notDestroyed without poisoning what already landed; its update argument is a PatchObject whose JSON-Pointer paths may not overlap or point inside an array, with null meaning reset-or-delete; queryChanges models a result-set delta as an ordered removed/added list, and its upToId shortcut is sound only when both filter and sort touch exclusively immutable properties.

The load-bearing requirement is the state-string contract. Each type carries an opaque state string covering, in the RFC’s words, “all the data of this type in the account”. If the data changes it MUST change; if not, the server SHOULD return the same one. Foo/changes then reconstructs from any such past string which of three buckets each id falls in, with defined collapse rules for create-then-update, update-then-destroy and create-then-destroy. RFC 8620 §5.2 is blunt about the escape hatch:

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.

A server may legally answer every Foo/changes with cannotCalculateChanges and force a full resync. It would also have discarded the entire reason to prefer JMAP over the REST API that already exists.

The capability gap

Eleven capabilities checked against the workspace on 2026-09-01.

CapabilityStatusWhat is there today
Per-account, per-type state + Foo/changesabsentTwo per-mailbox counters, both solid: MailboxRow.highest_modseq (RFC 7162) and change_seq for IDLE. Neither is account-scoped; MailRepo has no “changes since N” method, and IMAP CHANGEDSINCE is an in-memory filter over an already-materialised snapshot (imap_session/src/session/selected.rs).
Destroyed-id recovery (tombstones)absentMailRepo::expunge deletes the row. Sixteen tables in the schema, none an expunge log. Already a known gap — BACKLOG.md’s QRESYNC entry lists “expunged-UID tombstones” as remaining work.
Immutable per-message id, mailboxIds as a setabsentIdentity is per copy: MessageRow exposes no id, and copy mints a fresh row (MESSAGE_COPY_ONE) while move_messages preserves one. blob_key is the only cross-mailbox handle, and it is shared across wallets on a multi-recipient delivery — so it cannot be Email.id unmodified without leaking identity between accounts.
ThreadingabsentNo thread ids, no RFC 5256/JWZ threading, no IMAP THREAD. Message-ID / In-Reply-To / References are re-parsed per request into mail_message’s MailSummary. The one persisted Message-ID index, reply_locators, exists for on-chain reply bounties and is never joined to messages.
Server-side body searchpartialReal but unindexed: GET /v1/mail/search scans 500 rows newest-first in one mailbox with a resume cursor; imap_session/src/search.rs parses lazily per message. No FTS index anywhere. Email/query is cross-mailbox, sortable and delta-able on top of that.
Blob downloadpresentThree routes already serve bytes, including /v1/mail/messages/{uid}/parts/{part} with a decoded content type and filename.
Blob uploadabsentNothing accepts bytes. Compose has no attachments field, the global body cap is a deliberate 2 MiB constant, BlobStore has no ranged read and stores no content type, and blob keys are global UUIDs with no wallet scoping — a blobId namespace needs authz designed, not just exposed.
Push transportpartialIn-process WatchRegistry plus a 2-second change_seq poll for split deployments. Verified absent across the server crates: no SSE, no text/event-stream, no WebSocket. JMAP wants an EventSource endpoint and outbound PushSubscription webhooks — the latter a new outbound-caller class in the security model, with an SSRF guard the RFC mandates.
Queued / undoable submissionabsentSpooling is immediate; local rows are visible to IMAP before the response returns. No EmailSubmission object, no undoStatus, no sendAt. The job queue’s visible_at column is the mechanism a delayed send could ride.
Identities and vacation responderabsentSend-as is hardwired to wallet@<first local domain>; chain aliases are readable but not sendable-as, and alias logins were rejected on record. The DND/away schedule is an advisory signal for senders and never sends an auto-reply, which is what VacationResponse means.
HTTP + auth substratepresentaxum 0.8, six independently-composed routers merged in main, and SessionAuthed yields wallet + jti + reading secret in one decode. Two frictions: the 2 MiB body constant needs a route-scoped override, and the 413→422 remap in error.rs would fight JMAP’s own urn:ietf:params:jmap:error:limit shape.

Two of those rows are the whole schedule. An append-only change log per account and type, retained around 30 days, is new schema on all six storage backends and a hard prerequisite for both Foo/changes and any push payload, since a StateChange is a map of per-type state strings. Query-state tracking for queryChanges is a different problem again — not “did this record change” but “did the order of an arbitrary client-defined filter and sort change”, answered incrementally. Nothing in IMAP SEARCH/SORT is an analogue.

One prior decision bears on this and should be read carefully. RFC 8474 (OBJECTID) — the IMAP extension that would have supplied exactly the stable EMAILID/THREADID the table twice calls absent — is recorded declined, final in the RFC-updates recon, which even names it “the JMAP-era resync feature set”. But the stated reason was that it is fork-blocked on imap-types, and that reason does not transfer to JMAP, which would touch none of those crates. The decline is not a precedent against this work. It does mean the underlying storage capability has never been built.

The collision: sealed bodies against a spec that parses everything

This is the part that is not an estimate. RFC 8621’s Email is a parsed object: the server is expected to hand clients typed headers, a MIME body structure, attachment part lists and a text preview. Its Security Considerations never contemplate a server holding ciphertext it cannot open; that case is outside the document’s threat model.

SithBit’s at-rest storage is per-account dual-mode, resolved at spool time by mail_submit’s at_rest_mode, into AtRest::Plaintext, AtRest::Sealed or AtRest::OperatorSealed. So “can the server parse this message” is answered per account, not per deployment. A plaintext-mode account is fully parseable and the JMAP work over it is ordinary. A sealed account’s body is an SBd DEK envelope the daemon can open only by unwrapping with a reading secret — the 32-byte value that arrives with the login, lives in memory keyed by the JWT’s jti, is zeroized on drop, and dies with the session.

The consequence is narrower than “JMAP cannot work”, and worth stating precisely. Inside a live keyed session the server can decrypt, and Email/get can be served. What is impossible is anything outside such a session: no background index, no server-side full-text search, no threading pass over historical mail, and no push payload richer than “something changed”, because the worker computing it holds no reading secret. One nuance decides how much this bites: Email/get’s default property list includes textBody, htmlBody, attachments, hasAttachment and preview, so a bare Email/get needs the MIME structure even though body text only ships when the caller sets fetchTextBodyValues. Structure, not just content, sits behind the seal.

Three ways through, in ascending cost. This is a decision, not an estimate, and it gates the change-log design — a schema built on the assumption that a background worker may read message content is a different schema.

  1. Serve JMAP for plaintext-mode accounts only. Advertise the mail capability per account, exactly as RFC 8620’s accountCapabilities is designed to allow, and omit it for sealed accounts, who keep the trustless webmail path that already unseals in the page. Honest, conformant, and it ships — at the price of JMAP being a second-class surface for the accounts the product pitch is built around.
  2. Keep headers outside the seal. Seal only the body. Threading, Email/query over from/to/subject, sorting and most of the default property list all become computable in the background. This narrows what sealing protects, and is a privacy-posture change owing its own decision — metadata is most of what surveillance wants.
  3. Client-side index, stored sealed. The keyed client builds the search and thread index and republishes it sealed; the server stores and serves an artifact it cannot read. Preserves the guarantee completely, and is a materially different architecture from “the server implements JMAP” — closer to a sync protocol wearing a JMAP-shaped façade.

Build or adopt

Standing rule 1 is ecosystem-first, so the crate landscape decides much of the cost. Checked live against the crates.io API on 2026-09-01.

CrateVersionDownloadsVerdict
jmap-server + jmap-mail-server (MarkAtwood/crate-jmap)0.1.3444 / 108Backend-agnostic, MIT/Apache-2.0, 26 RFC 8621 methods plus optional MDN. The right shape; first published 2026-05.
mailrs-jmap (goliajp/mailrs)1.1.3761Store trait is IMAP-shaped and would nearly drop in here — but it implements seven methods and no Foo/changes or Foo/queryChanges at all, which is precisely the half worth having.
jmap-client (stalwartlabs)0.4.273,688Mature, widely used, and a client. Useful for conformance testing, not for serving.
Stalwart’s own server JMAPInside the Stalwart monorepo, not published standalone. A reference implementation to read, not a dependency.

The crate-jmap family is the only credible adoption target, and its integration surface is small enough to quote in full. JmapBackend requires six methods — account_exists, get_objects, get_state, get_changes, query_objects, query_changes — and MailBackend adds ten: create_object, update_object, destroy_object, import_email, find_thread_by_message_ids, blob_exists, parse_email, copy_email, search_snippets, supports_type.

Sixteen methods is a tractable adapter, but note which ones. get_state, get_changes, query_changes, find_thread_by_message_ids and search_snippets are all capabilities the gap table marks absent. The crate does not reduce the backend work; it removes the protocol work and leaves the backend work fully intact. That is still worth a great deal — the dispatcher, back-reference resolution, patch semantics and 26 method handlers are the fiddly, conformance-critical half — but it should not be mistaken for a shortcut past the schema.

Two frictions belong in any adoption record. jmap-mail-server depends on mime-tree, a different MIME parser from the adopted mail-parser, so taking it means either two parsers in the tree or an adapter that skips the crate’s own. And at 108 downloads, four months old, single-author and pre-1.0 with an explicit “may break across minor versions” note, this is a larger bet than imap-codec was: the pinned-alpha precedent exists, but that crate had an established author and a large user base.

The extensions, ranked

Costs assume core and mail already work — the assumption doing most of the work in this table, since today neither does.

RFCCostFor a mail-only server
9007 MDNsmallBest value. Its dependencies are the Identity and parse plumbing mail already needs.
8887 WebSocketsmallValuable, and cheap once push exists — but push does not.
9425 QuotassmallValuable; MailRepo::wallet_bytes already computes the number, which needs a per-account read route.
9404 Blob managementmediumValuable, and gated behind building the upload path at all.
9749 VAPID pushmediumWebmail-PWA only, and large once Web Push infrastructure itself is counted.
9670 SharinglargeOut of scope — needs a principals/ACL model that does not exist.
9219 S/MIME verificationlargeOut of scope near-term; S/MIME is a separate product line here.
9661 Sieve managementvery largeOut of scope. The JMAP surface is the cheap part; the Sieve interpreter is the project.
9610 + 9553 ContactslargeOut of scope for mail.
Calendars + JSCalendar 2.0very largeOut of scope; both still drafts, on a data model that does not exist.

A defensible ordering, if it goes ahead

The dependency structure is real — nothing after the first item is worth starting before it lands, because everything later consumes the change log.

  1. The change log, alone, with no JMAP in the tree. Account-scoped, per-type, append-only, tombstoned, ~30-day retention, across all six backends, with the existing counter-allocation conformance group extended to cover it. Independently valuable: it also unblocks part of the QRESYNC wave, which is stalled partly on the same missing tombstones. If only one thing is ever built from this page, this is it.
  2. Message identity and threading. A stable per-message id distinct from (mailbox, uid) and namespaced per wallet, plus a thread id computed at spool time — the pattern SealedPin.parent_rfc822_id already establishes for sealed rows.
  3. The JMAP core surface. Session resource, API endpoint, back-reference resolution, /get and /set over Mailbox and Email.
  4. Upload/download and blob authz. Wallet-scoped blobIds, a route-scoped body limit, ranged reads, a stored content type. This is what finally lets webmail send an attachment.
  5. Query and queryChanges. The hardest piece, and the one most affected by the sealing decision.
  6. Push. EventSource first; PushSubscription webhooks with the SSRF guard only if a client actually needs them.

Deliberately excluded: identities, vacation responder, submission objects, and every extension except possibly MDN — real work with no dependents.

There is also a much cheaper first move that is not on this list: RFC 9698 JMAPACCESS advertises a JMAP endpoint from the existing IMAP server. It is worthless without a JMAP endpoint to advertise, so it cannot come first, but it is the natural closing step of any JMAP program here and costs almost nothing once one exists.

The decisions this page does not make

  1. Why JMAP, concretely? The strongest case is not standards coverage but that every SithBit client today rides a bespoke /v1/mail REST surface. Given the adoption picture above, JMAP would not in the near term make Thunderbird or Apple Mail work better against a SithBit mailbox — those clients still speak IMAP. If third-party interoperability is not actually wanted, the cost/benefit inverts sharply.
  2. Which sealing posture? The three options above. This gates the schema design and cannot be deferred into the build.
  3. Is the change log worth building on its own merits? It is the largest item, a prerequisite for everything else, and valuable independently of JMAP — it may deserve to be a work item whether or not JMAP is adopted.
  4. Adopt a four-month-old, 108-download crate family, or hand-roll? Ecosystem-first is standing rule 1 and a pinned-pre-release precedent exists, but that precedent carried a different risk profile. Either way it would need its own adoption-table row in DURABLE-RECORD.md.

Known softness

The RFC 8621 analysis behind the collision section was assembled through a summarizing fetch tool that caps verbatim quotes, so a few of its finer points are paraphrase-confidence rather than citation-grade. The classifications it drove are sound and the load-bearing ones were re-read from the RFC text directly — the Email/get default property list, the §5.2 retention text, the Session field list and the cannotCalculateChanges semantics. Its worked examples were not fully captured and should be re-fetched narrowly before anyone uses them as test fixtures.