docs: check webmail 1.10.0 to 1.18.0 against the app

This commit is contained in:
Linus Rath
2026-10-29 02:07:01 +01:00
parent 24232bf2e9
commit ec7e8e0530
2 changed files with 445 additions and 2 deletions
+4 -2
View File
@@ -98,9 +98,11 @@ the audit's "Fix pass, 2026-09-24" section.
| 07 | Filters (Sieve), vacation responder, Files | [docs/parity/07-filters-vacation-files.md](docs/parity/07-filters-vacation-files.md) | 32 | 29 | 3 | 3 | 8 | 21 |
| 08 | Settings, sync, push, i18n, themes, updates, misc UI | [docs/parity/08-settings-push-i18n-ui.md](docs/parity/08-settings-push-i18n-ui.md) | 46 | 38 | 8 | 0 | 14 | 32 |
| 09 | JMAP client core, live sync, offline, security, S/MIME | [docs/parity/09-jmap-core-sync-security.md](docs/parity/09-jmap-core-sync-security.md) | 56 | 54 | 2 | 7 | 23 | 26 |
| | **Total** | | **415** | **380** | **35** | **32** | **138** | **244** |
| 10 | Webmail 1.10.0 to 1.18.0 and Unreleased (all areas) | [docs/parity/10-webmail-1.10-to-1.18.md](docs/parity/10-webmail-1.10-to-1.18.md) | 112 | 0 | 112 | 10 | 40 | 62 |
| | **Total** | | **527** | **380** | **147** | **42** | **178** | **306** |
Counts are of the `- [ ]` and `- [x]` items per file as of 2026-09-24. Done and
Counts are of the `- [ ]` and `- [x]` items per file as of 2026-09-24 (row 10:
2026-10-29). Done and
Open split them by tick; the P columns count the priority tags on those items
(one item carries none).
+441
View File
@@ -0,0 +1,441 @@
# Webmail 1.10.0 to 1.18.0 (and Unreleased)
## Summary
The last parity pass was against webmail 1.9.2. This file walks every changelog entry from 1.10.0 to 1.18.0, the Unreleased section and the commits after the 1.18.0 release (b0d1a97bb..main, 2026-10-29), checked against native `feat/stalwart-1.0` with main merged in (f1db6b48). Web paths are relative to the webmail root, native paths to this repo.
What stands out:
- Several webmail security fixes never reached the app: the HTML sanitizer lets `<img src=x/onerror=…>` through, a sender can fake a DMARC pass in the auth chips, mailto unsubscribe sends to every address, a crafted winmail.dat freezes the JS thread, and the webmail hand-off still puts the password in the `bulwarkmobile://` fragment.
- Sieve interop broke in both directions: the app reads webmail 1.13+ scripts (metadata v2) as raw Sieve and its "Reset to visual builder" wipes them, and a save on the phone broadens "is the address" rules into substring matches.
- Account switches still leak: the composer follows a switch with an open draft (and its autosave can destroy another account's message), calendar, task and contact loads from the previous account land after a switch, Undo/Send now of a delayed send goes through the active login, and push renewal only reaches the active account.
- Everything the organisation policy drives (locked settings, session length, external recipient confirm and label, org signature and disclaimer) has no source on the phone: the policy is served by the webmail at `/api/admin/policy` behind its session cookie (`lib/auth/instance-details.ts:23`), and the app forgets which webmail it was paired with (`src/lib/oauth.ts:292`). These need a webmail change first (see "Needs a decision").
- Features that live on the JMAP server and only need porting: shared tags and team templates (JMAP Metadata `org.bulwarkmail/*`), recover deleted items (`x:ArchivedItem`), delegates (`shareWith`), Files trash, save attachment to Files, scheduled message view.
Classification: `bug` = fixed in the webmail, still live in the app; `missing`; `partial`. Not applicable on mobile and left out: admin panel, plugins and marketplace, command palette, keyboard shortcuts, print, Bulwark Lite, CSP/third-party switch, Mountain View, desktop-only layout (drag and drop, hover, multi-pane), server routes the app doesn't use, Docker, docs and translation-only entries.
Stalwart 1.0.0 items found in the same pass (1.0 SSO scopes, Files on 1.0, #1385) are tracked with the Stalwart 1.0 work and not repeated here.
## Findings
### Security
- [ ] **HTML sanitizer lets `on*` handlers through after a slash** — `P1` — `bug`
- What WEB does: DOMPurify for mail and drafts (`lib/email-sanitization.ts`, GHSA-g35q-3gpc-x2rr).
- What RN does: the handler regex needs whitespace before `on*` (`src/lib/email-html.ts:142-145`), so `<img src=x/onerror=…>` survives. The viewer WebView (`src/components/EmailBodyView.tsx:793`) and the editor (`src/lib/editor-html.ts:37`, seeded at `src/screens/ComposeScreen.tsx:557`) allow inline script; a received mail can navigate without a tap (`EmailBodyView.tsx:841-843`).
- Fix hint: drop every `on*` attribute whatever precedes it (or parse), and stop allowing inline handlers.
- [ ] **A sender can fake a DMARC/DKIM pass in the auth chips** — `P1` — `bug`
- What WEB does: splits Authentication-Results on `;` outside quotes and comments, reads the method at the start of each part, takes DKIM/DMARC from the topmost header only (`lib/email-headers.ts:231`, 8f7124c87).
- What RN does: matches `dkim=`/`dmarc=` anywhere in all headers joined (`src/lib/email-headers.ts:143`, `:152`, `:265`); this also feeds invitation trust.
- Fix hint: port the web parser.
- [ ] **mailto unsubscribe sends to every address and the confirm hides it** — `P1` — `bug`
- What WEB does: one address, ignores `to=`/`cc=`, caps subject and body, shows them in the confirm (`lib/validation.ts:180`, `components/email/unsubscribe-banner.tsx:42`, f0aa0eeda).
- What RN does: sends to every to/cc and `to=` address with the sender's subject and body (`src/lib/unsubscribe.ts:95-113`); the confirm only says an email will be sent (`src/components/email/UnsubscribeBanner.tsx:139-147`).
- Fix hint: port parseUnsubscribeMailto and the confirm text.
- [ ] **A crafted winmail.dat freezes the JS thread** — `P1` — `bug`
- What WEB does: clamps the MAPI value count and stops on a read that doesn't advance (`lib/tnef.ts:173`, 012fe6eda).
- What RN does: trusts the count (up to 2^32) and loops on non-advancing reads (`src/lib/tnef.ts:201`, `:211`).
- Fix hint: clamp to remaining/4, break on a null or non-advancing read.
- [ ] **Webmail hand-off returns the password in the `bulwarkmobile://` fragment** — `P2` — `bug` (cross-repo)
- What WEB does: `app/(main)/[locale]/login/page.tsx:426`, `lib/auth/mobile-handoff.ts:31` put the password in the redirect.
- What RN does: reads it from the fragment (`src/lib/oauth.ts:247`); any app can claim a custom scheme.
- Fix hint: hand back a one-time code and redeem it like QR pairing (`src/lib/oauth.ts:435`, `/api/auth/pair/redeem`). Needs the webmail side first.
- [ ] **Sender favicons are fetched from icons.duckduckgo.com on the device** — `P3` — `partial` (privacy)
- What WEB does: proxies favicons on the server (`app/api/favicon/route.ts:484`).
- What RN does: fetches directly, on by default (`src/lib/avatar-utils.ts:149`, `src/stores/settings-store.ts:312`), leaking sender domains and the device IP.
- Fix hint: default off, or use the paired webmail's proxy.
### Accounts and sign-in
- [ ] **Org user policy and "Set by your organisation"** — `P2` — `missing` (blocked)
- What WEB does: locks or hides settings per domain or group (`stores/policy-store.ts:110`, `hooks/use-restriction.ts:22`, `components/settings/org-locked-note.tsx:14`), incl. colour mode #1280, archiving off #1257, tags above shared mailboxes #1334, share rights, hiding delegates or recovery.
- What RN does: nothing (`src/stores/settings-store.ts:453`); the paired webmail is forgotten (`src/lib/oauth.ts:292`).
- Fix hint: keep the webmail URL at hand-off/pairing and fetch the policy with a bearer token. Needs a webmail route that accepts a JMAP bearer token. See "Needs a decision".
- [ ] **Admin-set idle sign-out and maximum session length** — `P2` — `missing` (blocked)
- What WEB does: `components/session/session-timeout-guard.tsx:94`, `lib/session-policy/limits.ts:36`, refuses to restore over-age sessions (6e5d8faec), saves drafts first (79c697a93).
- What RN does: stays signed in for good (`src/stores/auth-store.ts:929`, `App.tsx:818`).
- Fix hint: needs the policy; then an AppState idle timer and a max-age check in restoreSession, drafts flushed first.
- [ ] **With TOTP on, changing the password and turning TOTP off fail** — `P2` — `bug`
- What WEB does: asks for the current code and sends it (`components/settings/account-security-settings.tsx:52`).
- What RN does: sends neither (`src/api/account-security.ts:346-353`, `:387-395`; `src/components/settings/AccountSecuritySettings.tsx:292`); Stalwart answers "Current OTP code is required".
- Fix hint: ask for the code and patch `otpAuth/otpCode` (not the whole object).
- [ ] **A full sign-out keeps the search history** — `P2` — `bug`
- What WEB does: `stores/auth-store.ts:1301`, `lib/sign-out-cleanup.ts:22`.
- What RN does: clearAllFeatureStores (`src/stores/auth-store.ts:126-134`) never clears `src/stores/search-history-store.ts:44`.
- Fix hint: clear it, and check the other persisted stores in the same pass.
- [ ] **Internationalized domains** — `P3` — `missing`
- What WEB does: `lib/idn.ts:25` (#1100). What RN does: ASCII-only checks (`src/lib/recipients.ts:31`, `src/lib/server-discovery.ts:78`).
- Fix hint: toASCII on the wire, Unicode on screen.
- [ ] **Sign-out doesn't end the identity provider session** — `P3` — `missing`
- What WEB does: `app/api/auth/token/route.ts:90` (#905). What RN does: `src/lib/oauth.ts:219`, `src/lib/oauth-native.ts:158`.
- Fix hint: open end_session_endpoint, or use an ephemeral browser session.
- [ ] **Explain a refused token exchange on TOTP sign-in** — `P3` — `partial`
- What WEB does: `stores/auth-store.ts:153`, `:1432`. What RN does: `src/lib/totp-login.ts:148`, `:156` throw token_exchange_failed; `src/lib/login-errors.ts:129` has no copy.
- [ ] **Shared account settings scope for calendar and contacts** — `P3` — `missing` (old 01 item)
- What WEB does: `components/settings/account-settings.tsx:32`. What RN does: `src/lib/capabilities.ts:86` covers filters and vacation only.
### Mail list and folders
- [ ] **The list stops loading older mail when the server sends no total (#1364)** — `P2` — `bug`
- What WEB does: keeps paging while full pages come back (`stores/email-store.ts:6508-6515`).
- What RN does: a missing total becomes 0 (`src/api/email.ts:529`, `:757`) and paging stops (`src/stores/email-store.ts:1113`).
- [ ] **View scheduled messages; Cancel send puts them back in Drafts (native #84)** — `P2` — `partial`
- What WEB does: opens scheduled mail with a banner (Send now, Reschedule, Cancel) (`components/mail/mail-app.tsx:2523`, `components/email/email-viewer.tsx:773`, f7adbf1ef, 780b64461, 831a3e6b8).
- What RN does: a row tap only opens the action sheet (`src/screens/ScheduledScreen.tsx:278`); Cancel leaves the copy in Sent (`:129-132`, `src/api/email.ts:2079`).
- Fix hint: open the row in the thread screen with a scheduled banner; make Cancel do the cancel-to-Drafts that Edit already does.
- [ ] **Shared tags in group mailboxes (#1310, #470)** — `P2` — `missing`
- What WEB does: reads and writes `org.bulwarkmail/tags` via JMAP Metadata on the group account (`lib/shared-tags.ts:185`, `components/settings/shared-tag-settings.tsx`, `lib/jmap/bulwark-metadata.ts`).
- What RN does: local tag names only (`src/stores/keywords-store.ts:5`, `src/components/settings/KeywordSettings.tsx`).
- Fix hint: read the group's tags through Metadata (Stalwart 1.0 implements it; the 0.16 guard from 42751a6b stays). Locking needs the policy.
- [ ] **Search leaves out Spam and Trash by default** — `P2` — `missing`
- What WEB does: `inMailboxOtherThan` trash/junk unless "All folders" is picked (`lib/unified-mailbox.ts:98`, 763732fdc, 3904af2cb).
- What RN does: no mailbox filter (`src/stores/email-store.ts:268-271`, `:492`).
- [ ] **Unified "All mail" search leaves out Sent, Archive and Drafts** — `P2` — `bug`
- What WEB does: with a query, excludes only trash and junk (`lib/unified-mailbox.ts:510`).
- What RN does: applies the view exclusions also with a query (`src/api/unified-inbox.ts:353`, `:35`).
- [ ] **Undo after a move to another account and in the unified inbox** — `P3` — `partial`
- What WEB does: `stores/email-store.ts:5177` (ed9fa2de4), `lib/email-action-toast.ts` (6c5aead20).
- What RN does: Undo exists within an account and in spanning lists, not after crossAccountMove (`src/stores/email-store.ts:1540-1550`, `:1786-1796`) or for `src/screens/UnifiedInboxScreen.tsx:246-270`.
- [ ] **Archive without an Archive folder; unified archive modes (#1176, #1178)** — `P3` — `missing`
- What WEB does: creates Archive and reuses year/month folders (`stores/email-store.ts:1332`, `:2561`).
- What RN does: returns silently (`src/stores/email-store.ts:1594`, `:1729`) or throws (`src/api/unified-inbox.ts:546`).
- [ ] **A server's own "all" mailbox is included in All mail/Unread (#1241)** — `P3` — `bug`
- What WEB does: role `all` excluded (`lib/jmap/types.ts:1158`). What RN does: `src/api/unified-inbox.ts:34`, `:354`.
- [ ] **Tag views and counts include Trash and Spam (#1156)** — `P3` — `bug`
- What WEB does: `lib/unified-mailbox.ts:104`, `:577`. What RN does: `src/api/tag-counts.ts:37-48`, `src/stores/email-store.ts:455`.
- [ ] **User folders named like system folders get system icons (#1235, native #80)** — `P3` — `bug`
- What WEB does: icons by role only (`components/layout/sidebar.tsx:168`). What RN does: name matching fallback (`src/components/SidebarDrawer.tsx:92`).
- [ ] **Sort by "Date sent" falls back to received date in merged lists (#1219)** — `P3` — `bug`
- What WEB does: lists fetch sentAt (`lib/jmap/client.ts:443`). What RN does: EMAIL_LIST_PROPERTIES lacks it (`src/api/email.ts:22`; comparator `src/lib/message-list-order.ts:225`).
- [ ] **Previews start with a style sheet (#1136)** — `P3` — `bug`
- What WEB does: cleanPreview (`lib/utils.ts:320`, `:363`). What RN does: `src/screens/EmailListScreen.tsx:270`, `src/lib/push-background-task.ts:513`.
- [ ] **Paperclip on messages with only inline images** — `P3` — `bug`
- What WEB does: `components/email/attachment-chips.tsx:165`. What RN does: `src/screens/EmailListScreen.tsx:237`, `src/screens/UnifiedInboxScreen.tsx:358` use hasAttachment.
- [ ] **List row grows when its attachment chips arrive** — `P3` — `bug`
- What WEB does: reserves the row (7b25d3a95). What RN does: `src/components/email/ListAttachmentChips.tsx:89`.
- [ ] **Cold start reopens the last folder or search instead of the inbox** — `P3` — `bug`
- What WEB does: 64b39c504. What RN does: `src/stores/email-store.ts:2195`, `:829`.
- [ ] **List keeps its scroll position on folder, tag or search switch (#1138)** — `P3` — `bug` (from the code, not seen on a device)
- What RN does: `src/screens/EmailListScreen.tsx:1590`.
- [ ] **Unified inbox live updates for other logins (#1350)** — `P3` — `partial`
- What RN does: `src/screens/UnifiedInboxScreen.tsx:154-167` listens to the active login only.
- [ ] **Copy to another account (#1150); move several folders (#1173); move menu order in a shared mailbox (#1149)** — `P3` — `missing`
- What WEB does: `hooks/use-message-file-actions.ts:94`, `components/settings/folder-settings.tsx:202`. What RN does: `src/components/MoveSheet.tsx:31`, `src/components/settings/FolderSettings.tsx:137-173`, `src/lib/mailbox-tree.ts:162-205`.
- [ ] **Hide shared mail accounts (#1228); remove from this folder (#615); folder name on all-folder results (#1172); own folders section (#1205); per-folder counter (#982); hide account dot (#912)** — `P3` — `missing`
- What WEB does: `lib/shared-mail-visibility.ts:19`, `hooks/use-remove-from-folder.ts:15`, `stores/settings-store.ts:695`. What RN does: `src/components/SidebarDrawer.tsx:124-150`, `:672`; `src/screens/EmailListScreen.tsx:103`.
- [ ] **Search folder picker is flat and capped at 12** — `P3` — `partial`
- What WEB does: `lib/search-scope-folders.ts`. What RN does: `src/screens/EmailListScreen.tsx:1729-1740` (`slice(0, 12)`).
- [ ] **Tags without a definition are grey; no account tint (#1052); tag visibility, nesting, order** — `P3` — `partial`
- What WEB does: `hooks/use-keyword-format.ts`, `stores/settings-store.ts:391`. What RN does: `src/screens/EmailListScreen.tsx:164`, `src/stores/keywords-store.ts:9`.
- [ ] **Today/yesterday follow the time zone setting (#1259); .eml import and cross-account moves keep the date (#891)** — `P3` — `partial`
- What RN does: `src/lib/date-format.ts:111-115` (device zone); `src/api/email.ts:1059-1072` sends no receivedAt.
- [ ] **Reorder folders; custom folder icons** — `P3` — `missing` (old 02 items)
- What WEB does: `stores/email-store.ts:7502`, `components/settings/folder-settings.tsx:261`, `:701`. What RN does: `src/components/settings/FolderSettings.tsx:350`, `:32` (`src/api/email.ts:297` already takes sortOrder).
- [ ] **RTL swipe sides** — `P3` — `partial` (old 08 item; web doesn't swap either)
- What RN does: `src/components/SwipeableRow.tsx:353`.
- [ ] **`/mail/folder/<ref>` deep link opens only the tab** — `P3` — `bug`
- What RN does: `src/navigation/linking.ts:134` parses it, `:285-287` drops it.
### Reading mail
- [ ] **Unverified-sender warning, badge, no "Always trust" for forged mail** — `P2` — `partial`
- What WEB does: getSenderVerification (`lib/email-headers.ts:112`), banner and badge (`components/email/email-viewer.tsx:4037`).
- What RN does: a spoof line in the collapsed details only, first DKIM signature only, trust still offered (`src/components/email/MessageHeader.tsx:108`, `:273`; `src/lib/email-headers.ts:99`; `src/components/EmailBodyView.tsx:771`).
- [ ] **External sender chip** — `P2` — `missing` (blocked on the policy)
- What WEB does: `lib/external-mail.ts:119`, `components/email/external-sender-chip.tsx`, `components/email/email-viewer.tsx:903`.
- What RN does: `src/components/email/MessageHeader.tsx:159`, nothing.
- Fix hint: internal and partner domains come from the policy; identity domains would only be a fallback.
- [ ] **Invitation trust uses unaligned SPF/DKIM and flags colleagues (#1374)** — `P2` — `bug`
- What WEB does: alignment check and own-domain exemption, REPLY/COUNTER checked against the answering attendee (`lib/calendar-invitation.ts:210-216`, `:400-422`).
- What RN does: `src/lib/calendar-invitation.ts:325-328`, `:375-376`.
- [ ] **Copy chip for sign-in and verification codes** — `P2` — `missing`
- What WEB does: `lib/verification-code.ts:275`, `components/email/email-viewer.tsx:1378`, setting at `stores/settings-store.ts:540`.
- What RN does: nothing (`src/components/email/MessageHeader.tsx`, `src/screens/EmailListScreen.tsx`).
- [ ] **A mail full of blockquotes freezes the reader** — `P3` — `bug`
- What WEB does: single walk (`lib/quote-collapse.ts:92`). What RN does: quadratic walk (`src/lib/quote-collapse.ts:95-112`).
- [ ] **Attachment names keep direction-override characters** — `P3` — `bug`
- What WEB does: `lib/bidi.ts:10`. What RN does: `src/lib/attachment-display.ts:34`, `src/lib/email-export.ts:56`.
- [ ] **Part names that are paths (#1371, #1373)** — `P3` — `bug`
- What WEB does: `lib/jmap/download-name.ts:11`. What RN does: `src/api/jmap-client.ts:1350` puts `%2F` into the download URL.
- [ ] **Dark mode recolours instead of inverting** — `P3` — `missing`
- What WEB does: `lib/email-dark-mode.ts:48`, `:71`. What RN does: `filter: invert(1) hue-rotate(180deg)` (`src/lib/email-html.ts:500`); mail's own dark styles follow the system scheme (`:640`).
- [ ] **"via" badge repeats the sender's address** — `P3` — `bug`
- What WEB does: `components/email/email-identity-badge.tsx:69`. What RN does: `src/components/email/MessageHeader.tsx:116-117`, `:223`.
- [ ] **Fixed-width tables don't reflow (#1020)** — `P3` — `partial`
- What WEB does: `lib/email-fit-width.ts:111`. What RN does: shrinks the whole mail (`src/components/EmailBodyView.tsx:277-290`).
- [ ] **Invitation banner: View in calendar, collapse, counter-proposal review** — `P3` — `missing` (old 03 item)
- What WEB does: `components/email/calendar-invitation-banner.tsx:727`, `:463`, `:245`. What RN does: `src/components/email/CalendarInvitationBanner.tsx:153`.
- [ ] **Quick reply can be turned off (#1370)** — `P3` — `missing`
- What WEB does: `stores/settings-store.ts:560`. What RN does: always rendered (`src/screens/EmailThreadScreen.tsx:1233`).
### Composer and sending
- [ ] **An open draft follows an account switch; its autosave can destroy another account's message** — `P1` — `bug`
- What WEB does: pins the account when the composer opens (`components/email/email-composer.tsx:506`, d58344329).
- What RN does: reads Drafts/Sent, identities and `jmapClient.accountId` live (`src/screens/ComposeScreen.tsx:443-455`, `:778`, `:1355`; `src/api/email.ts:1899`, `:1928-1930`). A notification tap or deep link switches with the composer open (`App.tsx:117-120`); the next autosave creates the draft in the other account and destroys `previousDraftId` there.
- Fix hint: pin account, identities and mailboxes at open; pass the account to saveDraftVersion, sendEmail and uploads.
- [ ] **Undo or Send now of a delayed send goes through the active login** — `P1` — `bug`
- What WEB does: GHSA-8x23 (`components/mail/mail-app.tsx:2549`).
- What RN does: the pending send has no login (`src/stores/send-undo-store.ts:89`, `:109-118`; `src/screens/ComposeScreen.tsx:2202`); after a switch the cancel hits the same submission id on another server.
- Fix hint: store the login with the pending send; refuse or clear on a switch.
- [ ] **Reschedule and Send now create the replacement before cancelling (#1308)** — `P2` — `bug`
- What WEB does: cancels first, restores on failure (`lib/jmap/client.ts:11532-11664`).
- What RN does: creates first (`src/api/email.ts:2189`).
- [ ] **A send the server doesn't confirm is reported as sent (#1019)** — `P2` — `bug`
- What WEB does: `lib/jmap/client.ts:994`, `:5115`.
- What RN does: returns success without a submission id (`src/api/email.ts:1857`), drops the draft and closes (`src/screens/ComposeScreen.tsx:2158`).
- [ ] **Send and save drafts as a group or shared identity (#1090)** — `P2` — `missing` (old 04 item too)
- What WEB does: resolveSendAccountId (`lib/jmap/client.ts:6257`), identities of every account (`components/email/email-composer.tsx:214`, `:271`).
- What RN does: primary account's identities only (`src/api/identity.ts:17`, `src/screens/ComposeScreen.tsx:778`); sendEmail has no account (`:2151`).
- [ ] **Team templates, personal templates in the account, org template library** — `P2` — `missing`
- What WEB does: templates in JMAP Metadata with tombstones (`lib/templates/personal-templates.ts:141`, `lib/templates/team-templates.ts:57`, `lib/templates/org-library.ts:13`); on 1.0 the webmail moves them into the account.
- What RN does: local only (`src/stores/templates-store.ts:30`, `src/components/TemplateSheet.tsx`).
- Fix hint: read and write `org.bulwarkmail` templates via Metadata. The org library and locks need the policy.
- [ ] **Attach from Files (#1179)** — `P2` — `missing`
- What WEB does: `components/email/email-composer.tsx:4574`. What RN does: device picker only (`src/screens/ComposeScreen.tsx:1686-1711`).
- [ ] **External recipient confirm (composer, quick reply, invitations)** — `P2` — `missing` (blocked on the policy)
- What WEB does: `lib/outgoing/external-recipients.ts:53`, `:107`, `components/email/external-recipient-confirm.tsx`.
- What RN does: nothing (`src/screens/ComposeScreen.tsx:2151`, `src/components/email/QuickReplyBox.tsx`, `src/components/calendar/EventModal.tsx`).
- [ ] **Org signature with user details; legal disclaimer on every sent message** — `P2` — `missing` (blocked on the policy)
- What WEB does: added in the client before sending (`lib/org/outgoing.ts:73`, `:99`, `lib/org-signature.ts:311`, 0154e24ff).
- What RN does: mail from the phone goes out without the disclaimer (`src/screens/ComposeScreen.tsx:2151`, `src/lib/signature-utils.ts`).
- [ ] **Unfilled placeholder warning** — `P3` — `missing`
- What WEB does: findUnfilledPlaceholders with per-locale words and a confirm (`lib/placeholder-check.ts:115`, `components/email/email-composer.tsx:3261`), setting placeholderWarningEnabled.
- What RN does: nothing (`src/screens/ComposeScreen.tsx:2151`).
- [ ] **Signature swap when changing From** — `P3` — `bug`
- What WEB does: `components/email/email-composer.tsx:1148`, `components/email/compose-format.ts:71`.
- What RN does: an identity without a signature keeps the primary's (`src/lib/signature-utils.ts:83-91`, `src/screens/ComposeScreen.tsx:975`); typing under the signature breaks the swap (`src/lib/signature-utils.ts:245`).
- [ ] **Plain-text part of sent mail** — `P3` — `bug`
- What WEB does: `lib/html-to-text.ts:73`, `:169`. What RN does: "-- " trimmed to "--", "> " dropped, ordered lists bulleted (`src/lib/compose-html.ts:109`, `:112`, `:125`).
- [ ] **Composer prompts: missing-subject asks again, autosaved draft says "unsaved", no Discard for a reopened draft** — `P3` — `bug`
- What WEB does: `components/email/email-composer.tsx:3251`, `:3770`, `:3758`. What RN does: `src/screens/ComposeScreen.tsx:1983`, `:2135`, `:1501`, `:1454-1471`.
- [ ] **Offline send shows a raw network error; no offline send queue** — `P3` — `partial` (old 04/09 item)
- What WEB does: says offline and keeps the message (`components/email/email-composer.tsx:3661`). What RN does: `src/screens/ComposeScreen.tsx:2236`, `src/lib/send-failure-message.ts:57`, `src/stores/outbox-store.ts:53`.
- [ ] **Scheduled time shows a minute early; presets (tomorrow 13:00, next Monday 08:00)** — `P3` — `bug`/`partial`
- What WEB does: `lib/scheduled-send-label.ts:11`, `lib/schedule-presets.ts:20`. What RN does: `src/screens/ScheduledScreen.tsx:53`, `:222`; `src/screens/ComposeScreen.tsx:2046`.
- [ ] **Reply puts no cursor in the body; autofill offered in address fields** — `P3` — `missing`
- What WEB does: `components/email/email-composer.tsx:2885`. What RN does: `src/screens/ComposeScreen.tsx:2466`, `:2316`, `:2946`, `src/components/calendar/ParticipantInput.tsx:119`.
- [ ] **Composer extras: priority (#970), numeric date placeholders (#1284), @mentions (#1134), directory lookup (#1253), font size, pasted lists, DSN/REQUIRETLS** — `P3` — `missing`
- What WEB does: `lib/recipient-mentions.ts`, `components/email/plain-text-paste.ts`, `components/email/rich-text-editor.tsx:445`, `lib/jmap/client.ts:1392-1413`. What RN does: `src/lib/template-utils.ts:9-15`, `src/components/RichTextEditor.tsx:26`, `src/lib/editor-html.ts`.
- [ ] **The recipient splitter treats an escaped quote as closing** — `P3` — `bug`
- What WEB does: `lib/email-composer-utils.ts:276`. What RN does: `src/lib/recipients.ts:57-90` (reached only from IdentitySettings).
### Calendar
- [ ] **Calendar and task data from the previous account lands after a switch** — `P1` — `bug`
- What WEB does: epoch check (`stores/calendar-store.ts:956`).
- What RN does: in-flight fetchEvents and `tasksInFlight` survive reset (`src/stores/calendar-store.ts:742`, `:750`, `:786`; reset at `src/stores/auth-store.ts:830`); a tick or delete then writes to the new account by the old one's id.
- [ ] **Task reminders relative to the start use the due date (native #83, webmail #1328)** — `P2` — `bug`
- What WEB does: `lib/calendar-alerts.ts:343`. What RN does: `src/lib/calendar-alert-scheduler.ts:86`.
- [ ] **Daily series stop at a DST gap** — `P2` — `bug`
- What WEB does: candidates from the start's wall clock (`lib/recurrence-expansion.ts:420`, 34fb9d247).
- What RN does: `src/lib/recurrence-expansion.ts:526-531`, `:396-398`, `:709`.
- [ ] **Subscribed feed changes never reach imported events** — `P2` — `bug`
- What WEB does: calendar-feed-diff (`stores/calendar-store.ts:2012`). What RN does: known UIDs skipped (`src/stores/calendar-subscriptions-store.ts:222`, `src/stores/calendar-store.ts:1158`).
- [ ] **Invitation time in the wrong zone (#1252)** — `P2` — `bug`
- What WEB does: `lib/calendar-invitation.ts:701`. What RN does: `src/components/email/CalendarInvitationBanner.tsx:175`.
- [ ] **Default reminders per calendar and email reminders (#1209)** — `P2` — `partial`
- What WEB does: `components/settings/calendar-management-settings.tsx:184`, `components/calendar/event-modal.tsx:448`, `lib/calendar-alerts.ts:175`.
- What RN does: fires a calendar's defaults (`src/lib/calendar-alert-scheduler.ts:96`) but can't set them and new events don't start with them (`src/components/calendar/EventModal.tsx:266`).
- [ ] **Search events** — `P2` — `missing`
- What WEB does: `components/calendar/calendar-search.tsx:82`. What RN does: `src/screens/CalendarScreen.tsx`.
- [ ] **Zero-length events run to 23:59 (#837)** — `P3` — `bug`
- What RN does: `src/stores/calendar-store.ts:144`, `src/lib/calendar-utils.ts:116`. Fix: `duration || 'PT0S'`.
- [ ] **.ics export breaks emoji and colon names** — `P3` — `bug`
- What RN does: `src/lib/calendar-ics-export.ts:9`, `:160`, `:167`. Fix: port foldLine and parameter quoting.
- [ ] **Blank participant names are written (#748)** — `P3` — `bug`
- What WEB does: `lib/calendar-participants.ts:368`. What RN does: `src/lib/calendar-participants.ts:360`, `:385`, `:484`.
- [ ] **Offer to save without invitations when the server refuses them** — `P3` — `partial`
- What WEB does: `lib/jmap/scheduling-error.ts:7`. What RN does: `src/components/calendar/EventModal.tsx:385-390`.
- [ ] **Moving a task into or within a shared calendar (#1204)** — `P3` — `partial`
- What WEB does: `lib/jmap/client.ts:9473`. What RN does: updateTask sends unmapped calendarIds (`src/stores/calendar-store.ts:1311-1315`).
- [ ] **Working hours (#1164); week all-day area (#1122); light tinted colours; Jalali; birthday colour; copy title/append note; component set on new calendars (#760); date deep links; background reminder refresh without device sync** — `P3` — `missing`/`partial`
- What RN does: `src/components/calendar/WeekView.tsx`, `src/components/calendar/TimeGridScrollView.tsx:177-185`, `src/lib/birthday-calendar.ts:6`, `src/components/calendar/EventDetailSheet.tsx:422`, `src/api/calendar.ts:1150`, `src/navigation/linking.ts:151`, `:288-299`, `src/lib/push-notifications.ts:243`.
- [ ] **Free/busy, CalendarEventNotification, organizer from ParticipantIdentity (1.10.0)** — `P3` — `missing`
- What WEB does: `lib/jmap/client.ts:1198`.
### Contacts
- [ ] **Contacts from the previous account land after a switch** — `P1` — `bug`
- What WEB does: epoch guard (`stores/contact-store.ts:597`). What RN does: `src/stores/contacts-store.ts:357`, `:373`; reset (`:873`) keeps contactsPromise.
- [ ] **Contacts with a calendar, scheduling or free/busy link don't save** — `P2` — `bug`
- What WEB does: maps them to `calendars`/`schedulingAddresses` (`lib/jmap/contact-wire.ts:63-87`).
- What RN does: writes `calendarUri`, `schedulingUri`, `freeBusyUri` as card properties (`src/lib/contact-form.ts:412-414`, `src/api/contacts.ts:97`); Stalwart refuses the card.
- [ ] **vCard import sends fields Stalwart rejects** — `P2` — `bug`
- What WEB does: `lib/jmap/contact-wire.ts`. What RN does: flat ADR and `source` (`src/lib/vcard.ts:652-672`, `:989-990`).
- [ ] **Company directory in Contacts and the composer (#1253)** — `P2` — `missing`
- What WEB does: `components/contacts/directory-browser.tsx:61`, `stores/contact-store.ts:924`. What RN does: `src/screens/ContactsScreen.tsx`, `src/stores/contacts-store.ts:728`; listPrincipals exists (`src/api/files.ts:593`).
- [ ] **Deleting an address book that still holds contacts fails** — `P3` — `bug`
- What WEB does: `onDestroyRemoveContents` (`lib/jmap/client.ts:6937`). What RN does: `src/api/contacts.ts:404`.
- [ ] **vCard 4.0 export (#1407)** — `P3` — `bug`
- What RN does: dashes in dates, MOBILE instead of CELL (`src/lib/vcard.ts:21`, `:24`, `:1264`).
- [ ] **Delete contact categories; address book sharing; list filters; deep links new/edit** — `P3` — `missing`/`partial`
- What WEB does: `components/settings/address-book-management-settings.tsx:84`, `:435`, `components/contacts/contact-list.tsx:30`, `lib/deep-links.ts:394`. What RN does: `src/components/contacts/ContactsSidebarDrawer.tsx:144`, `src/components/settings/ContactsSettings.tsx:33`, `src/screens/ContactsScreen.tsx:163`, `src/navigation/linking.ts:155`.
### Files
- [ ] **Files trash** — `P2` — `missing`
- What WEB does: `lib/files-trash.ts:20`, `components/files/files-app.tsx:419`.
- What RN does: deletes for good (`src/api/files.ts:316`, `src/screens/FilesScreen.tsx:673`) and lists the webmail's `/.Trash` as an ordinary folder.
- [ ] **Save an attachment to Files (incl. shared and group mail)** — `P2` — `missing`
- What WEB does: `components/email/save-to-files-dialog.tsx:43`, `lib/jmap/client.ts:9981`. What RN does: `src/components/email/AttachmentChips.tsx:183`; `src/api/files.ts:411` has createFileNodeFromBlob.
- [ ] **Copy folders, numbered new folder names, next/previous in preview, starred and recent, deep links** — `P3` — `partial`/`missing`
- What WEB does: `lib/jmap/client.ts:10155`, `stores/file-store.ts:640`, `lib/deep-links.ts:444`. What RN does: `src/api/files.ts:511-519`, `:328-345`, `src/components/files/FilePreviewModal.tsx`, `src/navigation/linking.ts:158`, `:307`.
- [ ] **Sharing fails on Stalwart before 0.16.6** — `P3` — `bug`
- What WEB does: LegacyFileNodeRights (`lib/jmap/client.ts:66-70`). What RN does: `src/components/files/ShareSheet.tsx:33-41`.
### Filters and vacation
- [ ] **Filter scripts saved by webmail 1.13+ open as raw Sieve; reset wipes them (#1152)** — `P1` — `bug`
- What WEB does: accepts metadata v1 and v2 (`lib/sieve/parser.ts:855`); writes v2 for rule periods, vacation forwarding or audience (`lib/jmap/sieve-types.ts:146`).
- What RN does: `if (metadata.version !== 1) return OPAQUE` (`src/lib/sieve/parser.ts:804`); "Reset to visual builder" (`src/components/settings/FilterSettings.tsx:290-318`) wipes the rules and forwarding.
- Fix hint: accept v2 and carry activeFrom/activeUntil, vacationForward, vacationAudience through parse and save.
- [ ] **Webmail "is the address" / "has the domain" rules are broadened on a phone save** — `P1` — `bug`
- What WEB does: `address :is` / `address :domain :is` (`lib/sieve/generator.ts:106`, `lib/sieve/parser.ts:485`).
- What RN does: no such comparators (`src/lib/sieve/types.ts:27`); the generator's default branch writes `header :contains` (`src/lib/sieve/generator.ts:130-131`).
- [ ] **"All messages" condition (#1154)** — `P2` — `missing`
- What WEB does: `lib/sieve/generator.ts:61`. What RN does: throws on save (`src/lib/sieve/generator.ts:113`), so every save fails once a web rule uses it.
- [ ] **"Stop processing" after Delete silently or Reject (#1153)** — `P2` — `bug`
- What WEB does: `lib/sieve/generator.ts:487`. What RN does: skips `stop;` (`src/lib/sieve/generator.ts:396`).
- [ ] **Rule names with double spaces get a duplicate (#1153)** — `P2` — `bug`
- What WEB does: collapses whitespace (`lib/sieve/generator.ts:455`, `lib/sieve/parser.ts:904`). What RN does: `src/lib/sieve/parser.ts:841`, `src/lib/sieve/generator.ts:39`.
- [ ] **Out of Office forwarding, reply audience and rule periods (#1152)** — `P2` — `missing`
- What WEB does: `components/settings/vacation-settings.tsx:135`, `components/filters/filter-rule-modal.tsx:188`, `:299`, `lib/sieve/period.ts`. What RN does: `src/components/settings/VacationSettings.tsx:209`, `src/lib/sieve/types.ts:66-108`.
- [ ] **Create rules from a message** — `P2` — `missing`
- What WEB does: `components/email/rules-menu.tsx`, `lib/filters/quick-rules.ts:187`. What RN does: `src/components/email/ActionSheet.tsx`, `src/components/filters/FilterRuleModal.tsx`.
- [ ] **Forward limit (#1155, #1208); vacation length warning; run a rule on a folder (#1166); Sieve editor loses typed text (#1162); size value raw into Sieve; undo after deleting tags/rules/trusted senders** — `P3` — `missing`/`bug`
- What WEB does: `lib/filters/forward-limit.ts:16`, `components/settings/vacation-settings.tsx:253`, `lib/filters/quick-rule-flow.ts:221`, `lib/sieve/generator.ts:63`. What RN does: `src/components/filters/FilterRuleModal.tsx:63`, `:413`, `src/components/settings/VacationSettings.tsx:106`, `src/components/filters/SieveEditorSheet.tsx:35-41`, `src/lib/sieve/generator.ts:69-74`, `src/components/settings/KeywordSettings.tsx:50-56`.
### Push, settings and core
- [ ] **Push is renewed for the active account only; settings show only that account (#1054)** — `P2` — `bug`
- What WEB does: `components/push-notification-prompt.tsx:173`, `:219`, `components/settings/multi-account-push.tsx:33`.
- What RN does: resyncPushNotifications works on the active client (`App.tsx:671`, `:723`; `src/lib/push-notifications.ts:1592-1602`), so other accounts' subscriptions expire.
- [ ] **Push with two logins that share a JMAP account id** — `P2` — `bug`
- What WEB does: 07e6ecce1.
- What RN does: the iOS extension's credentials are keyed by JMAP id (`src/lib/ios-push-shared.ts:54`); a tap opens the first match (`src/lib/push-notifications.ts:1873`); the background task checks every login with that id (`src/lib/push-background-task.ts:201`).
- [ ] **Writes other than sends are replayed after a dropped connection** — `P2` — `partial`
- What WEB does: retries read-only batches only (`lib/jmap/client.ts:245`).
- What RN does: blocks the retry only for EmailSubmission/set, Email/import, Blob/upload (`src/api/jmap-client.ts:102-109`, `:853-857`).
- [ ] **Recover deleted items (#1319)** — `P3` — `missing`
- What WEB does: `x:ArchivedItem/query` (`lib/recovery/archived-items.ts:139`, `lib/jmap/client.ts:12119`, `components/settings/recover-deleted-settings.tsx`).
- What RN does: nothing (`src/screens/SettingsScreen.tsx:112`). The day limit and hiding need the policy.
- [ ] **Delegates** — `P3` — `missing`
- What WEB does: Mailbox/AddressBook/Calendar shareWith in one batch (`lib/delegation/delegation.ts:96`).
- What RN does: no mailbox sharing (`src/components/settings/FolderSettings.tsx`).
- [ ] **Shared accounts treated as capable when their capabilities say otherwise (#1239)** — `P3` — `bug`
- What WEB does: `lib/jmap/client.ts:6431`. What RN does: `src/api/contacts.ts:76`, `src/api/files.ts:98`, `src/api/sieve.ts:43`, `src/api/vacation.ts:77`.
- [ ] **Storage quota ignores softLimit (#1242)** — `P3` — `bug`
- What WEB does: `lib/quota.ts:32`. What RN does: `src/api/quota.ts:50`.
- [ ] **Push extras: Inbox-only switch (#983), one ring per message (#1141), generic notification when the lookup fails (#1041), re-check on return (#1148)** — `P3` — `missing`/`partial`
- What RN does: `src/lib/notification-mutes.ts`, `src/lib/push-background-task.ts:468-488`, `:518`, `src/components/settings/NotificationSettings.tsx:115-134`.
- [ ] **Settings extras: language search, date locale override, font size everywhere, nav-bar icon style, sidebar apps never rendered, screenshot protection, Flat fields theme** — `P3` — `missing`/`partial` (old 08/09 items)
- What RN does: `src/components/settings/LanguageSettings.tsx:87`, `src/theme/dynamic.ts:48`, `src/lib/sidebar-apps.ts:10`, `src/theme/builtin-themes.ts:18`.
- [ ] **Search snippets, size filter, mail folder sharing, global search (1.10.0)** — `P3` — `missing`
## Old open items (docs/parity 01-09, audit-2026-09)
Of 42 open boxes and "Missing vs webmail" items, 8 are done (84fef142 calendar UI strings, f62849d1 iOS push, 75299244 Tabler icons, 26a34d59 autocomplete groups, 5802cae contact deep link, status bar theming, birthday time zone, folder colours 548b647), 9 are no longer relevant (identity and template sync, settings sync, S/MIME is a plugin, #379 migration, stability banner, admin relay list, Android badge is iOS-only by design), and 25 are still open; those are folded into the rows above.
## Needs a decision
- Organisation policy on the phone: the app needs the webmail it was paired with and a webmail route that accepts a JMAP bearer token. Until then locked settings, session limits, external recipient confirm and label, the org signature and disclaimer, template locks and recovery limits can't be enforced on the phone.
- Webmail hand-off: replacing the password in the redirect with a one-time code needs the webmail side first.