Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3f0f26f
docs: add the webmail 1.10–1.12 parity delta and the parity roadmap
wkennedy Oct 4, 2026
f66084f
fix: take DKIM and DMARC results only from the receiving server's own…
wkennedy Oct 4, 2026
80ef7ce
docs: add the calendar-invitation trust fix to the phase 1 plan
wkennedy Oct 4, 2026
5f9812a
fix: keep an escaped quote inside a recipient's display name
wkennedy Oct 4, 2026
600f355
fix: fail a send the server refused for every recipient, and never ca…
wkennedy Oct 4, 2026
e113118
fix: say which recipients the server refused, and point at Sent when …
wkennedy Oct 4, 2026
2996c02
fix: escape header names, sizes and rule names in the filter script
wkennedy Oct 4, 2026
f11bfbd
fix: keep a "*/" in a rule from ending the filter script's metadata c…
wkennedy Oct 4, 2026
f15be2c
fix: stop after a silent delete or reject, and read the webmail's all…
wkennedy Oct 4, 2026
fd2f129
feat: add the all-messages condition and address/domain matching to t…
wkennedy Oct 4, 2026
ef341b1
fix: send a mailto unsubscribe to the one listed address and show it …
wkennedy Oct 4, 2026
4d85f28
fix: stop reading a winmail.dat value list that makes no progress
wkennedy Oct 4, 2026
0c07c68
fix: judge an invitation's sender by the receiving server's own authe…
wkennedy Oct 4, 2026
c565dfe
fix: refuse to save a filter size that is not a whole number with an …
wkennedy Oct 4, 2026
31900f5
fix: accept only one plain address in a mailto unsubscribe link
wkennedy Oct 4, 2026
e1c8b0c
fix: keep the refused-recipients warning up longer and do not trust r…
wkennedy Oct 4, 2026
011eb76
test: pin what happens to the old draft after an unconfirmed or partl…
wkennedy Oct 4, 2026
071fe8b
docs: tick the phase 1 parity fixes with their commits
wkennedy Oct 4, 2026
a680476
docs: list what phase 1 left for later
wkennedy Oct 4, 2026
429d6f8
docs: plan phase 2 of the webmail parity work
wkennedy Oct 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 41 additions & 12 deletions PARITY_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,22 +85,46 @@ 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.

## Webmail 1.10.0 → 1.12.0+ delta, 2026-10-04

Webmail moved from 1.9.2 to 1.12.0 (2026-09-30) plus 47 unreleased commits up
to `a4e313f` (2026-10-02). Every user-facing changelog bullet and commit in that
range was checked against native `main` at `76180b3`, skipping what
[docs/audit-2026-09.md](docs/audit-2026-09.md) already lists. The 74 new items
sit in a "Webmail 1.10.0 → 1.12.0+ delta" section in each area file (3 P1,
24 P2, 47 P3); what was already at parity is one "1.10–1.12 delta" line at the
end of each area's Verified list. Phase 1 of the [roadmap](docs/superpowers/plans/2026-10-04-webmail-parity-roadmap.md) closed the three P1s and five security/send P2s below on branch `parity/phase-1-security-send`. The three P1s, and the P2s worth doing next:

- P1: forged `Authentication-Results` can fake a DMARC/DKIM pass (03); refused
recipients in `deliveryStatus` are never reported, so a send that reached no
one shows as sent (04); an escaped quote in a display name splits off an extra
recipient (04).
- P2 security: Sieve values unescaped (07), multi-address `mailto:` unsubscribe
(03), TNEF parser loop (03).
- P2 data: contacts with calendar/scheduling URIs and vCard imports rejected by
Stalwart, address book delete refused (06); cross-account move drops the date
(02); a draft follows an account switch into the wrong account (04); Sieve
`stop` missing after discard/reject and `field: 'all'` rules breaking every
native save (07).
- P2 reliability: push subscriptions lapse after Stalwart's 7-day expiry (08);
list actions fail silently (02); daily events stop at DST (05).

## 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 |
| | **Total** | | **415** | **380** | **35** | **32** | **138** | **244** |

