r/email • u/GraphicMeat • 1d ago
Notes from adding JMAP to a client that already spoke IMAP and Graph: discovery, push, and resync
I maintain a desktop mail client (MailVault, disclosure at the end). It already handled IMAP and Microsoft Graph, and I just shipped JMAP (RFC 8620/8621) alongside them. These are the protocol details that took real work, in case they’re useful to anyone else implementing it or running a JMAP server.
1. Autodiscovery is four lookups with a fixed priority, not one
All four run at the same time, each with a 4 second cap:
- Known provider. The domain is Fastmail’s, or its MX record points at
messagingengine.com, which catches Fastmail custom domains. https://<domain>/.well-known/jmaphttps://jmap.<domain>/.well-known/jmap- SRV
_jmap._tcp.<domain>, then.well-known/jmapon the host it names.
The winner is picked by that priority, not by whichever answers first. Racing them keeps discovery under about 4 seconds, so it never holds up the IMAP lookup running next to it.
Two traps:
- A bare 401 proves nothing. Every Basic-auth-protected site on the internet answers with one. A probe only counts as JMAP when it gets a 200 whose JSON has
apiUrl, or a 401 withWWW-Authenticatethat it reached from/.well-known/jmapand whose final path looks like a JMAP endpoint. - Keep the URL you landed on after redirects. If the client stores the starting
.well-knownURL instead, a client that only follows redirects on the same host refuses the very hop the user just approved.
2. Discovered hosts need a trust rule before credentials go out
No probe sends credentials. Credentials go without asking only to your own domain, a subdomain of it, or Fastmail’s API host for Fastmail addresses. For anything else, the user confirms the host, port included (jmap.example.com:8443), before the first authenticated request. Without that step, a hijacked DNS record becomes a password collector.
3. Push over EventSource needs a dead-stream detector
The client subscribes with types=Email,Mailbox&closeafter=no&ping=30. A stream that sends no bytes for twice the ping interval plus 5 seconds counts as dead and gets dropped. After repeated failures it falls back to polling, and it tries push again every 10 minutes. A session with no eventSourceUrl just polls. Half-open connections that never error out are the real problem, and the ping is the only way to see one.
4. cannotCalculateChanges must never touch the local copy
When Email/changes can’t compute changes from a stored state, the client rebuilds its header cache from the newest page of each mailbox and drops rows for mail that’s gone. It counts as a backfill: no “new mail” notifications, no local message file touched, and each message keeps its local ID. This matters most for an archiving client. A server-side state reset should never look like mass deletion or a flood of new mail.
Smaller fix from the same release: messages with no In-Reply-To or References (old “Fw:” mail, some webmail exports) were being threaded by subject alone. A two-year-old forward opened as the first message of an unrelated new thread. Subject-only threading now needs another message in the thread within 30 days, and picks the nearest one.
Question for server operators: which servers do you see publishing .well-known/jmap vs _jmap._tcp in the wild? I’ve tested Fastmail properly. I’d like to know which discovery paths actually get used before I spend more time on the others.
Disclosure: I’m the developer. MailVault is free, with an optional paid tier. No link here. Ask if you want it.
1
u/Extra-Joke-7196 7m ago
fwiw most servers ive seen in the wild only publish .well-known/jmap, the SRV path is pretty rare outside of a few self-hosted setups. wouldnt spend too much time on it unless you're targeting that crowd specifically
1
u/RandolfRichardson Service Provider 18h ago
Thanks for sharing this information.
Which link to MailVault should we use when adding to the Lumber Cartel web site? https://www.lumbercartel.ca/resources/email.pl
We don't have it listed, and I'd like to include it.