Files
native/PARITY_CHECKLIST.md
T

19 KiB
Raw Blame History

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
    P1 data loss, wrong send/delete, security, crash, or a core flow that is blocked
    P2 notable feature missing or visibly wrong
    P3 polish, nice-to-have, minor divergence
    Category Meaning
    missing webmail has it, native does not
    partial native has a reduced version
    bug native has it, but it is wrong
    bugfix-parity a bug the webmail fixed (changelog / issue number cited) that is still live in native
    rn-only-bug a bug that only exists in native (found while reading)
  • WEB paths are relative to the webmail repo, RN paths to this repo. Issue numbers #NNN refer 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 test is red on main — P2 — rn-only-bug (fixed in b84d4d8)
    • src/stores/__tests__/auth-store.test.ts fails to load with SyntaxError: Unexpected token 'typeof'. The chain auth-store → email-store → outbox-store → network-store imports @react-native-community/netinfo, which src/test-setup.ts never mocks (it mocks react-native, expo-secure-store, expo-web-browser, AsyncStorage only). Broken since 5d47f47 (2026-05-30) when the outbox import was added; email-store.test.ts only passes because it mocks ../outbox-store itself.
    • Fix hint: add vi.mock('@react-native-community/netinfo', () => ({ default: { addEventListener: vi.fn(() => () => {}), fetch: vi.fn(async () => ({ isConnected: true, isInternetReachable: true })) } })) to src/test-setup.ts. Expected afterwards: 31/31 suites (432+ tests).
  • CI runs neither tsc nor vitest — P2 — missing (fixed in fb4796e)
    • .github/workflows/ only has release-android.yml and release-ios.yml. Add a ci.yml running npm ci && npm run typecheck && npm test on push/PR.
  • npx tsc --noEmit is 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 in 9ae91a6) 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: 380 of 415 items are done, 35 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 38 8 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
10 Webmail 1.10.0 to 1.18.0 and Unreleased (all areas) 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 (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).

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 in ccbe67c)
  • In-Reply-To / References are written with the JMAP email id instead of the RFC Message-ID (messageId/references are never even fetched) — every reply breaks threading in recipients' clients. → 03, 04 (fixed in 7f956d6)
  • Reply addressing: Reply-To ignored, the user's own address kept on reply-all, wrong recipients on self-sent replies (#703, 1.9.0). → 03 (fixed in 7f956d6)
  • Forward drops every attachment. → 03, 04 (fixed in 7f956d6)
  • Sieve: RN generator has no attachment field and emits header :contains "undefined" "" for webmail-authored attachment rules — any save on the phone corrupts the shared server script. → 07 (fixed in 1c6aa68)
  • 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 in 1c6aa68)
  • FilterRuleModal is always mounted and never re-seeds: editing rule B shows and saves rule A's fields under B's id. → 07 (fixed in 1c6aa68)
  • 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 start to the occurrence date (series shifts, earlier occurrences vanish). → 05 (fixed in 161baf1)
  • 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 in 58a6f45)
  • Invitations are never sent: participants use the retired sendTo, no organizer participant, no organizerCalendarAddress (#500, #731). → 05 (fixed in 5dc4070)
  • 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=oauth after a TOTP-upgraded login). → 01 (fixed in 0d94d76)
  • 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.capabilities but Stalwart advertises urn:stalwart:jmap only in accountCapabilities (verified on 0.16.19) — native #47. → 01 (fixed in 032732c)
  • 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 in 06742ef)

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/*.ts functions index methodResponses[0][1] blindly and never check error / notUpdated / notDestroyed — a rejected mutation is reported as success and the outbox drops it. → 09 (fixed in ccbe67c)
  • Password change leaves the stored credential stale; the next launch evicts the account. → 09 (fixed in 032732c)
  • No AppState handling: SSE is never paused in background nor recovered on foreground, and react-native-sse never reconnects after a status-0 network error — push is silently dead after the first blip. → 09 (fixed in edc26ce)
  • SSE Authorization header is captured once; after an OAuth refresh every reconnect sends the stale token (401 → re-poll every 5 s forever). → 09 (fixed in edc26ce)
  • Push effect is keyed on the singleton client, so the SSE stream stays bound to the previous account after switchAccount. → 09 (fixed in edc26ce)
  • Webmail password handoff sends the clear-text password in a custom-scheme redirect fragment that any app can register; OAuth state uses Math.random; server_url/token_endpoint in the callback are trusted as-is. → 09, 01 (fixed in 2c0dbd1)

Repo health

  • npm test is red on main (see Baseline health above). (fixed in b84d4d8)

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 done on Android: Settings → Contacts / Calendar → "Sync to this device" syncs both ways with the device's Contacts and Calendar apps (design); iOS has no API for it
#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.

  1. Email headers are never fetched. EMAIL_FULL_PROPERTIES lacks messageId, inReplyTo, references, headers, replyTo usage. 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)
  2. No thread model. The list collapses threads but the viewer is a single-message pager over the folder page. (02, 03)
  3. 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)
  4. 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)
  5. Webmail bugfixes that are one-line ports: $junk/$notjunk flip on spam (#850), search defaults to all folders (#788), folder substring dedup (#771), permanent-delete confirmation, $pinned vs $important, FileNode modified vs updated (#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-set pinning (#760), request timeouts (#702), relative session URL resolution.
  6. 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)
  7. Deep links / intents. bulwarkmobile:// is registered but nothing handles it; no mailto: or share-to-app intent filters. (04, 08)
  8. Push. Subscribes to Email+Mailbox instead of EmailDelivery, no emailPush spam filter, ignores relay-forwarded ids and guesses "newest unread", never matches the payload to an account, no device list/revoke, Android/FCM only. (08)
  9. JMAP client robustness. src/api/jmap-client.ts is a thin transport with none of the layers webmail added since 1.4.5: request deadlines, maxConcurrentRequests back-off, transient retry, rate-limit pause, maxObjectsInSet / maxSizeUpload chunking, keep-alive monitoring, first-touch serialisation, and — most importantly — method-error checking. A shared requireMethodResult / assertSetResult helper used by every src/api/*.ts function fixes a whole class of silent failures. (09)
  10. Live sync lifecycle. No AppState or 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

  1. Repo health: fix test-setup.ts, add CI. Half a day.
  2. 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).
  3. 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.
  4. 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).
  5. 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.