diff --git a/PARITY_CHECKLIST.md b/PARITY_CHECKLIST.md index 5a474e6a..afbb5754 100644 --- a/PARITY_CHECKLIST.md +++ b/PARITY_CHECKLIST.md @@ -75,23 +75,34 @@ commit hash. The remaining `- [ ]` items are either deferred with a note 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: 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 | P1 | P2 | P3 | -|---|---|---|---|---|---|---| -| 01 | Authentication, login, session, multi-account | [docs/parity/01-auth-accounts.md](docs/parity/01-auth-accounts.md) | 30 | 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 | 1 | 20 | 33 | -| 03 | Email viewer, thread view, rendering, attachments | [docs/parity/03-email-viewer.md](docs/parity/03-email-viewer.md) | 46 | 6 | 13 | 26 | -| 04 | Composer, drafts, sending, identities, templates, scheduled send | [docs/parity/04-composer-send.md](docs/parity/04-composer-send.md) | 51 | 5 | 14 | 32 | -| 05 | Calendar and tasks | [docs/parity/05-calendar.md](docs/parity/05-calendar.md) | 51 | 5 | 20 | 26 | -| 06 | Contacts and address books | [docs/parity/06-contacts.md](docs/parity/06-contacts.md) | 51 | 1 | 18 | 31 | -| 07 | Filters (Sieve), vacation responder, Files | [docs/parity/07-filters-vacation-files.md](docs/parity/07-filters-vacation-files.md) | 33 | 3 | 8 | 22 | -| 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 | 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 | 7 | 23 | 26 | -| | **Total** | | **418** | **32** | **138** | **246** | +| # | 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 | 37 | 9 | 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** | **379** | **36** | **32** | **138** | **244** | -Counts are of `- [ ]` items per file (a handful of "verified" notes carry a -priority tag too, so P-sums can differ by one or two from the item count). +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) diff --git a/docs/parity/01-auth-accounts.md b/docs/parity/01-auth-accounts.md index 3f533912..050b8dcd 100644 --- a/docs/parity/01-auth-accounts.md +++ b/docs/parity/01-auth-accounts.md @@ -131,7 +131,7 @@ RN covers the happy paths (password login, webmail-mediated OAuth handoff, QR pa - What RN does: `MAX_ACCOUNTS = 5` (`src/lib/account-utils.ts:1`) although only the active account holds a live connection. - Fix hint: raise the constant (or drop the cap) once the unified inbox cost per account is acceptable. -- [ ] **Shared/group account settings scope is missing** — `P3` — `missing` (changelog 1.7.5 "Manage shared/group account settings from the Accounts page") — deferred: shared accounts are now listed read-only in AccountSettings (490355c); scoped filter/vacation stores need an accountId parameter first +- [ ] **Shared/group account settings scope is missing** — `P3` — `missing` (changelog 1.7.5 "Manage shared/group account settings from the Accounts page") — partly: shared accounts are listed in AccountSettings (490355c), and tapping one scopes Filters and Vacation to it since a86cd1d; calendar and contacts settings are not scoped yet - What WEB does: lists `client.getSharedAccounts()` (non-primary) on the Accounts page and enters a scoped settings mode (filters, vacation, calendars, contacts) via `managed-account-store` (`components/settings/account-settings.tsx:43-48, 112-118, 225-262`; `stores/managed-account-store.ts`). - What RN does: `getSharedMailAccounts()` exists for mail/unified inbox (`src/api/jmap-client.ts:480-495`) but there is no shared-account listing or scoped settings. - Fix hint: list non-personal `session.accounts` in `AccountSettings` and pass an `accountId` into the filter/vacation stores. diff --git a/docs/parity/02-mail-list-folders.md b/docs/parity/02-mail-list-folders.md index 8fa46c0d..ea7a6590 100644 --- a/docs/parity/02-mail-list-folders.md +++ b/docs/parity/02-mail-list-folders.md @@ -74,7 +74,7 @@ RN covers the core single-folder loop well (folder tree incl. shared/group accou ### Unified mailbox / cross-account views -- [x] **Unified scope: account-bounded by default, cross-account opt-in; single-account + group inboxes case** — `P2` — `partial` — fixed in 6ae21d5 +- [x] **Unified scope: account-bounded by default, cross-account opt-in; single-account + group inboxes case** — `P2` — `partial` — fixed in 6ae21d5; the drawer only shows the Unified section when it can hold more than one account's mail since af6290f (#843) - What WEB does: unified views stay inside the active account (own + shared/group folders) unless `unifiedCrossAccount` is on *and* the admin gate allows it (`components/mail/mail-app.tsx:465-500`, `stores/settings-store.ts:405-431`); the section is shown with one account as soon as `includeGroupInUnified && hasGroupInboxes` (`components/layout/sidebar.tsx:905-907`). - What RN does: "All inboxes" is shown only when `accounts.length > 1` (`src/components/SidebarDrawer.tsx:438-446`) and always spans every account (`src/api/unified-inbox.ts:236-257`). A single account with group inboxes gets no unified view even with "Include Group Inboxes" on (`ReadingSettings.tsx:257`). Admin policy gates do not exist in RN (see N/A). - Fix hint: show the row when `accounts.length > 1 || (includeGroupInUnified && mailboxes.some(m => m.isShared))`; add a `unifiedCrossAccount` toggle (default off, like WEB) and pass only the active account id when it is off. Note WEB's default for `includeGroupInUnified` is `true` (`settings-store.ts:639`) while RN defaults to `false` (`src/stores/settings-store.ts:235`). @@ -102,7 +102,7 @@ RN covers the core single-folder loop well (folder tree incl. shared/group accou - [x] **No actions on unified rows (swipe, star, read, delete, archive, spam, selection)** — `P2` — `missing` — fixed in 6ae21d5 - What WEB does: every list action resolves the email's own client + owner account via `resolveEmailActionContext` (`stores/email-store.ts:581-640`), incl. batch actions grouped by `sourceAccountId` (`:2634-2660`, `:2740-2760`), archive into the owner's archive (`components/mail/mail-app.tsx:2141-2160`); changelog 1.7.5/1.7.7 "route counter/keyword updates to the email's own account in aggregate views". - What RN does: rows are plain `Pressable`s (`src/screens/UnifiedInboxScreen.tsx:95-138`); nothing but open. - - Fix hint: wrap rows in `SwipeableRow`; because the JMAP client is single-account, actions on a *different registry account* need the detached `jmapPost` path (`unified-inbox.ts:116-131`) with `Email/set` against `email.jmapAccountId`; for the active account and its group accounts route through `setEmailKeywords(..., email.jmapAccountId)` / `moveEmail(...)` overrides. + - Fix hint: wrap rows in `SwipeableRow`; because the JMAP client is single-account, actions on a *different registry account* need the detached `jmapPost` path (`unified-inbox.ts:116-131`) with `Email/set` against `email.jmapAccountId`; for the active account and its group accounts route through `setEmailKeywords(..., email.jmapAccountId)` / `moveEmail(...)` overrides. (`setEmailKeywords` is gone since 5041897; keyword changes now go through `patchKeywordsForEmails` in `src/api/email.ts` or the store's `setKeywordForEmails`.) - [x] **Unified list stays stale after acting on a message; just-read mail handling** — `P3` — `partial` — fixed in 6ae21d5 - What WEB does: aggregate views refresh through the fan-out on push (`refreshCurrentMailbox`, #791) and keep just-read/unstarred rows in the Unread/Starred views until re-opened (`retainedInViewIds`, `stores/email-store.ts:1007-1050`). @@ -130,7 +130,7 @@ RN covers the core single-folder loop well (folder tree incl. shared/group accou - What RN does: `EmailRow` shows no tags; `tagPill/tagDot/tagText` styles exist but are unused (`src/screens/EmailListScreen.tsx:1481-1495`). Tags are only visible inside the thread screen tag menu. - Fix hint: compute tag ids from `item.keywords` (`$label:*`, `$color:*`), look up `useKeywordsStore().keywords` for label/colour (unknown ids → grey with raw id), render pills on the subject row; add a `tintListRowsByTag` setting if desired. -- [x] **Tag view (tap a tag → cross-folder `hasKeyword` list, #175) and tag counts in the drawer** — `P2` — `missing` — fixed in 6ae21d5 +- [x] **Tag view (tap a tag → cross-folder `hasKeyword` list, #175) and tag counts in the drawer** — `P2` — `missing` — fixed in 6ae21d5; the view and the badges cover shared accounts since 8d5bced (#1038), and the counts are no longer re-fetched on every drawer open since d097ae9 - What WEB does: "Tags" sidebar section with unread/total per tag (`fetchTagCounts`, `stores/email-store.ts:1192`), visibility rules (`components/layout/sidebar.tsx:1002-1013`), `selectKeyword` → `getEmails(undefined, …, '$label:')` across all folders (`stores/email-store.ts:1343-1420`, changelog 1.4.13 #175), counts kept in step with read/unread (1.7.8). - What RN does: no tag section in `SidebarDrawer`; `EmailFilters` has no keyword field (`src/stores/email-store.ts:140-149`). - Fix hint: add `keyword?: string` to `EmailFilters` and a "Tags" section in the drawer; when set, `buildJmapFilter` should omit `inMailbox` (search across folders) and add `{ hasKeyword: '$label:' }`; counts via one `Email/query` (calculateTotal) per tag, optional. @@ -219,7 +219,7 @@ RN covers the core single-folder loop well (folder tree incl. shared/group accou - What RN does: `formatListDate` follows the app locale only (`src/lib/date-format.ts:17-31`), "Just now"/"m ago" are English (`:38-41`). - Fix hint: localize the relative strings via `t()`; add a `dateLocale` select if parity is wanted. -- [x] **Show avatars in Junk (off by default, 1.5.1); avatar priority** — `P3` — `partial` — fixed in bea3fc4 +- [x] **Show avatars in Junk (off by default, 1.5.1); avatar priority** — `P3` — `partial` — fixed in bea3fc4; contact photos win over favicons and initials since eca3871 (#54) - What WEB does: `showAvatarsInJunk` hides favicon/photo images in Junk (`components/email/thread-list-item.tsx:137`); avatar priority contact photo > plugin (Gravatar) > favicon > initials (`components/ui/avatar.tsx:236`). - What RN does: favicons always load, including in Junk (`src/components/SenderAvatar.tsx:37`); no contact-photo lookup. - Fix hint: pass `disableImages={currentMailbox.role === 'junk' && !showAvatarsInJunk}` into `SenderAvatar`; contact photos optional. @@ -291,11 +291,11 @@ RN covers the core single-folder loop well (folder tree incl. shared/group accou - Missing Archive/Trash/Junk → error alert (`EmailListScreen.tsx:363-387`), never silently destroys when Trash is missing (#195 equivalent). - Delete semantics: trash / trash-and-read (#323) / permanent / junk auto-permanent, single and batch (`email-store.ts:1268-1335`, `1434-1527`); undo snackbar for archive/delete/move (single + batch) — RN is ahead of WEB here. - Archive single / by year / by month with folder creation in one request, batch archive, "Reorganize existing archive" (`src/api/email.ts:579-692`, `ReadingSettings.tsx:105-159`). -- Swipe actions: configurable per direction, instant and reveal modes, pure gesture module with tests, stale-props fix (`SwipeableRow.tsx`, `swipe-gesture.ts`); more actions than WEB (pin, move). RTL: RN ships no RTL locales, nothing to do yet. +- Swipe actions: configurable per direction, instant and reveal modes, pure gesture module with tests, stale-props fix (`SwipeableRow.tsx`, `swipe-gesture.ts`); more actions than WEB (pin, move). RTL: ar, he and fa ship since 990cd84, but swipe directions are not mirrored yet (area 08, RTL finding). - Multi-select: long-press, select-all-visible/indeterminate, batch star/read/tag/move/archive/delete (`EmailListScreen.tsx:320-500`). - Pagination via `onEndReached`, pull-to-refresh, incremental `Email/queryChanges` + `Email/changes` refresh with per-account state tokens, base-view restore after search (#10), sort change invalidates snapshots (#5), offline seed and fallback. - Search: full-text with wildcard suffix (`src/lib/search-utils.ts` = WEB `toWildcardQuery`), from/to/subject/date/attachment/unread/starred tri-state filters, chips row, search inside a shared folder routed to its owner. -- Keyword writes send the whole `keywords` map (no JSON-pointer escaping issue, no `false` values) — not affected by 5c484af1 / be97c8bf; tag ids normalized exactly like WEB `normalizeKeywordLevel` (`KeywordSettings.tsx:118-123`); batch tag toggle via `TagSheet` with "all have it" semantics. +- Keyword writes send RFC 6901-escaped `keywords/` patch pointers since 5041897 (`src/api/patch-pointer.ts`; the whole-map writes erased other keywords, audit B4), with `null` instead of `false` — not affected by 5c484af1 / be97c8bf; tag ids normalized exactly like WEB `normalizeKeywordLevel` (`KeywordSettings.tsx:118-123`); batch tag toggle via `TagSheet` with "all have it" semantics. - Unread dot (#27), date formats smart/relative/full with 12/24h, preview toggle, density-aware rows, sender favicon avatars with failure cache, `.eml`/`.zip` import into the open folder, Scheduled quick view, "Include group inboxes" setting and shared badge/account dot in the unified list. - Mark-as-read delay (0/3s/5s) in the thread screen; both clients still display `receivedAt` in the list (WEB `lib/email-date.ts` is not wired into the list yet). @@ -305,6 +305,6 @@ RN covers the core single-folder loop well (folder tree incl. shared/group accou - Admin policy gates for unified/cross views (`usePolicyStore` feature flags) — RN has no admin policy channel; only the user-side toggles apply. - Return-to-list-after-action setting — the RN thread screen always returns after delete/archive/spam. - "Refresh" toolbar button and F5/Ctrl+R interception — pull-to-refresh covers it. -- Favicon unread badge, sidebar collapse/resize, Pro multi-pane shell, folder subscription (neither client exposes `isSubscribed`). +- Favicon unread badge (its native counterpart, the app-icon badge, landed in 25c0883, iOS only), sidebar collapse/resize, Pro multi-pane shell, a folder subscription UI (neither client shows `isSubscribed`; both subscribe the folders they create, native since 30aea63, #951). - Plugin search hooks / external search results, `onEmailsFetched` transforms. - Source-folder chip on rows is N/A *until* All-mail/cross views exist in RN. diff --git a/docs/parity/03-email-viewer.md b/docs/parity/03-email-viewer.md index e5b2790c..8e89c446 100644 --- a/docs/parity/03-email-viewer.md +++ b/docs/parity/03-email-viewer.md @@ -124,7 +124,7 @@ RN has a solid single-message reader (WebView body with CSP, shrink-to-fit + pin - [x] **Tags, `$important` badge and answered/forwarded state in the header — fixed in d2ed27f** — `P3` — `missing` - What WEB does: tag badges (removable) under the subject, "Important" pill (`components/email/email-viewer.tsx:3789-3806`); `$answered`/`$forwarded` set after send (`components/mail/mail-app.tsx:1663-1667`). - What RN does: Tag sheet exists but no badges are rendered in the pane; `$answered`/`$forwarded` are never set after a reply/forward (no hit for `$answered` in `src/`). - - Fix hint: render `keywordDefs` matches from `email.keywords` under the subject; after a successful send in `ComposeScreen` call `setEmailKeywords(originalId, {...keywords, $answered|$forwarded: true}, accountId)`. + - Fix hint: render `keywordDefs` matches from `email.keywords` under the subject; after a successful send in `ComposeScreen` call `setEmailKeywords(originalId, {...keywords, $answered|$forwarded: true}, accountId)`. (`setEmailKeywords` is gone since 5041897: set `keywords/$answered` or `keywords/$forwarded` through `patchKeywordsForEmails` or the store's `setKeywordForEmails`.) ### Calendar invitation banner @@ -138,7 +138,7 @@ RN has a solid single-message reader (WebView body with CSP, shrink-to-fit + pin - What RN does: `extractMethodFromRawIcs` exists (`src/lib/calendar-invitation.ts:103-106`) but is never called; only `inferInvitationMethod` heuristics are used (`CalendarInvitationBanner.tsx:62`), so a cancellation whose event status is not `cancelled`, or a REPLY from a single attendee, is shown as a plain invitation with RSVP buttons. - Fix hint: fetch the blob text (`getDownloadUrl` + `secureFetch`, as `fetchRawEmail` does) in parallel with parse and prefer `extractMethodFromRawIcs`. -- [ ] **Banner lacks WEB's trust assessment, actor summary, existing-event / "already in calendar" state, calendar picker, "View in calendar", collapse, sequence badge, counter-proposal review** — `P3` — `partial` — deferred: trust assessment, iTIP method and calendar picker landed in 8206893 (calendar agent); existing-event state, "View in calendar", collapse, sequence badge and counter-proposal review remain open in the calendar area +- [ ] **Banner lacks WEB's trust assessment, actor summary, existing-event / "already in calendar" state, calendar picker, "View in calendar", collapse, sequence badge, counter-proposal review** — `P3` — `partial` — deferred: trust assessment, iTIP method and calendar picker landed in 8206893 (calendar agent); the existing-event state followed in c0582ab, which looks the event up on the server by `uid` (also so Accept sends the RSVP, audit B22); "View in calendar", collapse, sequence badge and counter-proposal review remain open in the calendar area - What WEB does: `getInvitationTrustAssessment` (sender vs organizer + auth results) warning, actor line ("X accepted"), `queryCalendarEvents({uid})` to show "already in calendar" and current RSVP, picker when >1 calendar, "View in calendar" navigation, collapsible card, `sequence` "updated" pill, apply counter proposal for organizers (`components/email/calendar-invitation-banner.tsx:392-474`, `502-553`, `657-672`, `721-752`, `802-1140`). - What RN does: title/date/location/video/organizer rows, Yes/Maybe/No, Add to first writable calendar (`src/components/email/CalendarInvitationBanner.tsx:83-207`); no dedupe check before offering "Add", no picker, no trust warning, no navigation to the event. - Fix hint: port `getInvitationTrustAssessment` (needs the headers finding for auth), look up `useCalendarStore.events` by `uid` to switch to "already in calendar"/current response, add a calendar picker sheet when `calendars.length > 1`, and a "View in calendar" button that navigates to `CalendarScreen` with the start date. @@ -165,7 +165,7 @@ RN has a solid single-message reader (WebView body with CSP, shrink-to-fit + pin - What RN does: shows `winmail.dat` as an opaque attachment; the real files inside are unreachable. - Fix hint: `lib/tnef.ts` is pure `Uint8Array` code; copy it and feed `jmapClient.fetchBlobArrayBuffer`. -- [x] **Attachment chip list: no MIME-based fallback names, MDN/DSN report parts and non-inline cid images mishandled — fixed in d2ed27f** — `P3` — `partial` +- [x] **Attachment chip list: no MIME-based fallback names, MDN/DSN report parts and non-inline cid images mishandled — fixed in d2ed27f** — `P3` — `partial` — body-embedded `cid:` parts typed octet-stream are hidden from the chips since 1d1d6a5 - What WEB does: unnamed parts get `Document.pdf`/`Email.eml`/`Attachment.` (`components/email/email-viewer.tsx:191-225`); `message/disposition-notification` and `message/delivery-status` parts are hidden (`1604-1606`); inline hiding requires `disposition === 'inline'` (`1601-1603`). - What RN does: unnamed → `'attachment'` (`src/screens/EmailThreadScreen.tsx:745`); report parts are listed; any `cid` image is hidden regardless of disposition (`719-721`), so an image attached with a Content-ID but `disposition: attachment` (common from some clients) disappears from the list even when the body does not reference it. - Fix hint: port `getAttachmentDisplayName`, add the two report-type filters, and check `disposition === 'inline'` (or that the body actually references the cid via `extractCidRefs`). @@ -247,7 +247,7 @@ RN has a solid single-message reader (WebView body with CSP, shrink-to-fit + pin - External content policy ask/block/allow, per-message "Load external content", "Trust sender" persisting locally and to the address book, trusted list from the address book and contacts (`src/components/EmailBodyView.tsx:480-535`, `643-672`). - cid inline images fetched with auth and injected as data URIs, transparent placeholder otherwise (`src/components/EmailBodyView.tsx:546-584`, `src/lib/email-html.ts:213-218`). - Shrink-to-fit of fixed-width emails + in-page pinch zoom/pan with pager/scroll locking (`src/components/EmailBodyView.tsx:159-460`) — RN-only, better than WEB's horizontal scroll (#409). -- Mark read on open with delay setting (apart from the `-1` bug), optimistic star/unread/tag toggles, delete/archive/spam/not-spam/move scoped to the message's account, `no trash folder` alert (`src/screens/EmailThreadScreen.tsx:221-364`). +- Mark read on open with delay setting (apart from the `-1` bug), optimistic star/unread/tag toggles, delete/archive/spam/not-spam/move scoped to the message's account, `no trash folder` alert (`src/screens/EmailThreadScreen.tsx:221-364`). The account scoping was wrong for messages opened from the Unified Inbox, a notification or a deep link until 5c7301b (audit B3); star, tag and unread are queued offline since ddff2f7. - Prev/next navigation with prefetched neighbour panes and swipe (`src/screens/EmailThreadScreen.tsx:122-181`, `535-572`) — RN-only pager. - Skeleton while loading (`src/screens/EmailThreadScreen.tsx:845-886`). - Attachments: chips with size, show-all toggle, `hideInlineImageAttachments` setting, `attachmentPosition` setting, `mailAttachmentAction` preview/download, templated filenames + transforms identical to WEB (`src/lib/download-filename.ts` diff vs `lib/download-filename.ts` is whitespace/quotes only, minus `DEFAULT_BUNDLE_TEMPLATE`/`BUNDLE_TOKENS`), Downloads settings screen with live preview (`src/components/settings/DownloadsSettings.tsx`). @@ -259,7 +259,7 @@ RN has a solid single-message reader (WebView body with CSP, shrink-to-fit + pin ## N/A on mobile - Print (WEB `handlePrint`, `components/email/email-viewer.tsx:2621-2670`) — could be done with `expo-print` later, but not a parity requirement. -- Copy permalink / deep link to a message (#733) — RN has no linking configuration; no `linking` prop in `App.tsx`. +- Copy permalink to a message (#733) — RN opens incoming message links since 5802cae (`src/navigation/linking.ts`) but has no "copy link" action. - Fullscreen reading toggle, Pro split panes, toolbar overflow measurement, plugin slots/hooks (`email-banner`, `email-detail-sidebar`, `onRenderEmailBody`, `onBeforeExternalLink`, `ui.rerenderEmail`), keyboard shortcuts, drag attachments out to the file system (#267), drag emails out as `.eml`, import `.eml` from the viewer menu (RN imports from the list screen instead). - Iframe-specific fixes: "Render the email body on DOM parse instead of iframe load" (#635), iframe flash/flicker fixes (1.5.4, 1.6.7), `blob:` in CSP `object-src`/`frame-src` for PDF previews (#253), pdf.js bytes vs `blob:` fetch (#871), RSC 307 loop (#919), iOS Safari viewport zoom on input focus (#838). - Demo-mode welcome pane, tour, contact sidebar panel layout (desktop only; the actions themselves are listed above). diff --git a/docs/parity/04-composer-send.md b/docs/parity/04-composer-send.md index 483a27f1..f21231a4 100644 --- a/docs/parity/04-composer-send.md +++ b/docs/parity/04-composer-send.md @@ -54,7 +54,7 @@ Legend for refs: WEB paths are relative to `the webmail repo`, RN paths to `the - [x] **Reply / forward does not mark the original `$answered` / `$forwarded`** — fixed in 7f956d6 — `P2` — `missing` - What WEB does: after a successful reply/forward the original gets `$answered` or `$forwarded` (`components/mail/mail-app.tsx:1663-1673`), routed to the owning account. - What RN does: nothing — `grep -F '$answered'` over `src/` returns no hits; `performSend` (`ComposeScreen.tsx:941-974`) has no access to the original email id (`replyTo` params in `src/navigation/types.ts:10-19` carry no `emailId`). - - Fix hint: add `originalEmailId`/`jmapAccountId` to the `replyTo` route param and call the existing keyword primitive (`setEmailKeywords` via `applyOrQueue` in `src/stores/outbox-store.ts`) after the send resolves. + - Fix hint: add `originalEmailId`/`jmapAccountId` to the `replyTo` route param and call the existing keyword primitive (`setEmailKeywords` via `applyOrQueue` in `src/stores/outbox-store.ts`) after the send resolves. (`setEmailKeywords` is gone since 5041897; the keyword primitive is now `patchKeywordsForEmails`, and the outbox queues `keywords/` patches.) - [x] **Identity `replyTo` / `bcc` are never applied to outgoing mail** — fixed in 7f956d6 — `P2` — `missing` - What WEB does: `sendEmail` copies the identity's `replyTo` onto the message (`lib/jmap/client.ts:3167-3172`, `:3184`); identity form lets users edit Reply-To and Bcc (`components/identity/identity-form.tsx:39-41`, `:66-70`, `:132-133`). @@ -281,7 +281,7 @@ Legend for refs: WEB paths are relative to `the webmail repo`, RN paths to `the - Fix hint: route through `t()` with keys in `locales//common.json`. ## Verified at parity (brief list, so the fixer knows what NOT to redo) -- Scheduled send via `EmailSubmission` envelope `HOLDFOR` with explicit `rcptTo` (bare addresses, names stripped) and `maxDelayedSend`/FUTURERELEASE capability checks: `src/api/email.ts:828-844`, `src/api/jmap-client.ts:513-531`, `ComposeScreen.tsx:796-821` — matches `lib/jmap/client.ts:592-612`, `:3245-3258`. +- Scheduled send via `EmailSubmission` envelope `HOLDFOR` with explicit `rcptTo` (bare addresses, names stripped) and `maxDelayedSend`/FUTURERELEASE capability checks: `src/api/email.ts:828-844`, `src/api/jmap-client.ts:513-531`, `ComposeScreen.tsx:796-821` — matches `lib/jmap/client.ts:592-612`, `:3245-3258`. The capability check read only the session-level object, which Stalwart leaves empty, so scheduling and the undo delay were off on Stalwart until e28e8b4 (#57, audit B8); holds are capped at Stalwart's 7-day limit since 4e7ae2f. - Undo-send delay setting (0/5/10/20/30 s) applied only when the server supports delayed send: `ComposeScreen.tsx:825-828`, `ComposingSettings.tsx:42-48`. - Send-time readback of the live editor DOM with timeout + loud abort (RN #9/#11): `RichTextEditor.tsx:143-152`, `ComposeScreen.tsx:890-902`; escape-free page script with a parse test. - `Name ` recipients split into JMAP `name`/`email` (#672): `ComposeScreen.tsx:69-84`, `:944-947`. @@ -310,4 +310,4 @@ Legend for refs: WEB paths are relative to `the webmail repo`, RN paths to `the - Signature "double-click to unlock" atom (Tiptap-specific); the RN contenteditable is always editable. - Quoted-HTML shadow-island typing detection (#654) — Tiptap/shadow-DOM specific. - Send through another logged-in account's client for DKIM (#461) — RN is single-active-account; identities always belong to the submitting account. -- Route scheduled sends / identity on reschedule to a shared account (#874, a15af722) — RN does not compose on behalf of shared accounts. +- Route scheduled sends / identity on reschedule to a shared account (#874, a15af722) — RN does not compose on behalf of shared accounts, but the Scheduled screen lists, cancels, reschedules and edits sends held in shared accounts since 72f48db. diff --git a/docs/parity/05-calendar.md b/docs/parity/05-calendar.md index 49fff812..84ab8b21 100644 --- a/docs/parity/05-calendar.md +++ b/docs/parity/05-calendar.md @@ -10,7 +10,7 @@ RN has a solid read path (month/week/agenda, shared-calendar namespacing, per-vi - [x] **Recurrence scope dialog is a stub: "This event" / "This and following" behave as "All events"** — `P1` — `missing` — fixed in 161baf1 - What WEB does: `handleScopeSelect` implements all three scopes: "this" writes a `recurrenceOverrides/` patch (or the synthetic id on Stalwart >= 0.16.20), "this_and_future" truncates the master with `until` and creates a new series, "all" patches the master; delete does the mirror (exclude / truncate / destroy) and refetches the range (ref `components/calendar/calendar-app.tsx:842-998`, `lib/recurrence-overrides.ts:21-32`, changelog 1.9.0 "Edit a single recurring occurrence via a one-shot override patch"). - What RN does: `handleScopeSelect` ignores the chosen scope with a TODO ("treat all scopes as 'all'") and either opens the edit modal on the occurrence or deletes `action.event.id`, which the store resolves to the master via `originalId` (ref `src/screens/CalendarScreen.tsx:347-363`, `src/stores/calendar-store.ts:306-329`). Choosing "This event" on delete destroys the entire series. - - Fix hint: port WEB's `handleScopeSelect`/`truncateRecurrenceAtEvent`/`findMasterEvent` into CalendarScreen; add `buildRecurrenceOverridePatch` (WEB `lib/recurrence-overrides.ts`) and `RECURRENCE_OVERRIDE_IMMUTABLE_KEYS`; for "this" write `{ [`recurrenceOverrides/${recurrenceId}`]: override }` on `originalId`; for delete-this write `{ excluded: true }`; refresh after. + - Fix hint: port WEB's `handleScopeSelect`/`truncateRecurrenceAtEvent`/`findMasterEvent` into CalendarScreen; add `buildRecurrenceOverridePatch` (WEB `lib/recurrence-overrides.ts`; removed again in f397ee3, where "This event only" goes to `updateEvent`/`deleteEvent` with the occurrence's id) and `RECURRENCE_OVERRIDE_IMMUTABLE_KEYS`; for "this" write `{ [`recurrenceOverrides/${recurrenceId}`]: override }` on `originalId`; for delete-this write `{ excluded: true }`; refresh after. - [x] **Editing an expanded occurrence rewrites the master's `start` to the occurrence date (series shifts / earlier occurrences vanish)** — `P1` — `bug` — fixed in 161baf1 - What WEB does: for scope "all" the updates go to the master but `start` from an occurrence is never applied to the master unless the user changed it; scope "this" goes into an override (ref `components/calendar/calendar-app.tsx:930-943`). @@ -27,7 +27,7 @@ RN has a solid read path (month/week/agenda, shared-calendar namespacing, per-vi - What RN does: `buildRuleFromEditorValue` sets `rscale: 'gregorian', skip: 'omit'` on every custom rule (ref `src/lib/recurrence.ts:220-221`). RN affected: any custom recurrence created on mobile breaks Android CalDAV sync. - Fix hint: delete the two properties in `buildRuleFromEditorValue`; the simple presets in `EventModal.tsx:143-146` are already clean. -- [ ] **Synthetic occurrence ids / server-side expansion (Stalwart >= 0.16.20) not used** — `P3` — `missing` — deferred: optional; client-side expansion is at parity and the scope dialog now writes overrides through the master, so the synthetic-id path buys nothing functionally yet +- [x] **Synthetic occurrence ids / server-side expansion (Stalwart >= 0.16.20) not used** — `P3` — `missing` — done in 8454889 (server expansion when the probe finds synthetic ids, device-side expansion otherwise), f397ee3 (one occurrence changed or deleted through its own id) and 96c5a3f (RSVP to one occurrence) - What WEB does: probes `CalendarEvent/set` with `h333333` once, then queries with `expandRecurrences: true`, hydrates occurrences from base events and writes occurrence patches through the synthetic id with a fallback to base-event overrides (ref `lib/jmap/client.ts:5648-5680, 5747-5751, 5828-5868`, `lib/recurrence-instances.ts:35, 73, 208-232`, `stores/calendar-store.ts:82-184`). - What RN does: always fetches raw events and expands on the client (`src/stores/calendar-store.ts:228`, `src/lib/recurrence-expansion.ts`). Functionally fine on all server versions; RN's expansion file is a line-for-line port of WEB's (diffed: only comments, formatting and the `isServerRecurrenceInstance` pass-through differ). - Fix hint: optional. If adopted, port `recurrence-instances.ts` wholesale and the `isServerRecurrenceInstance` guard in `expandRecurringEvents`. @@ -169,7 +169,7 @@ RN has a solid read path (month/week/agenda, shared-calendar namespacing, per-vi ### Views / navigation / locale -- [ ] **Entire calendar UI is hard-coded English although RN has i18n** — `P2` — `missing` — partial in 0f80e98: screen, month/week/agenda views, event card/sheet/modal, drawer, scope dialog, settings, banner and calendar/share sheets use `t()` + date-fns locales; TasksSheet (09c24fa) and `formatReminder` (2abea97) localized too; deferred: ICalImportSheet, ICalSubscriptionSheet and RecurrenceEditor still hard-code English +- [ ] **Entire calendar UI is hard-coded English although RN has i18n** — `P2` — `missing` — partial in 0f80e98: screen, month/week/agenda views, event card/sheet/modal, drawer, scope dialog, settings, banner and calendar/share sheets use `t()` + date-fns locales; TasksSheet (09c24fa), `formatReminder` (2abea97) and ICalImportSheet (b7f7110) localized too; deferred: ICalSubscriptionSheet and RecurrenceEditor still hard-code English - What WEB does: everything through `next-intl` (`t('calendar.*')`), month/day names and popover dates localized (changelog 1.6.x "Localize event start date in detail popover and event modal"). - What RN does: `src/i18n/index.ts` + `useLocaleStore` are used by 18 other files, but none of `src/components/calendar/*`, `src/screens/CalendarScreen.tsx` or `CalendarSettings.tsx` import it; all labels ("Today", "No events", "Does not repeat", "Going?", RSVP labels, drawer titles, etc.) and `date-fns` `format()` calls without a `locale` are English (`CalendarScreen.tsx:408, 448, 504, 640-641`, `EventModal.tsx:60-67, 326, ...`, `MonthView.tsx:25-26`, `AgendaView.tsx:33-37`). - Fix hint: add a `calendar.*` namespace to `locales/*.json` (WEB's keys can be copied) and pass a date-fns locale from the locale store into `format()`. @@ -194,7 +194,7 @@ RN has a solid read path (month/week/agenda, shared-calendar namespacing, per-vi - What RN does: `CalendarSettings` shows "Hover preview" with delay options and nothing consumes `calendarHoverPreview` (`src/components/settings/CalendarSettings.tsx:87-102`; no other reference in `src/`). - Fix hint: remove the setting row (keep the store key for settings-sync compatibility or drop it). -- [x] **No day view (falls back to agenda); tapping a month day does not open the day** — `P3` — `partial` — fixed in 0f80e98 ("Day" hidden from the mobile default-view options; a synced "day" shows as Agenda) +- [x] **No day view (falls back to agenda); tapping a month day does not open the day** — `P3` — `partial` — fixed in 0f80e98 ("Day" hidden from the mobile default-view options; a synced "day" shows as Agenda); a real day view is back since de3d697, with the week and month views scrolling freely (de3d697, caf61ec, #759) and an endless agenda (71522b9) - What WEB does: day view with time grid; on mobile, tapping a date in month view switches to day view and offers "back to month" (ref `components/calendar/calendar-app.tsx:461-470`, `calendar-day-view.tsx`). - What RN does: `calendarDefaultView === 'day'` maps to agenda (`src/screens/CalendarScreen.tsx:131-137`); month view shows a list below the grid instead. Acceptable UX, but the settings option "Day" is misleading. - Fix hint: either reuse `WeekView` with a single-day `weekDays` array as a day grid, or hide "Day" from the RN default-view options. @@ -267,7 +267,7 @@ RN has a solid read path (month/week/agenda, shared-calendar namespacing, per-vi ### Settings -- [ ] **No custom time-zone setting (#755); no birthday-calendar colour; no "Day" view on mobile (see above)** — `P3` — `missing` — time-zone setting (`calendarTimeZone`, used for saved events and JMAP queries) done in 58a6f45 + 0f80e98; deferred: birthday-calendar colour setting +- [ ] **No custom time-zone setting (#755); no birthday-calendar colour; no "Day" view on mobile (see above)** — `P3` — `missing` — time-zone setting (`calendarTimeZone`, used for saved events and JMAP queries) done in 58a6f45 + 0f80e98; the editors and views work in calendar-zone wall time since a8e6f6e and 8af163a, and floating task dues since 1a651a9; deferred: birthday-calendar colour setting - What WEB does: `timeZone: 'auto' | IANA` overriding browser detection, used for display (`displayNow`, `toDisplayDate`) and for the `timeZone` on saved events (ref `lib/timezone.ts:29-75`, `stores/settings-store.ts:317, 553`, changelog 1.9.2 #755); `birthdayCalendarColor` (`settings-store.ts:377, 609`). - What RN does: device zone only (`src/api/calendar.ts:15-21`); birthday colour fixed (`src/lib/birthday-calendar.ts:7`). - Fix hint: add `timeZone` to the settings store (synced with WEB's key so it round-trips through settings sync), pass it as the JMAP `timeZone` arg and into saved events; convert display via `Intl` like WEB's `getWallClock`. diff --git a/docs/parity/06-contacts.md b/docs/parity/06-contacts.md index 9f03768a..558267f6 100644 --- a/docs/parity/06-contacts.md +++ b/docs/parity/06-contacts.md @@ -190,7 +190,7 @@ RN covers the visible surface reasonably well (list with alphabetical index, det ### Composer autocomplete - [ ] **Autocomplete lacks groups, recent recipients (Sent), on-demand server search and directory principals** — `P2` — `missing` - - store-side done in 26a34d5 (`getAutocomplete` with group entries + recent recipients, `loadRecentRecipients`/`searchRecipients` fed by `src/api/recent-recipients.ts`, `getGroupRecipients`, #672 normalisation); wiring `ComposeScreen`/`ParticipantInput` belongs to the composer agent; directory principals not ported. + - store-side done in 26a34d5 (`getAutocomplete` with group entries + recent recipients, `loadRecentRecipients`/`searchRecipients` fed by `src/api/recent-recipients.ts`, `getGroupRecipients`, #672 normalisation); the composer uses `getAutocomplete` and the recent Sent recipients since 5b72c4d. Still open: the on-demand server search (`searchRecipients` has no caller), directory principals, and the calendar's `ParticipantInput`, which still offers contacts only. - What WEB does: `getAutocomplete` merges contacts, group entries (as one chip with `memberCount`), RFC 9670 directory principals and recent Sent recipients, deduped and sanitised (#672) (`stores/contact-store.ts:622-707, 23-49`; `loadRecentRecipients` `:1118-1142` fed from `mail-app.tsx:423`; `searchRecipients` `:1144-1154` used by `components/email/email-composer.tsx:1175-1212`; group chip insertion `:1218-1233`; changelogs 1.6.x "Recipient autocomplete from Sent, with on-demand server search", 1.7.x group chips). - What RN does: suggestions come only from `individuals` matched with `matchesContactSearch` (`src/screens/ComposeScreen.tsx:367-392`); groups are explicitly filtered out and nothing else is consulted. `ParticipantInput` (calendar) likewise flattens contacts only (`src/components/calendar/ParticipantInput.tsx:34-51`). - Fix hint: add `recentRecipients` (scan Sent `to/cc`, 300 msgs) to the contacts store, include groups as a pickable entry that expands to member emails on pick, and reuse `sanitizeDisplayName/splitMailbox`-style cleanup for names that contain a mailbox (#672). @@ -203,7 +203,7 @@ RN covers the visible surface reasonably well (list with alphabetical index, det - What RN does: no contact lookup or "add" action in `EmailThreadScreen` (grep contact/ContactForm: only `SenderAvatar` at `:794`). Only `ContactForm`'s `route.params` support `contactId/addressBookId/asGroup` (`src/navigation/types.ts:24`) — no email/name prefill. - Fix hint: add `prefill?: { email; name }` to the `ContactForm` route, and a sender-tap sheet in `EmailThreadScreen` with "Add to contacts" / "Open contact" (lookup by email in `useContactsStore`). -- [ ] **Sender-name tap → contact popup missing** — `P3` — `missing` +- [x] **Sender-name tap → contact popup missing** — `P3` — `missing` — done in d2ed27f (tapping a sender or recipient opens `AddressActionSheet`, which shows the matching contact and "Open contact") - deferred: viewer agent (same sheet as above; `findContactByEmail` + `ContactDetail { contactId }` are ready). - What WEB does: clicking the sender name opens the contact popup with details/actions (changelog 1.4.x "Show contact popup when clicking the sender name", `email-viewer.tsx:392-455`). - What RN does: none (see previous item). @@ -294,11 +294,11 @@ RN covers the visible surface reasonably well (list with alphabetical index, det ## Verified at parity (brief list, so the fixer knows what NOT to redo) - Address book list/create/rename/delete with `myRights` checks (`src/components/settings/ContactsSettings.tsx`, `AddressBookPickerSheet`); inline "New address book…" in the move sheet (WEB #415). - Move contacts between books (single from detail, bulk from list) via `addressBookIds` patch (`contacts-store.ts:237-241`) — RN equivalent of WEB drag-and-drop. -- Group by first letter with sticky headers + settings toggle (`ContactsScreen.tsx:158-162`, `ContactsSettings.tsx:132-137`); sort by display name. +- Group by first letter with sticky headers + settings toggle (`ContactsScreen.tsx:158-162`, `ContactsSettings.tsx:132-137`); sort by display name, or by last name since 1827748 (#963); the sort no longer re-runs on every search keystroke since e6be892. - Search over name/emails/phones/orgs (RN also matches keywords). - Detail screen renders every WEB field block: emails/phones with contexts+features+labels, addresses (components, flat, timezone), org/units/titles/roles, anniversaries with age, gender/pronouns, languages, personalInfo, online services, calendar URIs, crypto keys, categories, relatedTo, notes, address book names, created/updated; quick actions call/SMS/mail/share; copy via long-press. - Contact form: prefix/given/middle/surname/suffix, nickname, display name (`name.full`), multiple emails/phones with context + phone feature pills, addresses as RFC 9553 components, multiple orgs (org/department/title/role), anniversaries with native date picker, online services, personalInfo w/ level, gender+pronouns, calendar URIs, categories (chips + suggestions from existing keywords), multiple notes, photo pick/remove, address book pick, dirty-state discard confirm, email regex validation, `media: {}` when photo removed (equivalent of WEB `media: null`). -- `name.full` display fallback (#179), `getContactPrimaryEmail` with `pref`. +- `name.full` display fallback (#179), `getContactPrimaryEmail` with `pref`. `name.full` (the vCard FN) is written on every form, "add sender" and import save since a734d0a (#430). - Group detail: member list resolved via id/uid/`urn:uuid:` (`contacts-store.ts:401-415`), add members via `ContactPickerSheet`, remove member, delete group, email all. - Tags (keywords) = WEB categories: drawer section with counts, filter by tag, bulk assign via `TagAssignSheet`, per-contact edit in form. - vCard import sheet: file picker, 5 MB cap, parse, duplicate detection by email (`detectDuplicates` identical), select all/none, target book, result count; export all (settings) and single (detail) with share sheet; multi-vCard files; QUOTED-PRINTABLE + charset decode (RN has a Hermes-safe `decodeBytes`), RFC 6868 params, KIND/MEMBER, PHOTO/LOGO/SOUND, RFC 6474/6715/8605/9554 properties. diff --git a/docs/parity/07-filters-vacation-files.md b/docs/parity/07-filters-vacation-files.md index 4d103689..22de4820 100644 --- a/docs/parity/07-filters-vacation-files.md +++ b/docs/parity/07-filters-vacation-files.md @@ -32,7 +32,7 @@ Filters: RN carries a byte-for-byte port of WEB's Sieve parser/generator/tests a - What RN does: the `filtersExpandedView` toggle exists (`src/components/settings/FilterSettings.tsx:350-355`) but only lifts the `numberOfLines` clamp on the same one-line summary (`:268`, `:292`); `if`/`then`/`match_all_conditions` locale keys are unused. - Fix hint: add a `VisualRuleSummary` component rendering rows of chips (all conditions, all actions) when `expandedView` is true. -- [ ] **Per-account (shared/group) filters not supported** — `P3` — `missing` (depends on whether RN gets managed shared-account settings at all) — deferred: RN has no managed/shared-account settings yet +- [x] **Per-account (shared/group) filters not supported** — `P3` — `missing` (depends on whether RN gets managed shared-account settings at all) — done in a86cd1d (tap a shared account in Account settings to manage its filters; the rule editor offers that account's folders) - What WEB does: `filter-store` tracks `availableAccounts`/`selectedAccountId` and every Sieve call takes an `accountId` (`stores/filter-store.ts:62-134`); `FilterSettings` scopes to `managedAccountId` and fetches that account's mailboxes for the move target list (`components/settings/filter-settings.tsx:188-232`, `248-255`); client `getSieveAccounts()` (`lib/jmap/client.ts:4465-4469`). - What RN does: `src/api/sieve.ts:20-23` always uses the primary Sieve account; store has no account selection (`src/stores/filter-store.ts:32`). No `managedAccount` concept exists in RN (`grep -ri managedAccount src` → none). - Fix hint: only if the Accounts area adds shared-account management: thread an optional `accountId` through `api/sieve.ts` and the store's `fetchFilters/saveFilters`. @@ -69,7 +69,7 @@ Filters: RN carries a byte-for-byte port of WEB's Sieve parser/generator/tests a - What RN does: `VacationSettings.tsx` never calls `useLocaleStore`; "Vacation Responder", "Date Range", "Saved", warnings etc. are literals (`:57-59`, `:72-74`, `:88`, `:101-158`). RN `locales/en/common.json` already contains `settings.vacation.{title,description,status,date_range,message,preview,save,saving,warnings}`. - Fix hint: wire `t('settings.vacation.…')` like `FilterSettings.tsx` does. -- [ ] **Vacation fetch/save on a shared account** — `P3` — `missing` (same dependency as the filters item) — deferred: same dependency (no managed-account concept in RN) +- [x] **Vacation fetch/save on a shared account** — `P3` — `missing` (same dependency as the filters item) — done in a86cd1d - What WEB does: `fetchVacationResponse(client, managedAccountId)` (`components/settings/vacation-settings.tsx:52-56`, `121-128`). - What RN does: always `jmapClient.accountId` (`src/api/vacation.ts:27`, `:43`). @@ -110,7 +110,7 @@ Filters: RN carries a byte-for-byte port of WEB's Sieve parser/generator/tests a - What RN does: `getDocumentAsync({ multiple: false })` (`src/screens/FilesScreen.tsx:357-360`); uploading the same name twice creates two nodes with the same name. - Fix hint: `multiple: true` + sequential upload loop; reuse WEB's `getUniqueName` against `visibleFiles`. -- [ ] **No in-app preview (image/text/markdown/PDF/audio/video/eml)** — `P2` — `missing` — deferred: the OS viewer/share sheet stays the mobile answer for now; no inline viewer ported +- [x] **No in-app preview (image/text/markdown/PDF/audio/video/eml)** — `P2` — `missing` — done in 409cdb1 (images, text and source files, `.eml`, and PDFs on iOS open in the reader's preview; PDFs on Android, audio, video, HTML and Markdown still go to another app) - What WEB does: `ImagePreviewModal` and `FilePreviewModal` (text, markdown, PDF incl. mobile pdf.js viewer, audio, video, EML) chosen via `getFilePreviewKind` (`lib/file-preview.ts:29-68`; `components/files/file-preview-modal.tsx`, `image-preview-modal.tsx`, `pdf-mobile-viewer.tsx`; `files-app.tsx:487-495`, `690-708`). - What RN does: tapping a file calls `shareAttachment` which downloads to cache and hands off to the OS share/viewer sheet (`src/screens/FilesScreen.tsx:248-258`; `src/lib/email-export.ts` "preview" variant). Works but there is no inline viewer and no "recent files" tracking. - Fix hint: reuse whatever attachment viewer the mail area has (image modal / WebView for text+PDF); N/A if the team decides the OS viewer is the mobile answer - then close this item. diff --git a/docs/parity/08-settings-push-i18n-ui.md b/docs/parity/08-settings-push-i18n-ui.md index 137b4409..6e4c553f 100644 --- a/docs/parity/08-settings-push-i18n-ui.md +++ b/docs/parity/08-settings-push-i18n-ui.md @@ -99,7 +99,7 @@ RN toggles that are stored but never read (fix: either wire them or remove the c - What RN does: 25, true, true, true, false, false, right=read/left=archive, 4 English keywords (`RN: src/stores/settings-store.ts:232-325`). - Fix hint: align defaults where the behaviour is the same on both clients (attachment keywords, includeGroupInUnified, autoSelectReplyIdentity, showBirthdayCalendar) before any sync exists, otherwise the first sync flips them for existing users. -- [ ] **`trustedSendersAddressBook` can never be turned on in RN** — `P3` — `partial` +- [x] **`trustedSendersAddressBook` can never be turned on in RN** — `P3` — `partial` — done in 78114b6 (toggle in Content & Senders); since d89e331 it stays off until the user turns it on, where the webmail turns it on by itself - What WEB does: `null` resolves to `true` on first connect when the server has contacts; the Content & Senders tab has a toggle (`components/settings/content-senders-settings.tsx`, `updateSetting('trustedSendersAddressBook'`). - What RN does: default `false` (`RN: src/stores/settings-store.ts:239`), read by `EmailBodyView.tsx:470,489` but `ContentSendersSettings.tsx` has no control for it. - Fix hint: add the toggle; treat undefined as auto-on when `useHasContacts()`. @@ -176,7 +176,7 @@ RN toggles that are stored but never read (fix: either wire them or remove the c - What RN does: `getNative()` returns `null` off Android and setup throws "Push notifications require Android" (`RN: src/lib/push-notifications.ts:125-128,303-304`); the relay has only FCM and Web Push transports (`repos/relay/README.md` endpoints). - Fix hint: needs an APNs transport in the relay (`POST /api/push/register/apns` storing a device token, HTTP/2 to `api.push.apple.com` with a `.p8` key) plus an iOS native module (or `expo-notifications`) for the token and a Notification Service Extension to fetch sender/subject over JMAP. Content-blind payload stays the same. -- [ ] **No non-FCM transport (UnifiedPush, native #44/#48)** — `P3` — `missing` — deferred: relay must accept key-less web registrations + a UnifiedPush connector module +- [x] **No non-FCM transport (UnifiedPush, native #44/#48)** — `P3` — `missing` — done in ee04e95 (Android: UnifiedPush connector through a distributor app such as ntfy; the relay falls back to unencrypted delivery for distributors without Web Push keys); not in a release yet - What WEB does: N/A (browser push). - What RN does: FCM only (`BulwarkMessagingService.kt`, `push-notifications.ts:120-128`). - Fix hint: UnifiedPush distributors expose an RFC 8030 endpoint, which is exactly what the relay's `/api/push/register/web` already accepts (`repos/relay/src/server.ts` handleRegisterWeb); VAPID/`keys` are optional in UnifiedPush so the relay must accept a record without `p256dh`/`auth` and send unencrypted (or only the id). Client side: `unifiedpush-react-native`/`org.unifiedpush.android.connector` receiving the message and calling the same headless task. @@ -250,7 +250,7 @@ RN toggles that are stored but never read (fix: either wire them or remove the c - What RN does: `RN: app.config.js:33` sets `userInterfaceStyle: 'dark'`, which prebuild writes as `UIUserInterfaceStyle = Dark` in Info.plist, so `useColorScheme()` (`src/theme/colors.ts:16`, `App.tsx:220`) always returns `dark` and "System" behaves like "Dark". On Android without `expo-system-ui` (not in `package.json`) the key is ignored with a prebuild warning, so Android is probably fine. Splash background is also dark-only (`:38`). - Fix hint: set `userInterfaceStyle: 'automatic'` and give the splash a light variant (`splash.dark`). -- [x] **Themes tab is a stub with fake themes; `activeThemeId` never applied** — fixed in fe45a44 — `P3` — `missing` (record as N/A for now) — Qui/Nord/Catppuccin/Solarized token sets generated from lib/builtin-themes.ts (src/theme/builtin-themes.ts); useColors() applies activeThemeId; upload button removed +- [x] **Themes tab is a stub with fake themes; `activeThemeId` never applied** — fixed in fe45a44 — `P3` — `missing` (record as N/A for now) — Qui/Nord/Catppuccin/Solarized token sets generated from lib/builtin-themes.ts (src/theme/builtin-themes.ts); useColors() applies activeThemeId; upload button removed; Roundcube Elastic and Aurora Glass added in 7964e64, "Flat fields" not ported - What WEB does: 6 built-in themes (`lib/builtin-themes.ts:879-936`), zip upload, marketplace, admin forced/default theme, compiled CSS tokens, PWA theme-color meta (`stores/theme-store.ts`). - What RN does: `ThemesSettings.tsx:18-22` lists `Default/Qui/Sepia` (Qui exists in WEB; Sepia does not), stores `activeThemeId` that nothing reads, "Upload .zip" has no handler (`:81-83`); the tab is marked Experimental. - Fix hint: minimal viable subset = map WEB's built-in theme token sets (`builtin-themes.ts` light/dark `colors`) onto `ThemePalette` and let `useColors()` pick `activeThemeId`; remove the upload button. Otherwise hide the tab. diff --git a/docs/parity/09-jmap-core-sync-security.md b/docs/parity/09-jmap-core-sync-security.md index e6b6c407..0adea2cf 100644 --- a/docs/parity/09-jmap-core-sync-security.md +++ b/docs/parity/09-jmap-core-sync-security.md @@ -52,14 +52,14 @@ RN's JMAP client (`src/api/jmap-client.ts`, 615 lines) is a thin transport: sess - What RN does: `rewriteSessionUrls` only rewrites when `extractOrigin(url)` matches `^https?://` (`src/api/jmap-client.ts:320-340`); a relative `apiUrl` is returned unchanged and `fetch('/jmap/')` then fails with "Network request failed". Same gap in `src/api/unified-inbox.ts:55-60`. (RN correctly avoids `new URL()` so templates are not corrupted.) - Fix hint: in `rewrite()`, when `extractOrigin` is null and the string starts with `/`, return `serverOrigin + url`. -- [x] **Capability check uses the broad `principals` URN, not `principals:owner` (44360024)** — `P2` — `bugfix-parity` — fixed in a0ce925 +- [x] **Capability check uses the broad `principals` URN, not `principals:owner` (44360024)** — `P2` — `bugfix-parity` — fixed in a0ce925; since Stalwart advertises `principals:owner` nowhere, that also hid sharing, so since caea3d0 the sharing UI is gated on `principals` and `principals:owner` only goes into `using` when advertised - What WEB does: declares `urn:ietf:params:jmap:principals:owner` in `using` only when that exact capability is advertised in session or account capabilities (`lib/jmap/client.ts:4689-4698`, `6472`; changelog "Check the specific capability a request declares (principals:owner), not a broader one"). - What RN does: `supportsSharing()` checks `CAPABILITIES.PRINCIPALS` (`src/api/files.ts:23-25`) and then pushes `PRINCIPALS_OWNER` into `using` (`files.ts:40-43`). A server advertising `principals` but not `principals:owner` rejects every FileNode request with `unknownCapability`. - Fix hint: check `hasCapability(CAPABILITIES.PRINCIPALS_OWNER) || accountCapabilities[...]` before adding it. - [x] **PatchObject pointer keys not JSON-Pointer escaped (5c484af1)** — `P3` — `bugfix-parity` — fixed in ccbe67c - What WEB does: `keywordPointer()`/`pointerToken()` escape `~` and `/` (`lib/jmap/patch-pointer.ts:39-55`); mailbox moves use a whole-map replacement (`lib/jmap/client.ts:66-74`). - - What RN does: `moveEmail`/`moveEmails` build `mailboxIds/${id}` pointers unescaped (`src/api/email.ts:456-457`, `473-474`). Stalwart ids never contain `/`, so exposure is limited to non-Stalwart servers; keywords are always sent as a whole map (`email.ts:436-445`), so tag names with `/` are safe. + - What RN does: `moveEmail`/`moveEmails` build `mailboxIds/${id}` pointers unescaped (`src/api/email.ts:456-457`, `473-474`). Stalwart ids never contain `/`, so exposure is limited to non-Stalwart servers; keywords are always sent as a whole map (`email.ts:436-445`), so tag names with `/` are safe. (Since 5041897 keywords are sent as `keywords/` pointers too, escaped by `keywordPointer` in `src/api/patch-pointer.ts`.) - Fix hint: add `pointerToken()` or switch `moveEmail(s)` to the full `mailboxIds` replacement RN already uses in `restoreEmailMailboxes`. - [x] **Send: message created directly in Sent instead of Drafts + `onSuccessUpdateEmail` (#188)** — `P2` — `bugfix-parity` — fixed in ccbe67c @@ -181,7 +181,7 @@ RN's JMAP client (`src/api/jmap-client.ts`, 615 lines) is a thin transport: sess - What RN does: `android:allowBackup="true"` with `@xml/secure_store_backup_rules` from the expo-secure-store plugin, which only excludes SecureStore's own prefs (`android/app/src/main/AndroidManifest.xml:16`); the AsyncStorage SQLite DB (full cached bodies `webmail:offline-cache:entry:v2:*`, outbox, settings, account registry) is backed up to the user's Google account. - Fix hint: add an app-owned `fullBackupContent`/`dataExtractionRules` excluding `databases/RKStorage` (AsyncStorage) or set `allowBackup=false` via an Expo config plugin. -- [ ] **Send has no offline path and no draft fallback** — `P2` — `missing` +- [ ] **Send has no offline path and no draft fallback** — `P2` — `missing` — partly: drafts autosave to the server since 5b72c4d, and since 92b42e4 the close dialog no longer drops the text when the draft can't be saved; an offline send is still not queued, and a draft can't be saved offline - What WEB does: no offline queue either, but send failures leave the composer open with the draft autosaved (composer agent's area); send itself has a deadline (#702). - What RN does: `ComposeScreen` shows `Alert('Send failed')` and keeps the modal (`src/screens/ComposeScreen.tsx:975-979`); there is no draft API in `src/api/email.ts` (`grep -n '[Dd]raft' src/api/email.ts` only finds the send create id), so the body is lost if the user leaves, and an offline send is not queued although the outbox exists. - Fix hint: either add an `outbox` `send` op kind (blob uploads must already be done) or at minimum persist the composer state locally on failure. @@ -298,11 +298,11 @@ RN's JMAP client (`src/api/jmap-client.ts`, 615 lines) is a thin transport: sess - `Email/queryChanges` + `Email/changes` incremental list refresh with snapshot guard (issue #10) and full re-query fallback (`src/stores/email-store.ts:770-980`). - `null` for patch removal, `false` stripped from `mailboxIds` maps (`src/api/email.ts:456`, `551-556`). - FilterOperator/FilterCondition kept separate in query filters (`src/api/email.ts:225-237`). -- `getSharedMailboxes` chunks to `maxCallsInRequest` (`src/api/email.ts:84-105`); `getEvents`/`getContacts`/`fetchEmailsChunked`/offline sync chunk to `maxObjectsInGet`. -- Send: `/error`, `notCreated` checked; HOLDFOR envelope for scheduled send; FUTURERELEASE capability check (`src/api/email.ts:836-873`, `src/api/jmap-client.ts:505-531`). -- Push subscription create/verify/update/destroy with `using: [core]` and 90-day expiry refresh (`src/api/push.ts:9-114`, `src/lib/push-notifications.ts`). +- `getSharedMailboxes` chunks to `maxCallsInRequest` (`src/api/email.ts:84-105`); `getEvents`/`getContacts`/offline sync chunk to `maxObjectsInGet`, and list pages are capped at it since `queryEmailPage` replaced `fetchEmailsChunked` (d987930). +- Send: `/error`, `notCreated` checked; HOLDFOR envelope for scheduled send; FUTURERELEASE capability check (`src/api/email.ts:836-873`, `src/api/jmap-client.ts:505-531`). Two gaps found later: the capability was read from the session instead of the account until e28e8b4 (#57, audit B8), and a method error after a created submission (Stalwart's failed filing into Sent) was reported as a failed send until c51a848 (audit B23). +- Push subscription create/verify/update/destroy with `using: [core]` and 90-day expiry refresh (`src/api/push.ts:9-114`, `src/lib/push-notifications.ts`); calls that read or write `emailPush` also name `urn:ietf:params:jmap:emailpush` since ccaa24e, and every call checks the response since ddfe85a. - Outbox: idempotent full-state ops, per-account buckets, destroy-wins coalescing, oldest-first replay, account-switch guards (`src/stores/outbox-store.ts`). -- Offline cache: per-account buckets, index + entry keys, eviction by `receivedAt`, optimistic `patch`, abort on account switch (`src/stores/offline-cache-store.ts`); `getEmailDetail` cache fallback. +- Offline cache: per-account buckets, index + entry keys, eviction by `receivedAt`, optimistic `patch`, abort on account switch (`src/stores/offline-cache-store.ts`); the offline copy is shown before the network since 33e303e (`src/lib/email-detail-cache`, which replaced `getEmailDetail`). - Credentials in `expo-secure-store` (never AsyncStorage); SecureStore key sanitising; per-account keys; legacy migration. - Client-cert module uses the platform default `X509TrustManager` (no trust-all) (`BulwarkClientCertModule.kt:216-222`). - WebView hardening: `originWhitelist=['about:blank']`, no file/universal access, `mixedContentMode="never"`, no multiple windows, no cookies/DOM storage, `incognito`, external links limited to `https?|mailto|tel|sms` (`src/components/EmailBodyView.tsx:675-724`).