Counts are of the `- [ ]` and `- [x]` items per file as of 2026-09-24. Done and
| 01 | Authentication, login, session, multi-account | [docs/parity/01-auth-accounts.md](docs/parity/01-auth-accounts.md) | 38 | 29 | 9 | 4 | 12 | 22 |
| 02 | Mail list, folders, unified views, search, tags | [docs/parity/02-mail-list-folders.md](docs/parity/02-mail-list-folders.md) | 76 | 50 | 26 | 1 | 24 | 51 |
| 03 | Email viewer, thread view, rendering, attachments | [docs/parity/03-email-viewer.md](docs/parity/03-email-viewer.md) | 52 | 48 | 4 | 7 | 17 | 27 |
| 04 | Composer, drafts, sending, identities, templates, scheduled send | [docs/parity/04-composer-send.md](docs/parity/04-composer-send.md) | 60 | 49 | 11 | 7 | 16 | 37 |
| 05 | Calendar and tasks | [docs/parity/05-calendar.md](docs/parity/05-calendar.md) | 61 | 43 | 18 | 5 | 23 | 33 |
| 06 | Contacts and address books | [docs/parity/06-contacts.md](docs/parity/06-contacts.md) | 53 | 46 | 7 | 1 | 21 | 31 |
| 07 | Filters (Sieve), vacation responder, Files | [docs/parity/07-filters-vacation-files.md](docs/parity/07-filters-vacation-files.md) | 38 | 32 | 6 | 3 | 11 | 24 |
| 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) | 54 | 38 | 16 | 0 | 15 | 39 |
| 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) | 57 | 54 | 3 | 7 | 23 | 27 |
| | **Total** | | **489** | **389** | **100** | **35** | **162** | **291** |

Counts are of the `- [ ]` and `- [x]` items per file as of 2026-10-04. Done and
Open split them by tick; the P columns count the priority tags on those items
(one item carries none).

Expand Down Expand Up @@ -141,6 +165,11 @@ Open split them by tick; the P columns count the priority tags on those items
- [x] Push effect is keyed on the singleton client, so the SSE stream stays bound to the previous account after `switchAccount`. → [09](docs/parity/09-jmap-core-sync-security.md) *(fixed in edc26ce)*
- [x] 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](docs/parity/09-jmap-core-sync-security.md), [01](docs/parity/01-auth-accounts.md) *(fixed in 2c0dbd1)*

### Webmail 1.10–1.12 delta (2026-10-04)
- [x] A forged lower `Authentication-Results` header can supply a DKIM/DMARC pass in the security badge. → [03](docs/parity/03-email-viewer.md) *(fixed in f66084f, 0c07c68)*
- [x] Recipients refused at RCPT TO (`deliveryStatus`, #1123) are never read back; a send that reached nobody shows as sent. → [04](docs/parity/04-composer-send.md) *(fixed in 600f355, e113118)*
- [x] `splitRecipients` ignores escaped quotes, so a crafted display name splits off an extra recipient. → [04](docs/parity/04-composer-send.md) *(fixed in 5f9812a)*

### Repo health
- [x] `npm test` is red on `main` (see Baseline health above). *(fixed in b84d4d8)*

Expand Down
41 changes: 41 additions & 0 deletions docs/parity/01-auth-accounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,46 @@ RN covers the happy paths (password login, webmail-mediated OAuth handoff, QR pa
- What RN does: `randomState()` uses `Math.random` (`src/lib/oauth.ts:41-46`) although a CSPRNG helper with `getRandomValues`/`randomUUID` fallbacks already exists in `src/lib/totp.ts:16-46`. The state is the only guard against a forged `bulwarkmobile://` redirect delivering foreign credentials.
- Fix hint: export `randomBytes` from `totp.ts` (or a shared `random.ts`) and use it here.

## Webmail 1.10.0 → 1.12.0+ delta (audited 2026-10-04)

Webmail changelog 1.10.0, 1.11.0-beta.1 – 1.11.2 and 1.12.0, plus the
unreleased commits up to `a4e313f` (2026-10-02), checked against native `main`
at `76180b3`. Items already listed in [../audit-2026-09.md](../audit-2026-09.md)
are not repeated. "Unverified" means read from the code but not confirmed on a
device.

- [ ] **TOTP accounts cannot change their password or turn TOTP off** — `P2` — `bugfix-parity` (1.11.0)
- What WEB does: sends the current TOTP code with password and TOTP changes (`stores/account-security-store.ts:263-266,583-603,676-690`).
- What RN does: `src/api/account-security.ts:346,387` send no code; the server refuses the change.
- Fix hint: prompt for the current code in `AccountSecuritySettings.tsx` when TOTP is on, and pass it through.

- [ ] **Sign-out leaves account data on the device** — `P2` — `bugfix-parity` (1.11.0, WEB `lib/sign-out-cleanup.ts`)
- What RN does: search history is never cleared (`src/stores/search-history-store.ts:41`); the offline body cache keeps full message bodies after sign-out or account removal (`offline-cache-store.ts:292` `clearAll` is never called from `auth-store.ts:575-760`); outbox keys of removed accounts stay.
- Fix hint: one per-account cleanup function called from logout and removeAccount.

- [ ] **Internationalized domains (IDN) fail at sign-in and in addresses (#1100)** — `P2` — `missing` (1.11.0)
- What WEB does: punycode handling in `lib/idn.ts`, used by `stores/auth-store.ts:29`.
- What RN does: no punycode handling; `isValidEmail` accepts ASCII only (`src/lib/recipients.ts:32`). Unverified whether Hermes' URL covers the host part.

- [ ] **Filters and the security page can show the previous account's data after a switch** — `P2` — `rn-only-bug` (edge case, partly unverified)
- What RN does: `switchAccount` (`src/stores/auth-store.ts:658-742`) does not reset the filter or vacation stores, and the load effects in `FilterSettings.tsx:160-163` and `AccountSecuritySettings.tsx:870-896` do not depend on the active account. Reachable when a notification tap or deep link switches account while the screen stays mounted; a save would then write the old account's rules into the new one.
- Fix hint: reset the stores in `switchAccount` and key the effects on the active account id.

- [ ] **No "Sign in with an access token"; session URLs on another origin are rewritten** — `P3` — `missing` (29c74c3)
- What WEB does: Bearer-token login for servers such as Fastmail, and keeps download/upload/eventSource URLs that sit on another HTTPS origin.
- What RN does: `connectWithToken` exists (`src/api/jmap-client.ts:299`) but has no UI; `src/api/jmap-client.ts:587-596` rewrites every session URL onto the server origin, which would break such servers.

- [ ] **A refused token exchange on a TOTP login gets a generic error** — `P3` — `partial` (post-1.12)
- What RN does: throws `TotpLoginError('token_exchange_failed')` (`src/lib/totp-login.ts:148`), but `src/lib/login-errors.ts:129` only maps `invalid`.
- Fix hint: add a message for `token_exchange_failed`.

- [ ] **Security page shows an empty name for non-admin users** — `P3` — `bugfix-parity` (1.11.0)
- What WEB does: falls back when `x:Account/get` (admin-only on Stalwart) is refused (`stores/account-security-store.ts:519-529`).
- What RN does: `AccountSecuritySettings.tsx:886-888` reads it from `x:Account/get` only; `fetchAccountDisplayName` (`src/api/account-security.ts:291`) exists but is not used here.

- [ ] **SSO sign-out does not end the identity provider's session (#905)** — `P3` — `missing` (1.11.0)
- What RN does: `end_session_endpoint` is never called (`src/lib/oauth-native.ts:212`), so the next sign-in in the in-app browser reuses the provider session.

## Verified at parity (brief list, so the fixer knows NOT to redo)
- QR login payloads: WEB emits `bulwarkmail://pair?server=<webmailBase>&code=…` (`account-security-settings.tsx:971`); RN parses it plus a `connect` variant and bare URLs (`src/lib/oauth.ts:135-162`) and redeems at `/api/auth/pair/redeem` (`:165-207`) into the same OAuth bundle the browser handoff yields.
- Webmail handoff (`mobile_redirect_uri`/`mobile_state`, fragment transport, state check, password/oauth flows) matches `login/page.tsx:115-130, 282, 638-650` and `auth/callback/page.tsx` mobile branch.
Expand All @@ -175,6 +215,7 @@ RN covers the happy paths (password login, webmail-mediated OAuth handoff, QR pa
- Server discovery by email domain (`/.well-known/jmap` probe on bare/`mail.`/`webmail.` hosts, 401 counts as a hit, known servers trusted) is RN's equivalent of WEB's admin server list / auto-pick-by-domain (#799); neither side does `_jmap._tcp` SRV.
- Login error → session-expired banner: RN's `ChooseStep` shows the store error ("Session expired") like WEB's `session_expired` banner.
- `AuthenticationError` on 401, `RateLimitError` on 429 with Retry-After parsing in `request()`.
- 1.10–1.12 delta: `prompt=select_account` on add account; permanent refresh failures not retried (#972); discovery base path (#971); session refetch after a redirect drops auth (#892); no `max_age=0` (#938); SSO discovery uses the selected server (#952); rate-limited token endpoint doesn't sign out; full sign-out removes an account whose restore failed; webmail "Link Mobile App" password bundles (`src/lib/oauth.ts:193-215,438-451`).

## N/A on mobile
- "Remember me" (RN always stores credentials in the device keychain), `rememberMeEnabled`/`SESSION_SECRET` cookie logic, per-slot cookie handling (`cookieSlot`, `oauth_cookie_slot`), orphan-cookie adoption, `serverIdentifiers`/`classifySessionMatch` slot→token desync guard (RN keys credentials per registry id, no shared slot).
Expand Down
Loading