# 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/](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) - [x] **`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). - [x] **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. - [x] **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](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](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](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](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](docs/parity/04-composer-send.md) | 51 | 46 | 5 | 5 | 14 | 32 | | 05 | Calendar and tasks | [docs/parity/05-calendar.md](docs/parity/05-calendar.md) | 50 | 43 | 7 | 5 | 20 | 25 | | 06 | Contacts and address books | [docs/parity/06-contacts.md](docs/parity/06-contacts.md) | 50 | 46 | 4 | 1 | 18 | 31 | | 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 | | 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 | 52 | 60 | 10 | 40 | 62 | | | **Total** | | **527** | **432** | **95** | **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 - [x] No draft autosave, no save-on-close, and the OS back gesture bypasses the discard guard — closing a compose loses everything. → [04](docs/parity/04-composer-send.md) *(fixed in 5b72c4d)* - [x] Cannot open or edit an existing draft (Drafts folder opens the read-only viewer). → [04](docs/parity/04-composer-send.md) *(fixed in 5b72c4d)* - [x] Message is filed into **Sent before `EmailSubmission/set`**; a failed send leaves a never-sent copy in Sent and a retry duplicates it. → [04](docs/parity/04-composer-send.md) *(fixed in ccbe67c)* - [x] `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](docs/parity/03-email-viewer.md), [04](docs/parity/04-composer-send.md) *(fixed in 7f956d6)* - [x] 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](docs/parity/03-email-viewer.md) *(fixed in 7f956d6)* - [x] **Forward drops every attachment.** → [03](docs/parity/03-email-viewer.md), [04](docs/parity/04-composer-send.md) *(fixed in 7f956d6)* - [x] 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](docs/parity/07-filters-vacation-files.md) *(fixed in 1c6aa68)* - [x] 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](docs/parity/07-filters-vacation-files.md) *(fixed in 1c6aa68)* - [x] `FilterRuleModal` is always mounted and never re-seeds: editing rule B shows and **saves rule A's fields under B's id**. → [07](docs/parity/07-filters-vacation-files.md) *(fixed in 1c6aa68)* - [x] Calendar recurrence scope dialog is a stub: "This event" / "This and following" act as "All events" — deleting one occurrence **destroys the series**. → [05](docs/parity/05-calendar.md) *(fixed in 161baf1)* - [x] Editing an expanded occurrence rewrites the master's `start` to the occurrence date (series shifts, earlier occurrences vanish). → [05](docs/parity/05-calendar.md) *(fixed in 161baf1)* - [x] All-day events grow by one day on every create-via-toggle and every re-save. → [05](docs/parity/05-calendar.md) *(fixed in 58a6f45)* - [x] Events are created without `timeZone` (floating time). → [05](docs/parity/05-calendar.md) *(fixed in 58a6f45)* - [x] Invitations are never sent: participants use the retired `sendTo`, no organizer participant, no `organizerCalendarAddress` (#500, #731). → [05](docs/parity/05-calendar.md) *(fixed in 5dc4070)* - [x] 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](docs/parity/06-contacts.md) *(fixed in e650cb2)* ### Core flow blocked / wrong content shown - [x] TOTP / 2FA password login is impossible (402 is shown as a generic failure). → [01](docs/parity/01-auth-accounts.md) *(fixed in b52768e)* - [x] Webmail password handoff breaks for TOTP-protected accounts (cross-repo: webmail should hand off `flow=oauth` after a TOTP-upgraded login). → [01](docs/parity/01-auth-accounts.md) *(fixed in 0d94d76)* - [x] A failed "add account" resets the singleton client before connecting, killing the live session until relaunch. → [01](docs/parity/01-auth-accounts.md) *(fixed in b52768e)* - [x] 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](docs/parity/01-auth-accounts.md) *(fixed in 032732c)* - [x] Thread view: only the newest message of a thread is reachable; older messages cannot be opened unless threading is disabled. → [03](docs/parity/03-email-viewer.md) *(fixed in d2ed27f)* - [x] 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](docs/parity/02-mail-list-folders.md), [03](docs/parity/03-email-viewer.md) *(fixed in c8be383)* - [x] HTML-only messages are rendered — and quoted in replies — as raw HTML source (**native #46**). `