Tick the parity items the fix pass finished (server-side recurrence, filters and vacation for shared accounts, in-app file preview, the trusted-senders toggle, UnifiedPush, sender taps in the viewer), and note on the already ticked ones what this pass changed, such as the tag view and search reaching shared accounts or contact photos as avatars. Items that only moved forward say what is left. Fix hints and "verified" lines that named removed functions (`setEmailKeywords`, `getEmailDetail`, `fetchEmailsChunked`, `buildRecurrenceOverridePatch`) or the old whole-map keyword writes now point at what replaced them. PARITY_CHECKLIST.md counts done and open items per area: 379 of 415 are done, 36 open.
19 KiB
Webmail parity checklist
Audit of Bulwark Mobile (bulwarkmail/native, v0.1.58 @ 82b97fa) against
Bulwark Webmail (bulwarkmail/webmail, v1.9.2 @ 39dcc6b2), taken on
2026-08-29. The goal is feature and bugfix parity: every user-facing webmail
behaviour, and every webmail changelog fix since the native app was started
(1.4.5 → 1.9.2), was checked against the native code.
The audit was done by reading code on both sides, not by running the app. Every
finding cites file:line on both sides so it can be re-verified quickly; a
sample of the 32 P1 findings was additionally spot-checked by hand.
How to use this document
-
The detailed findings live in docs/parity/, one file per area. Tick the
- [ ]boxes there as you fix things; this file is the index. -
Each finding has a priority and a category:
Priority Meaning P1data loss, wrong send/delete, security, crash, or a core flow that is blocked P2notable feature missing or visibly wrong P3polish, nice-to-have, minor divergence Category Meaning missingwebmail has it, native does not partialnative has a reduced version bugnative has it, but it is wrong bugfix-paritya bug the webmail fixed (changelog / issue number cited) that is still live in native rn-only-buga bug that only exists in native (found while reading) -
WEBpaths are relative to the webmail repo,RNpaths to this repo. Issue numbers#NNNrefer to the webmail tracker unless prefixed "native #NN". -
Each area file ends with Verified at parity (do not redo) and N/A on mobile (deliberately out of scope) lists.
-
A few findings appear in two areas because they sit on a boundary (e.g. "forward drops attachments" is in both viewer and composer). Fix once, tick both.
Baseline health of this repo (2026-08-29)
npm testis red onmain—P2—rn-only-bug(fixed inb84d4d8)src/stores/__tests__/auth-store.test.tsfails to load withSyntaxError: Unexpected token 'typeof'. The chainauth-store → email-store → outbox-store → network-storeimports@react-native-community/netinfo, whichsrc/test-setup.tsnever mocks (it mocksreact-native,expo-secure-store,expo-web-browser, AsyncStorage only). Broken since5d47f47(2026-05-30) when the outbox import was added;email-store.test.tsonly passes because it mocks../outbox-storeitself.- Fix hint: add
vi.mock('@react-native-community/netinfo', () => ({ default: { addEventListener: vi.fn(() => () => {}), fetch: vi.fn(async () => ({ isConnected: true, isInternetReachable: true })) } }))tosrc/test-setup.ts. Expected afterwards: 31/31 suites (432+ tests).
- CI runs neither
tscnorvitest—P2—missing(fixed infb4796e).github/workflows/only hasrelease-android.ymlandrelease-ios.yml. Add aci.ymlrunningnpm ci && npm run typecheck && npm teston push/PR.
npx tsc --noEmitis clean.- Uncommitted work in the tree at audit time: ascending/descending mail sort
(native #5) in
src/stores/email-store.ts,src/stores/settings-store.ts,src/screens/EmailListScreen.tsx,src/stores/__tests__/email-store.test.ts. Findings that touch list ordering assume that lands. - README is stale —
P3— it says "Filters & rules, S/MIME, plugins, (fixed in9ae91a6) themes, file storage – UI stubs only". Filters, Files and file sharing are real (see areas 07); S/MIME, plugins, themes and sidebar apps are still stubs (area 08).
Status after the 2026-08-29 fix pass
Worked down the same day, area by area, by one orchestrating session plus one
agent per area, each change committed on its own (type: short description).
Per-area counts of ticked items are in the area files; every tick carries the
commit hash. The remaining - [ ] items are either deferred with a note
("deferred: …") or need work outside this repo (webmail hand-off flow=oauth
for TOTP accounts, relay APNs/UnifiedPush transports, a server-side settings
store for native #1).
Status after the 2026-09-24 fix pass
The native audit of 2026-09-22 (docs/audit-2026-09.md) spot-checked this list and found 14 ticks that were broken or overstated and 11 open items that were already done. The 2026-09-24 fix pass fixed 13 of the 14 (their ticks now also name the fixing commit; the date-locale item is open again), dropped three obsolete items and ticked what it completed: 379 of 415 items are done, 36 open. What still needs a device check or a decision is in the audit's "Fix pass, 2026-09-24" section.
Areas
| # | Area | File | Items | Done | Open | P1 | P2 | P3 |
|---|---|---|---|---|---|---|---|---|
| 01 | Authentication, login, session, multi-account | docs/parity/01-auth-accounts.md | 30 | 29 | 1 | 4 | 8 | 18 |
| 02 | Mail list, folders, unified views, search, tags | docs/parity/02-mail-list-folders.md | 54 | 50 | 4 | 1 | 20 | 33 |
| 03 | Email viewer, thread view, rendering, attachments | docs/parity/03-email-viewer.md | 46 | 45 | 1 | 6 | 13 | 26 |
| 04 | Composer, drafts, sending, identities, templates, scheduled send | docs/parity/04-composer-send.md | 51 | 46 | 5 | 5 | 14 | 32 |
| 05 | Calendar and tasks | docs/parity/05-calendar.md | 50 | 43 | 7 | 5 | 20 | 25 |
| 06 | Contacts and address books | docs/parity/06-contacts.md | 50 | 46 | 4 | 1 | 18 | 31 |
| 07 | Filters (Sieve), vacation responder, Files | 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 | 46 | 37 | 9 | 0 | 14 | 32 |
| 09 | JMAP client core, live sync, offline, security, S/MIME | docs/parity/09-jmap-core-sync-security.md | 56 | 54 | 2 | 7 | 23 | 26 |
| Total | 415 | 379 | 36 | 32 | 138 | 244 |
Counts are of the - [ ] and - [x] items per file as of 2026-09-24. Done and
Open split them by tick; the P columns count the priority tags on those items
(one item carries none).
All P1 findings (fix these first)
Data loss / wrong send
- No draft autosave, no save-on-close, and the OS back gesture bypasses the discard guard — closing a compose loses everything. → 04 (fixed in
5b72c4d) - Cannot open or edit an existing draft (Drafts folder opens the read-only viewer). → 04 (fixed in
5b72c4d) - Message is filed into Sent before
EmailSubmission/set; a failed send leaves a never-sent copy in Sent and a retry duplicates it. → 04 (fixed inccbe67c) In-Reply-To/Referencesare written with the JMAP email id instead of the RFC Message-ID (messageId/referencesare never even fetched) — every reply breaks threading in recipients' clients. → 03, 04 (fixed in7f956d6)- Reply addressing:
Reply-Toignored, the user's own address kept on reply-all, wrong recipients on self-sent replies (#703, 1.9.0). → 03 (fixed in7f956d6) - Forward drops every attachment. → 03, 04 (fixed in
7f956d6) - Sieve: RN generator has no
attachmentfield and emitsheader :contains "undefined" ""for webmail-authored attachment rules — any save on the phone corrupts the shared server script. → 07 (fixed in1c6aa68) - Sieve: multi-value conditions (
string[], webmail 1.7.3) make the whole script "opaque" on RN; "Reset to visual builder" then wipes all rules. → 07 (fixed in1c6aa68) FilterRuleModalis always mounted and never re-seeds: editing rule B shows and saves rule A's fields under B's id. → 07 (fixed in1c6aa68)- Calendar recurrence scope dialog is a stub: "This event" / "This and following" act as "All events" — deleting one occurrence destroys the series. → 05 (fixed in
161baf1) - Editing an expanded occurrence rewrites the master's
startto the occurrence date (series shifts, earlier occurrences vanish). → 05 (fixed in161baf1) - All-day events grow by one day on every create-via-toggle and every re-save. → 05 (fixed in
58a6f45) - Events are created without
timeZone(floating time). → 05 (fixed in58a6f45) - Invitations are never sent: participants use the retired
sendTo, no organizer participant, noorganizerCalendarAddress(#500, #731). → 05 (fixed in5dc4070) - vCard import writes dates as raw strings instead of RFC 9553 PartialDate (#224) — Stalwart rejects the whole card, so any contact with a birthday is lost on import. → 06 (fixed in
e650cb2)
Core flow blocked / wrong content shown
- TOTP / 2FA password login is impossible (402 is shown as a generic failure). → 01 (fixed in
b52768e) - Webmail password handoff breaks for TOTP-protected accounts (cross-repo: webmail should hand off
flow=oauthafter a TOTP-upgraded login). → 01 (fixed in0d94d76) - A failed "add account" resets the singleton client before connecting, killing the live session until relaunch. → 01 (fixed in
b52768e) - Account Security screen is dead against every Stalwart server: it checks
session.capabilitiesbut Stalwart advertisesurn:stalwart:jmaponly inaccountCapabilities(verified on 0.16.19) — native #47. → 01 (fixed in032732c) - Thread view: only the newest message of a thread is reachable; older messages cannot be opened unless threading is disabled. → 03 (fixed in
d2ed27f) - Pager shows another message's body (or nothing) when the opened id is not in the active folder page — unified inbox, group inboxes, contact activity. → 02, 03 (fixed in
c8be383) - HTML-only messages are rendered — and quoted in replies — as raw HTML source (native #46).
<style>stripping and an old dark-mode CSS revision are the likely cause of native #49. → 03 (fixed in06742ef)
JMAP client, live sync, security
- No request deadline on any fetch — a stalled socket (iOS background suspension, dead pooled connection) hangs send/save forever (#702). → 09 (fixed in
0b1c240) - Most
src/api/*.tsfunctions indexmethodResponses[0][1]blindly and never checkerror/notUpdated/notDestroyed— a rejected mutation is reported as success and the outbox drops it. → 09 (fixed inccbe67c) - Password change leaves the stored credential stale; the next launch evicts the account. → 09 (fixed in
032732c) - No
AppStatehandling: SSE is never paused in background nor recovered on foreground, andreact-native-ssenever reconnects after a status-0 network error — push is silently dead after the first blip. → 09 (fixed inedc26ce) - SSE
Authorizationheader is captured once; after an OAuth refresh every reconnect sends the stale token (401 → re-poll every 5 s forever). → 09 (fixed inedc26ce) - Push effect is keyed on the singleton client, so the SSE stream stays bound to the previous account after
switchAccount. → 09 (fixed inedc26ce) - Webmail password handoff sends the clear-text password in a custom-scheme redirect fragment that any app can register; OAuth
stateusesMath.random;server_url/token_endpointin the callback are trusted as-is. → 09, 01 (fixed in2c0dbd1)
Repo health
npm testis red onmain(see Baseline health above). (fixed inb84d4d8)
Native issues mapped to findings
| Native issue | Where it is explained |
|---|---|
| #49 Unreadable text in dark-mode emails | 03 <style> stripping + stale dark-mode CSS |
| #48 / #44 UnifiedPush / non-FCM push | 08 push section |
| #47 Account security not accessible | 01 capability map bug (P1) |
| #46 email preview shows css | 03 HTML-only body detection (P1) |
| #45 error when re-registering to relay | 08 push section |
| #34 Native contacts/calendar sync adapters | feature request, out of parity scope; noted in 06 |
| #5 Sort ascending/descending | in progress in the working tree; ordering presets in 02 |
| #3 TLS client auth | Android-only today; gaps in 01 and 09 |
| #1 Shared settings between webmail and native | 08 — webmail sync is server-side/cookie-bound; a JMAP-blob design is sketched there |
Cross-cutting themes
These come up in nearly every area; fixing them centrally pays off more than per-screen patches.
- Email headers are never fetched.
EMAIL_FULL_PROPERTIESlacksmessageId,inReplyTo,references,headers,replyTousage. This single gap causes the broken threading headers, missing Reply-To handling, no SPF/DKIM/DMARC chips, no List-Unsubscribe, no read receipts, no details panel. (03, 04) - No thread model. The list collapses threads but the viewer is a single-message pager over the folder page. (02, 03)
- Hard-coded English. RN has an i18n layer and 15 vendored locales, but
84 of 94 screen/component files never call
t(); calendar, files, vacation, scheduled, login and every settings pane except Language and Filters are English-only. Catalogs are ~6 weeks behind webmail (640 missing keys), and there is no interpolation, plurals or RTL. (all areas, 08) - Single-account assumptions. Contacts, calendars (subscriptions), filters, vacation and Files are primary-account only; shared/group accounts work for mail folders but not elsewhere. (01, 05, 06, 07)
- Webmail bugfixes that are one-line ports:
$junk/$notjunkflip on spam (#850), search defaults to all folders (#788), folder substring dedup (#771), permanent-delete confirmation,$pinnedvs$important, FileNodemodifiedvsupdated(#700), percent-decoding WebDAV names (#869), RSCALE/SKIP=OMIT removal (#805), first-touch gate for lazy default calendar/address book creation (#907),supported-calendar-component-setpinning (#760), request timeouts (#702), relative session URL resolution. - Settings that do nothing. 16–17 persisted keys are never read outside the settings UI (layout, sound, calendar notifications, sidebar apps, S/MIME, plugins, theme id, files thumbnails, …). Either wire them or remove the toggles. (08, table in that file)
- Deep links / intents.
bulwarkmobile://is registered but nothing handles it; nomailto:or share-to-app intent filters. (04, 08) - Push. Subscribes to
Email+Mailboxinstead ofEmailDelivery, noemailPushspam filter, ignores relay-forwarded ids and guesses "newest unread", never matches the payload to an account, no device list/revoke, Android/FCM only. (08) - JMAP client robustness.
src/api/jmap-client.tsis a thin transport with none of the layers webmail added since 1.4.5: request deadlines,maxConcurrentRequestsback-off, transient retry, rate-limit pause,maxObjectsInSet/maxSizeUploadchunking, keep-alive monitoring, first-touch serialisation, and — most importantly — method-error checking. A sharedrequireMethodResult/assertSetResulthelper used by everysrc/api/*.tsfunction fixes a whole class of silent failures. (09) - Live sync lifecycle. No
AppStateor connectivity hooks around the EventSource; reconnect, token refresh, account switch and client-cert users all leave the stream dead or stale. (09)
Suggested order of work
- Repo health: fix
test-setup.ts, add CI. Half a day. - The P1 list above, roughly in the order given (composer/send correctness, Sieve corruption, calendar write path, vCard dates, auth blockers, viewer, JMAP client + SSE lifecycle).
- Cross-cutting themes 1, 2 and 9 (fetch headers, thread view, method-error checking + request deadline) — they unlock a large slice of the P2 items in 03/04/09 and make the outbox trustworthy.
- P2 items per area, starting with 02 (spam keywords, search scope, unified
inbox actions/paging, tags in rows) and 08 (push correctness, i18n
catalogs +
t()sweep). - P3 items opportunistically while touching the same files.
Explicitly out of scope on mobile
Collected from the per-area N/A lists so nobody re-litigates them: keyboard shortcuts, drag & drop, three-pane / Pro split-screen shell, browser back/forward and permalinks in the address bar, PWA install / service worker, print, guided tour, CSP nonces / server-side SSRF guards, admin dashboard and setup wizard, plugin sandbox and marketplace (a minimal "sidebar apps in a WebView" subset is described in 08), impersonation, TOTP re-auth dialog for cookie sessions, server-side telemetry, OpenGraph cards. Where a webmail feature is server-mediated (settings sync, iCal fetch proxy, TOTP token exchange, push relay list) the area file says what the native equivalent would be.
Keeping this current
Re-run the audit after each webmail release: read the new changelog "Fixes"
bullets, decide RN affected / not affected / N/A, and append to the matching
area file. When a finding is fixed, tick it and add the native commit hash in
the same line so the next audit can skip it.