Skip to content

feat: sync Masked Email addresses when the server offers the capability#280

Open
salmonumbrella wants to merge 1 commit into
maathimself:mainfrom
salmonumbrella:feat/masked-email-sendable
Open

feat: sync Masked Email addresses when the server offers the capability#280
salmonumbrella wants to merge 1 commit into
maathimself:mainfrom
salmonumbrella:feat/masked-email-sendable

Conversation

@salmonumbrella

@salmonumbrella salmonumbrella commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to the JMAP identity sync: when a session advertises Fastmail's vendor extension (https://www.fastmail.com/dev/maskedemail) on a usable primary account, the sync also mirrors the account's enabled Masked Email addresses into the private sendable set. The capability is detected, never required — a server without it (Stalwart, any standard JMAP server) takes the identical code path with zero masked rows, and the vendor capability only enters the JMAP using array for the one call that needs it.

Result: mail arriving at a Masked Email address → reply → the From is already the masked address, transiently. Masked addresses appear nowhere else — no settings list, no picker enumeration, no alias creation, and nothing is ever written back to the server.

Changes

  • jmapClient.js: sessionHasMaskedEmail() capability check + MaskedEmail/get behind it, with the same response-shape validation and same-origin token pinning as Identity/get
  • identitySync.js: enabled masks reconcile as kind='masked' rows in the same transaction as identities; a server that stops advertising the capability has its masked rows wiped on the next sync while identity rows are untouched
  • replySender.js / senderAuthorization.js: dropped an incorrect kind = 'identity' filter so the sendable-set lookup is kind-agnostic (kind only records how a row was sourced, never whether it's usable) — with a seam-proof test driving a real send from a masked-sourced row with no other changes to the send path
  • No frontend changes, no new strings, no new dependencies

Testing

  • Backend: 750/750 (vitest), lint clean (--max-warnings 0) — capability-absent path (never rejected, never requested), reconcile add/update/remove, capability-disappears wipe, disabled/pending/deleted masks excluded, malformed MaskedEmail/get rejected, tampered apiUrl never receives the token, end-to-end masked-row send
  • Frontend: 1381/1381 (node --test), lint clean, production build clean — no functional diff, re-run for completeness

Runtime-tested against a live Fastmail account with Masked Email addresses on a real mailbox. Stacked on #278 and #279 (their diffs are included here so the branch stands alone); I'll rebase this down to just its own changes as they land.


Contributor License Agreement

By submitting this pull request I confirm that:

  • I have read and agree to the Contributor License Agreement.
  • My contribution is my own original work (or I have identified any
    third-party material and confirmed it is compatible with the CLA).
  • I have the right to submit this contribution under the terms of the CLA.

…ability

Extends the JMAP identity sync to also mirror Fastmail's Masked Email
addresses into the private sendable_addresses set, but only when a JMAP
session actually advertises the vendor capability
(https://www.fastmail.com/dev/maskedemail) on a usable primary account.
Servers without it -- Stalwart, or any standard JMAP server -- are completely
unaffected: the capability is detected, never required.

Only 'enabled' masks are synced (pending/disabled/deleted are not currently
usable to send as); the display name falls back to the mask's forDomain when
no description is set. If the server stops advertising the capability, every
previously-synced masked row for that account is removed on the next sync,
while identity rows are untouched -- the two reconcile together in one
transaction. A masked-email provider_id can collide with an identity's
provider_id without conflict, since the table's uniqueness is scoped per kind.

Fixed a latent bug this surfaced: replySender.js and senderAuthorization.js
both filtered sendable_addresses on kind = 'identity', which would have
silently hidden every masked row from reply resolution and send
authorization. Both are now kind-agnostic -- kind only disambiguates how a
row was sourced, never whether it's usable.

No UI surfaces, no masked-email creation, no per-mask settings -- a read-only
mirror of what already exists on the server, like the identity sync itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@salmonumbrella
salmonumbrella marked this pull request as ready for review July 16, 2026 10:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant