feat: JMAP identity sync (host-configurable) with a Fastmail provider profile - #279
Open
salmonumbrella wants to merge 1 commit into
Open
feat: JMAP identity sync (host-configurable) with a Fastmail provider profile#279salmonumbrella wants to merge 1 commit into
salmonumbrella wants to merge 1 commit into
Conversation
Adds a read-only JMAP identity sync (Identity/get only, RFC 8620/8621) that answers one question per account: which addresses is it allowed to send as. Results live in a private sendable_addresses table that is never rendered as a list anywhere in the UI. On reply, if the delivered-to address is in the sendable set, the From is set transiently for that one draft only -- nothing is auto-created or persisted. At send time the server re-authorizes any explicit From against the same set. This is the inverse of the previously-closed Fastmail-specific approach (maathimself#256): the JMAP session URL is a per-account setting, not a hardcoded host; only the standard core + submission capabilities are required, everything else (Masked Email included) is ignored rather than demanded; and Fastmail is one provider profile among others, prefilling IMAP/SMTP/JMAP settings the way the existing google/apple/purelymail profiles already do. The session URL goes through the same SSRF guards as every other user-configured host (validateHost + safeFetch, HTTPS unless the admin policy admits a genuinely private host). Existing behavior is unchanged: the send request keeps its current accountId/aliasId shape, with an optional fromEmail that is mutually exclusive with aliasId and requires server-side authorization (422 SENDER_UNAVAILABLE on a miss). The token is encrypted at rest like auth_pass, never returned to the client, and clearing it wipes every synced address. Migration 0036 is IF NOT EXISTS-guarded, numbered after 0035. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 tasks
salmonumbrella
marked this pull request as ready for review
July 16, 2026 10:53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Re-cut of #256 along the direction laid out there: host-configurable, capabilities optional, providers as profiles. A per-account, read-only JMAP identity sync answers one question — which addresses may this account send as — and stores the result in a private
sendable_addressestable that is never rendered as a list anywhere (no settings pollution, no From-picker enumeration — the objections to the previous cut). On reply, when the delivered-to address is in that set, the From is set transiently for that one draft; at send time the server authorizes the explicit From against the same set. So "delivered to X" alone never grants "send as X": a mailing-list delivery resolves to nothing and replies from the primary, as today.The JMAP client requires only
urn:ietf:params:jmap:core+urn:ietf:params:jmap:submission(RFC 8620/8621 — Stalwart-compatible), takes its session URL from account config, and ignores capabilities it doesn't know (Masked Email included — split into a follow-up so this layer stays vendor-neutral). Fastmail is one provider profile: a settings preset (IMAP/SMTP hosts + JMAP session URL prefill) and animapManagerruntime profile. The scope is deliberately tiny — identity listing only, no JMAP mail transport — so it can fold into a future general JMAP layer.Off unless a user configures a token; existing accounts and the existing send request shape are unchanged.
Changes
sendable_addresses(private authorization set) + JMAP session URL / encrypted token / sync-status columns onemail_accountsjmapClient.js: session load +Identity/getwith response-shape validation; the session-advertisedapiUrlis pinned same-origin to the session URL so the Bearer token can never travel to another hostvalidateHost+safeFetch(per-hop IP validation), HTTPS required unless the host is genuinely private and the admin's connection policy allows private hosts — same rule as the CardDAV server URLidentitySync.js: read-only reconcile (add/update/remove by provider id), including*@domaincatch-all identities; runs on save and via a rate-limited manual refresh endpoint; no background schedulersend.js: optionalfromEmail, mutually exclusive withaliasId(400 if both); authorized against the sendable set (exact, else wildcard cover) → 422SENDER_UNAVAILABLEon a miss; thealiasIdpath is byte-for-byte untouchedGET /messages/:id/reply-sender: resolves the transient reply From from the delivered-to capture (one-shot re-sync when stale); returns a single sender or null — never a listauth_pass, surfaced only as a "configured" flag; clearing it wipes every synced row — in the same save as any other edited fields), plus last-sync/error status and a refresh buttonimapManagerruntime profile (fastmail.com/messagingengine.com)Testing
--max-warnings 0) — session/capability validation incl. a Stalwart-style custom host, apiUrl origin pinning, SSRF host-policy cases (private host blocked by default, plaintext-HTTP-to-public blocked even with private hosts allowed), reconcile add/update/remove/wipe, sender authorization (exact / wildcard / miss / header-injection), token clear + field update in one save, token never present in any response or errorRuntime-tested against a live Fastmail account on a real mailbox: token setup and validation, identity sync, delivered-to reply-From selection, and sending. Stacked on #278 (its diff is included here so the branch stands alone); I'll rebase this down to just its own changes once #278 lands.
Contributor License Agreement
By submitting this pull request I confirm that:
third-party material and confirmed it is compatible with the CLA).