RFC 8620: JMAP — JSON Meta Application Protocol
Why This Exists
IMAP works, but it carries decades of complexity:
- Stateful TCP connections. IMAP requires a persistent connection per mailbox. Mobile clients on unreliable networks constantly reconnect and re-sync.
- Custom text protocol. IMAP's wire format requires specialized parsers. Every language needs a dedicated IMAP library.
- Extension fragmentation. Critical features (CONDSTORE, MOVE, SPECIAL-USE) are optional extensions that servers may or may not support.
- Push notification limitations. IMAP IDLE only works for one mailbox at a time, and many proxies/firewalls kill idle TCP connections.
JMAP solves all of these. It is stateless (each request is self-contained), uses HTTPS (works through every proxy and firewall), uses JSON (every language has a parser), and defines push notifications as a first-class feature via EventSource or Web Push.
How It Works
Service Discovery
A client discovers the JMAP endpoint via a well-known URI:
GET https://example.com/.well-known/jmap { "capabilities": { "urn:ietf:params:jmap:core": { ... }, "urn:ietf:params:jmap:mail": { ... } }, "apiUrl": "https://jmap.example.com/api/", "uploadUrl": "https://jmap.example.com/upload/{accountId}/", "downloadUrl": "https://jmap.example.com/download/{accountId}/{blobId}/{name}", "eventSourceUrl": "https://jmap.example.com/events/" }
Making API Calls
All JMAP operations are sent as a single POST to the API URL. The request body contains an array of method calls, and the response contains the corresponding results:
POST https://jmap.example.com/api/ { "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"], "methodCalls": [ ["Mailbox/get", { "accountId": "u1234", "ids": null }, "a"] ] } { "methodResponses": [ ["Mailbox/get", { "accountId": "u1234", "state": "m42", "list": [ { "id": "mb1", "name": "Inbox", "totalEmails": 142 }, { "id": "mb2", "name": "Sent", "totalEmails": 87 }, { "id": "mb3", "name": "Archive", "totalEmails": 4210 } ] }, "a"] ] }
Batching and Back-References
Multiple method calls can be batched in a single request. Back-references let one call use the results of a previous call:
"methodCalls": [ ["Email/query", { "accountId": "u1234", "filter": { "inMailbox": "mb1", "after": "2025-03-01T00:00:00Z" }, "sort": [{ "property": "receivedAt", "isAscending": false }], "limit": 10 }, "q"], ["Email/get", { "accountId": "u1234", "#ids": { "resultOf": "q", "name": "Email/query", "path": "/ids" }, "properties": ["from", "subject", "receivedAt", "preview"] }, "g"] ]
This searches the inbox and fetches the top 10 results in a single HTTP round-trip — something that would take multiple IMAP commands.
Key Technical Details
State Strings and Synchronization
Every JMAP /get response includes a state string. To sync changes, the client calls /changes with the last known state:
["Email/changes", { "accountId": "u1234", "sinceState": "e789" }, "c"] ["Email/changes", { "oldState": "e789", "newState": "e801", "created": ["em500", "em501"], "updated": ["em480"], "destroyed": ["em312"] }, "c"]
The client then fetches only the created/updated items. This is more efficient than IMAP's CONDSTORE because it works across all mailboxes simultaneously and does not require a persistent connection.
Push Notifications
JMAP defines two push mechanisms:
-
EventSource: The client opens a long-lived SSE connection to the
eventSourceUrl. The server sends state-change events as they happen. - Web Push (RFC 8030): For mobile/background clients. The server sends push notifications to a push subscription URL provided by the client.
The Foo/get, Foo/set, Foo/query Pattern
JMAP uses a uniform method pattern for every data type:
| Method | Purpose |
|---|---|
Foo/get |
Fetch objects by ID |
Foo/changes |
Get IDs of objects changed since a state |
Foo/set |
Create, update, or destroy objects |
Foo/query |
Search/filter and sort, returning matching IDs |
Foo/queryChanges |
Incremental updates to a previous query result |
For email, Foo is Email, Mailbox, Thread, EmailSubmission, etc. This consistency means once you understand one data type, you understand them all.
Common Mistakes
- Treating JMAP like a traditional REST API. JMAP uses a single endpoint with method calls in the request body, not resource-based URLs. Don't try to map it to REST conventions.
- Ignoring state strings. Every response has a state. If you discard it, you lose efficient sync and must re-fetch everything on the next call.
-
Fetching all properties.
Email/getwithout apropertieslist returns everything, including full message bodies. Always specify which properties you need. - Not using back-references. Making separate HTTP requests for query + fetch wastes a round-trip. Use back-references to batch them.
- Assuming universal server support. As of 2026, JMAP adoption is growing but IMAP is still far more widely deployed. Fastmail is the most prominent JMAP provider. Check server capabilities before committing to JMAP-only.
- Polling instead of using push. JMAP provides EventSource and Web Push specifically so you don't have to poll. Use them.
Deliverability Impact
-
JMAP is primarily a retrieval protocol. Like IMAP and POP3, it governs how recipients access mail, not how you send it. However, JMAP also defines
EmailSubmissionfor sending, which could replace SMTP submission for some use cases. - Better engagement data. JMAP's structured approach to flags and mailbox moves gives providers cleaner engagement signals. When a user moves your message from Junk to Inbox via JMAP, the provider sees a clear signal.
- Faster client sync. Because JMAP sync is more efficient than IMAP, mobile clients stay in sync more reliably. This means recipients see your messages sooner and engagement (opens, clicks) happens faster.
-
EmailSubmission object. JMAP's
EmailSubmission/setmethod allows sending email through the same API used for reading. For applications that both send and receive, this unifies the entire email workflow under one protocol.