Reference
JMAP
JMAP per RFC 8620 (core) and RFC 8621 (mail), over the same HTTPS port as the panel.
JMAP is the modern one: a single endpoint taking batched method calls, with back-references so a client can query and fetch in one round trip, and state strings so it can ask what changed rather than re-walking a mailbox.
Session#
curl -u you@example.com:mailbox-password https://mail.example.com/.well-known/jmapAuthentication is the address identity — HTTP Basic with the mailbox credential, which is what every JMAP client in the wild sends. A browser client carrying the corsair_webmail cookie is also accepted.
The session resource advertises:
| Capability | |
|---|---|
urn:ietf:params:jmap:core |
|
urn:ietf:params:jmap:mail |
|
urn:ietf:params:jmap:submission |
Capabilities are advertised rather than stubbed — a client has to be able to discover what is really there.
Method calls#
curl -u you@example.com:password https://mail.example.com/jmap \
-H 'content-type: application/json' \
-d '{
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
"methodCalls": [
["Mailbox/get", { "accountId": "u1", "ids": null }, "0"]
]
}'Supported methods#
| Method | |
|---|---|
Core/echo |
|
Mailbox/get |
|
Mailbox/query |
|
Mailbox/set |
Create, update, destroy |
Email/get |
|
Email/query |
Filter and sort |
Email/set |
Including moving between mailboxes |
Email/changes |
What changed since a state string |
Thread/get |
|
Identity/get |
|
EmailSubmission/set |
Send |
Back-references#
The reason JMAP is worth using. Query and fetch in one round trip, with the second call referring to the first's result:
{
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
"methodCalls": [
["Email/query", {
"accountId": "u1",
"filter": { "inMailbox": "inbox-id" },
"sort": [{ "property": "receivedAt", "isAscending": false }],
"limit": 20
}, "q"],
["Email/get", {
"accountId": "u1",
"#ids": { "resultOf": "q", "name": "Email/query", "path": "/ids" },
"properties": ["subject", "from", "receivedAt", "preview"]
}, "g"]
]
}One request, one response, twenty message summaries. Over IMAP that is a SEARCH followed by a FETCH, and over a slow link the difference is noticeable.
Incremental sync#
Email/changes takes the state string from a previous response and returns what has been created, updated, and destroyed since.
["Email/changes", { "accountId": "u1", "sinceState": "42" }, "c"]That is the JMAP answer to IMAP's CONDSTORE, and it is a good deal simpler to implement correctly on the client side.
Sending#
["EmailSubmission/set", {
"accountId": "u1",
"create": {
"s1": { "emailId": "draft-id", "identityId": "i1" }
}
}, "s"]Submission runs the same path as SMTP submission: the From address is proven to belong to the caller, the daily limit is checked, the message is DKIM-signed, a copy is filed in Sent, and delivery is queued.
Blobs#
Download:
GET /jmap/download/:accountId/:blobId/:nameUpload:
POST /jmap/upload/:accountIdBounded by MAX_MESSAGE_BYTES — 50 MB by default. Over it, the response is urn:ietf:params:jmap:error:limit with "limit": "maxSizeUpload".
Errors#
Request-level problems use JMAP's own error types rather than the panel API's envelope:
| Type | Status | Means |
|---|---|---|
urn:ietf:params:jmap:error:unauthorized |
401 | Bad or missing credentials |
urn:ietf:params:jmap:error:notJSON |
400 | The body did not parse |
urn:ietf:params:jmap:error:limit |
400 | Over maxCallsInRequest or maxSizeUpload |
urn:ietf:params:jmap:error:notFound |
404 | No such blob or part |
Method-level errors come back in the response array, per call, as JMAP requires — one failing method does not fail the batch.
The same store#
JMAP reads and writes the same messages and folders rows as IMAP, POP3, and the webmail. There is no per-protocol copy and no synchronisation step.
A JMAP client and an IMAP client on the same mailbox see each other's changes immediately. So do the webmail and a phone.
This is also why MOVE preserves the row id: JMAP requires an Email's id to survive a change of mailbox, and IMAP is satisfied either way. Implementing move as copy-then-expunge would satisfy IMAP and silently break JMAP.
Clients#
Any RFC 8621 client should work. The session resource is at https://mail.example.com/.well-known/jmap, with the full email address and mailbox password as Basic credentials.
Not implemented#
urn:ietf:params:jmap:vacationresponse |
Use a Sieve filter |
urn:ietf:params:jmap:calendars, contacts |
Corsair does email |
SearchSnippet/get |
|
Push (EventSource, PushSubscription) |
Poll Email/changes |
A client that asks for an unadvertised capability gets told it is not there, which is what capability advertisement is for.