Choose the right email protocol

JMAP for mail

Understand why JMAP uses HTTP and JSON for efficient synchronization and API-friendly mail clients.

IMAP is powerful but awkward to use from modern code. It’s a line-based protocol with its own parser, long-lived connections, and a lot of state. JMAP is the answer to that. It’s a synchronization protocol over HTTPS and JSON, and RFC 8621 maps it to mail: messages, mailboxes, searches, changes, and submission.

If you’ve written code against a REST API, JMAP will feel familiar in five minutes.

What JMAP is not

JMAP is not SMTP with JSON syntax. Servers still relay mail between domains with SMTP. JMAP replaces the client side, the part IMAP used to own, and adds submission so a client talks to one API for everything.

Discovery first

A JMAP client never guesses URLs. It fetches a session object that tells it where everything lives:

curl -H "Authorization: Bearer $FASTMAIL_TOKEN" https://api.fastmail.com/jmap/session

The response contains the API URL, your accounts, the server’s capabilities, and the upload and download URLs. Fastmail is the main JMAP provider today, hence its session URL here.

Batching method calls

Then you send method calls to the API URL. Several can go in one request, and later calls can reference the results of earlier ones:

{
  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
  "methodCalls": [
    ["Email/query", { "accountId": "a1", "filter": { "isUnread": true } }, "q1"],
    ["Email/get", { "accountId": "a1", "#ids": { "resultOf": "q1", "name": "Email/query", "path": "/ids" } }, "g1"]
  ]
}

The first call finds unread messages and is labeled q1. The second fetches them, and instead of a hardcoded ids list it uses #ids to say “take the ids from the result of q1”. One round trip, two operations, and each result comes back tagged with its label.

Compare that with IMAP, where the same job is a SEARCH, a wait, and then a FETCH.

State strings

Every response for a data type includes a state string. Save it. Next time, call Email/changes with that state and the server returns only what changed: created, updated, and destroyed IDs. No more re-downloading the inbox to see what’s new.

If your state is too old for the server to compute a diff, it returns a cannotCalculateChanges error and you do a full refetch. That’s the defined recovery path. Don’t try to be clever and overwrite newer server data with your stale copy.

Push works the same way: the server says a state changed, you ask for the changes.

Still HTTP underneath

Because JMAP runs over HTTPS, everything you know about HTTP applies: authentication, certificate validation, status codes, request-size limits, timeouts.

One trap. A 200 OK doesn’t mean every method succeeded. The JSON body can contain a per-method error like ["error", { "type": "invalidArguments" }, "g1"] next to successful results. Check each method response, not just the HTTP status.

Try identifying the query call, the result reference, and the get call in the example above. Then add the state value your cache would store for a later incremental sync.

Lesson completed