mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-04 16:06:41 +00:00
1068 lines
64 KiB
Markdown
1068 lines
64 KiB
Markdown
# Native app audit, 2026-09-22
|
||
|
||
Bugs, performance and webmail gaps in the React Native app, as a TODO list. Every item carries
|
||
severity, effort, a `file:line` in this repo (paths relative to `repos/react-native`), the webmail
|
||
file to match where there is one (paths relative to the webmail repo root), a repro or measurement,
|
||
and whether it was **verified** (reproduced on a device, on the wire or by measurement) or is
|
||
**suspected** (code reading only).
|
||
|
||
Severity: P0 crash / data loss / security, P1 broken feature, P2 degraded, P3 polish.
|
||
Effort: S (hours), M (a day or two), L (a week or more).
|
||
|
||
## Scope and method
|
||
|
||
- **Code:** local `main` at `6c82130` (0.1.62) plus the uncommitted UnifiedPush work in the tree
|
||
(push files only). GitHub `main` is 2 commits ahead (`c2584c2`, `5b8c435`, PR #53: tappable
|
||
links in event descriptions); nothing else differs. Webmail reference: `main` at `9ef7068b`.
|
||
- **Server:** a throwaway Stalwart **0.16.23** in local Docker (`stw-native-audit`, stopped, kept).
|
||
The 1blu test host (`stw-test19`) was unusable: its Docker daemon was stopped and disabled on
|
||
2026-09-19. Accounts: `usera`, `userb`, and a `team` group that `usera` belongs to.
|
||
- Seed: Inbox 653, Archive 300, Sent 162, Projects/Alpha 40, Projects/Beta 25, Receipts 30,
|
||
Junk 15; team Inbox 80.
|
||
- Message types: threads, 80 heavy HTML newsletters (half HTML-only), attachments, an iMIP
|
||
invite, an embedded message, an inline cid image, RTL mail, and a read-receipt request.
|
||
- Also 27 events (one weekly recurring), 150 contacts (two with photos), and 515 file nodes
|
||
(folders created after the 500th).
|
||
- **Device:** Android emulator `Bulwark_Pixel7_API35` (x86_64, WHPX) running a release APK built
|
||
from the tree with a temporary cleartext flag (reverted afterwards).
|
||
- Driven with `uiautomator` and `input`.
|
||
- Every request went through a logging proxy that recorded JMAP methods, arguments and
|
||
set-errors.
|
||
- UI timings come from `screenrecord` frames diffed with ffmpeg. They are emulator numbers:
|
||
compare them with each other, not with a phone.
|
||
- **Static review:** covered selectors, rendering, request patterns, bundle and build, and the
|
||
webmail diff since 2026-08-29 (142 non-merge commits). Every item below that says **verified**
|
||
was re-checked on the device, on the wire, or by direct measurement (APK and bundle inspection).
|
||
|
||
### Test suite status
|
||
|
||
- `npx tsc --noEmit`: clean on the tree and on a clean `git archive` of HEAD.
|
||
- `npx vitest run`: green. 77 files and 970 tests pass on the tree; HEAD passes too, apart from
|
||
the load-related timeout below.
|
||
- The note "red on main, netinfo not mocked" is stale. `b84d4d8` (2026-08-29) mocks netinfo
|
||
in `src/test-setup.ts`.
|
||
- The only failure seen: `src/i18n/__tests__/keys.test.ts:45` timed out once, taking 16.8 s
|
||
against the 5 s default while gradle was building. Run alone it takes 0.4 s. It walks `src/`
|
||
synchronously, so give it a longer timeout.
|
||
- Live suite (`src/api/__tests__/live-stalwart.test.ts`): 14/14 pass against 0.16.23. All three
|
||
Stalwart quirks still exist in 0.16.23 and are handled on the paths the suite covers:
|
||
- the third `Email/set` response on a send;
|
||
- the same `partId` in `textBody` and `htmlBody`;
|
||
- `urn:stalwart:jmap` appearing only in `accountCapabilities`.
|
||
- The zustand selector sweep found no crash-class selectors: 685 call sites, none returns a
|
||
fresh object or array.
|
||
- Open issue #50 (crash opening any mail) did not reproduce in about 20 opens, including mail
|
||
with attachments, an invitation, a 586 KB body and RTL mail.
|
||
|
||
## Summary: fix these ten first
|
||
|
||
1. [x] **P0 · S:** read receipts carry attacker-controlled headers and are sent for messages the user
|
||
never opened ([S1](#s1), [B7](#b7)). Fixed in `cb6ddd6`, `5f3f584`.
|
||
2. [x] **P0 · S–M:** published release APKs are signed with the public React Native debug key
|
||
([S2](#s2)). Fixed in `6f0abb4`, `746572c`; the release key and secrets still have to be set up
|
||
before the next release.
|
||
3. [x] **P0 · S–M:** viewer actions on a shared/group message hit the user's own account and can
|
||
delete an unrelated message ([B3](#b3)). Fixed in `5c7301b`.
|
||
4. [x] **P0 · S:** opening, starring or tagging replaces the whole keyword map; stars and tags are
|
||
erased ([B4](#b4)). Fixed in `5041897`.
|
||
5. [x] **P1 · S:** the viewer switches to another message when new mail arrives, and its buttons act
|
||
on the message you are not looking at ([B5](#b5)). Fixed in `9c31de2`.
|
||
6. [x] **P1 · S–M:** messages disappear from the list after a background refresh until you pull to
|
||
refresh ([B6](#b6)). Fixed in `0a8686a`.
|
||
7. [x] **P1 · S:** scheduled send, the undo-send delay and the Scheduled folder are dead on every
|
||
Stalwart server (#57, [B8](#b8)). Fixed in `e28e8b4`.
|
||
8. [x] **P1 · S–M:** sharing a file into the app always fails, and `SENDTO`/`+`-address `mailto:` links
|
||
open nothing ([B9](#b9), [B10](#b10), [B11](#b11)). Fixed in `cd4ce9d`, `06e8942`, `ee652d3` (not
|
||
yet checked on a device).
|
||
9. [x] **P1 · M:** opening a message takes 1.7–1.9 s and the layout jumps three times ([PF1](#pf1)).
|
||
Fixed in `33e303e`, `ac8e3fb`, `1348e7d`, `01fc6fb`, `6ebadf7`, `54b169c` (not re-timed yet).
|
||
10. [x] **P1 · M:** cold start takes 3.6–4.1 s to a settled inbox and is JS-bound ([PF2](#pf2)).
|
||
Fixed in `604eb39`, `063432b`, `5bcfe1f`, `82ca359`, `b9db376` (not re-timed yet).
|
||
|
||
**Why these ten:**
|
||
- **1–2:** security holes that can be exploited today without any user mistake.
|
||
- **3–6:** the app silently changes, hides or acts on the wrong mail. When users call the app
|
||
"buggy", this is the class that loses their trust, and all four were reproduced in minutes.
|
||
- **7–8:** core flows that fail for every Stalwart user or every share/`mailto:` user.
|
||
- **9–10:** the latency users feel on every launch and every tap, the "slow" in "buggy and slow".
|
||
|
||
Everything else below is narrower (file trees past 500 nodes; tasks, which are off by default),
|
||
suspected, or cosmetic.
|
||
|
||
## Fix pass, 2026-09-24
|
||
|
||
One orchestrating session and one fix agent per area, each in its own git worktree; their
|
||
commits were cherry-picked onto `main` (`ee04e95..main`, about 200 commits). `npx tsc --noEmit`
|
||
and `npx vitest run` passed after every merge (2,028 tests at `e5659ed`). A ticked box means the
|
||
fix landed with unit tests; the startup numbers in PF2 come from `npx expo export`, not device
|
||
timings. A release build (x86_64, R8, `-PallowDebugSignedRelease=true`) was built and
|
||
smoke-tested on the `Bulwark_Pixel7_API35` emulator up to the sign-in screen: every Kotlin, XML,
|
||
Gradle, Babel and Metro change compiles, cold start in light and dark mode is clean, the share
|
||
and `mailto:` intents are accepted cold and warm, the QR scanner opens, and `versionCode` is
|
||
10062. No test account was available, so nothing behind sign-in has run on a device.
|
||
|
||
The device run also found two push bugs that predate this pass, now fixed: a downgraded FCM
|
||
message or a UnifiedPush message crashed the app on Android 12+ with
|
||
`BackgroundServiceStartNotAllowedException` (`fef23e4`, falls back to a JobScheduler job), and
|
||
every push held a wake lock for 30 s because the new architecture never registered the
|
||
headless-task "finished" module (`5ddc2df`).
|
||
|
||
Result: of the 93 bug, UI, performance and webmail-gap items, 83 are ticked, 7 are fixed in part
|
||
and 3 are open (B2, Tabler icons, the password hand-off). 13 of the 14 stale parity entries are
|
||
resolved; `dateLocale` is still missing.
|
||
|
||
### Check on a device or server
|
||
|
||
- **Android intents (B9–B11):** they compile and the app accepts them, but the composer only
|
||
opens after sign-in. Signed in, check `SEND` with a `content://` image, `SENDTO mailto:` with
|
||
extras on a cold and a warm start, and `VIEW mailto:alice+news@partner.example`.
|
||
- **R8 (`5e3cbf2`):** the signed-out screens run clean under R8. Push registration (FCM and
|
||
UnifiedPush encryption), client certificates and the signed-in screens have not run under R8
|
||
yet. Releases now carry the R8 mapping for decoding stack traces (`e5659ed`).
|
||
- **Push fallback (`fef23e4`):** tested with simulated FCM and UnifiedPush deliveries; confirm with
|
||
a real downgraded FCM message and a real distributor (e.g. ntfy).
|
||
- **First build:** `063432b` adds `babel.config.js` and `metro.config.js`, so start Metro once with
|
||
`npx expo start --clear`. The AsyncStorage limit from `24849ba` only applies after a native
|
||
rebuild.
|
||
- **Timings:** record PF1, PF2, PF3, U1 and U2 again with `screenrecord`; none were re-measured.
|
||
- **Push:** the `forbidden` fallback with a mailbox shared by ACL (`d8046a8`), revoke detection
|
||
(`a3d3af7`), a group-mailbox notification tap (`8618abf`), and that a replacement subscription
|
||
fits under `maxPushSubscriptions`, since the old one is kept until the new one is verified.
|
||
- **Echo skip (`018accd`):** it assumes `Mailbox.totalEmails` matches the `Email/query` total on
|
||
Stalwart; confirm on the wire.
|
||
- **Scheduled send on Stalwart:** the 7-day hold clamp (`4e7ae2f`), the create-then-cancel
|
||
reschedule (`7e598c5`) and scheduled mail held in shared accounts (`72f48db`).
|
||
- **Server-side recurrence (`8454889`, `f397ee3`, `96c5a3f`):** needs Stalwart 0.16.20 or later;
|
||
check the synthetic-id probe, a single-occurrence edit and an RSVP to one occurrence.
|
||
- **Calendar time zone (`a8e6f6e`, `8af163a`):** open and save an event and a task with the
|
||
calendar zone set away from the device zone.
|
||
- **Sieve round trip:** save filters on the phone while the auto-reply is on; it should stay on,
|
||
and the webmail should read the script back unchanged (`d53bfec`, `3f88868`, `6c31c8d`).
|
||
- **Sign-in:** add a second OAuth account (`ab84578`), and restore a session behind a redirecting
|
||
proxy (`859770b`).
|
||
- **Composer WebView:** the editor CSP (`235f4ea`) and the 400 ms change throttle (`fc00781`).
|
||
- **iOS:** the app-icon badge (`25c0883`) and the in-app PDF preview (`409cdb1`).
|
||
- **Translations (`67935d2`):** 26 languages written without a native speaker; wording with no
|
||
precedent in the webmail catalog needs a review.
|
||
|
||
### Decisions for the maintainer
|
||
|
||
- **Release signing, before the next version bump:** create the release key, the rotation lineage
|
||
(`apksigner rotate`) and the five repository secrets (`docs/android-release.md`). The release
|
||
workflow now fails without them, and a local release build needs
|
||
`-PallowDebugSignedRelease=true`. `versionCode` is derived from `VERSION` (0.1.62 → 10062).
|
||
- **Trusted-sender sync:** the webmail turns "Sync trusted senders to address book" on at login;
|
||
the app now leaves it off until the user turns it on (`d89e331`). Follow the webmail, or keep
|
||
it opt-in?
|
||
- **Sender favicons:** still on by default, and the phone fetches them from icons.duckduckgo.com
|
||
itself, having no proxy like the webmail's `/api/favicon`. Default off, a proxy, or keep?
|
||
- **Design language:** "Flat fields" and the Tabler icons are not ported (open question below).
|
||
- **Short messages:** the quick reply now sits at the bottom of the page so it never jumps
|
||
(`54b169c`), which leaves empty space under a short message.
|
||
- **Android app badge:** the icon badge is iOS-only (`25c0883`). On Android, expo-notifications
|
||
clears a badge with `cancelAll()`, which would also dismiss mail notifications and reminders,
|
||
and launchers already badge from the notifications on display.
|
||
- **Password hand-off:** taking the password out of the `bulwarkmobile://…#password=` fragment
|
||
needs a one-time code exchange on the webmail (`/api/auth/mobile/handoff` plus a redeem call,
|
||
PKCE-style).
|
||
- **Composer font size:** the webmail's font-size picker was left out; it needs a
|
||
selection-rewriting command in the editor page that can only be tested on a device.
|
||
- The other open questions at the end (tasks default, office documents, barcode scanner, package
|
||
name, settings sync, feature requests) are unchanged.
|
||
|
||
### Follow-ups worth doing
|
||
|
||
- Offline cache: a cached message over about 2 MB can't be read back on Android; keep bodies in
|
||
files.
|
||
- Offline cache: eviction only counts the active account.
|
||
- Session restore: a store that hydrates after the 5 s timeout merges over the restored state.
|
||
- Calendar subscriptions and search history don't use `createPersistStorage` yet.
|
||
- Bodies cached truncated before `4767805` are never fetched again.
|
||
- Push: `expires` is patched on every launch, because Stalwart's 7-day cap equals the refresh
|
||
threshold; check revoke detection and expiry for users who open the app less than weekly.
|
||
- Push: the onboarding prompt comes back after push was turned off.
|
||
- Webmail: `lib/web-push.ts` has the same shared-account `emailPush` bug as B18.
|
||
- Mail list: the swipe drag still runs on the JS thread (needs gesture-handler or reanimated).
|
||
- Unified Inbox: it still has the inline `renderItem` pattern that `ebd8dac` removed from the
|
||
folder list, shows no attachment chips, and its `patchRows` duplicates the keyword-patch helper.
|
||
- Mail: the webmail's opt-in "All unread" and "All starred" views are missing.
|
||
- Composer: drafts can't be saved offline (B16 now says so instead of dropping the text).
|
||
- Composer: `findReceivingIdentity` scans identities in list order; no "remove text colour".
|
||
- Startup: the date-fns main barrel still adds about 300 modules; the 4.3 MB of catalogs stay in
|
||
the bundle, only no longer evaluated at startup.
|
||
- Startup: screens other than the mail list take their top inset from the native `SafeAreaView`
|
||
and jump; the window colour follows the system theme, not the app's.
|
||
- Intents: a `mailto:` without a recipient opens nothing; Bcc can't be passed through
|
||
`bulwarkmobile://compose`.
|
||
- Files: no refresh after a partly failed delete; the webmail's share-rights conversion for
|
||
Stalwart before 0.16.6 (`c3225b3f`) is not ported.
|
||
- File preview: no PDF renderer on Android, no audio, video, HTML or Markdown preview, and text
|
||
is read as UTF-8 only.
|
||
- Filters: calendar and contacts settings aren't scoped when managing a shared account; no UI for
|
||
the forward limit (`maxNumberRedirects`) or the auto-reply length warning (webmail
|
||
`f3aec163`); `fileinto` lingers in `require`.
|
||
- Settings: the search doesn't index sidebar apps; theme names and descriptions aren't translated.
|
||
- i18n: some `toLocaleDateString()` calls (settings panes, Files, contact detail) format in the
|
||
device language, not the app's; `formatDelta` keeps its "d/h/min" units, like the webmail.
|
||
- Push: React Native's own `HeadlessJsTaskService` can still start a task before the JS engine is
|
||
ready when several high-priority pushes arrive during startup; that task then only ends at its
|
||
30 s timeout. The job fallback from `fef23e4` avoids this.
|
||
- Release: keep each release's `…-mapping.txt` (attached since `e5659ed`); decoding a user's
|
||
stack trace needs the matching one.
|
||
- Release workflow: the `workflow_dispatch` hint "leave empty to skip upload" doesn't match what
|
||
the workflow does (older than this pass).
|
||
- Upstream: GitHub `main` has `c2584c2` and `5b8c435` (PR #53, links in event descriptions),
|
||
which local `main` lacks.
|
||
|
||
## Bugs
|
||
|
||
### Crashes
|
||
|
||
- [x] <a id="b1"></a>**B1 · P1 · M: session restore can hang forever when a persisted cache fails
|
||
to load.** Fixed in `29169c3`, `e37621b`, `24849ba`.
|
||
- Where: `src/stores/auth-store.ts:23-31`, `src/stores/auth-store.ts:603-608`,
|
||
`src/stores/settings-store.ts:423`.
|
||
- Why:
|
||
- AsyncStorage's Android database is capped at 6 MB, because `android/gradle.properties`
|
||
has no `AsyncStorage_db_size_in_MB`. Rows over about 2 MB can't be read back.
|
||
- The offline cache defaults to 50 MB. The email snapshot has no cap and stores the current
|
||
folder twice.
|
||
- zustand only marks a store hydrated when the read succeeds, and `waitForHydration` has
|
||
no timeout.
|
||
- **Suspected.**
|
||
- [ ] <a id="b2"></a>**B2 · P3 · S: the composer's WebView renderer process dies when the composer
|
||
closes.**
|
||
- Evidence: logcat shows `aw_browser_terminator … Renderer process (11521) crash detected
|
||
(code -1)` at 18:42:55 and again (11663) at 18:45:42, both on composer close.
|
||
- The app survives, but the cause is unknown.
|
||
- **Verified** (log only).
|
||
- 2026-09-24: not reproduced; the composer is only reachable after sign-in and no test account
|
||
was available for the device run.
|
||
|
||
### Data correctness
|
||
|
||
- [x] <a id="b3"></a>**B3 · P0 · S–M: viewer actions use the open folder's account instead of the
|
||
message's.** Fixed in `5c7301b`, `d2066da`.
|
||
- Where: `src/screens/EmailThreadScreen.tsx:107-113` (owner falls back to the list folder),
|
||
`src/screens/EmailThreadScreen.tsx:355-358` (delete), `:388-414` (archive, move, spam),
|
||
`src/stores/email-store.ts:1194-1197`.
|
||
- Web: `stores/email-store.ts` (`resolveUnstampedEmailAccountId`, rows stamped with their account).
|
||
- Repro:
|
||
- From the Unified Inbox, open team message `keaaaacr` and tap Delete. There is no confirmation.
|
||
- `Email/set` goes to account `c` and moves usera's own, unrelated "Security audit #18" (same
|
||
id, since Stalwart ids are only unique per account) to Trash.
|
||
- The team message stays where it was.
|
||
- **Verified.**
|
||
- [x] <a id="b4"></a>**B4 · P0 · S: mark-read, star, pin and tag replace the whole `keywords`
|
||
map.** Fixed in `5041897`, `ddff2f7`.
|
||
- Where: `src/stores/email-store.ts:963-983`, `src/api/email.ts:602-609`. Outbox replay does
|
||
the same.
|
||
- Web: `lib/jmap/client.ts` (`keywords/$seen` pointer patch, `batchMarkAsRead`). RN already has
|
||
`patchKeywordsForEmails` at `src/api/email.ts:612`.
|
||
- Repro 1 (message not in the loaded list):
|
||
- Open a team message that has `$flagged` and `$label:work` from the Unified Inbox.
|
||
- The app sends `update {keywords: {"$seen": true}}`, and the server keeps only `$seen`. The
|
||
star and the tag are gone, while the viewer still shows the star.
|
||
- Repro 2 (message in the list): Star sent the full map `{$notjunk, $seen, $flagged}`. That
|
||
reverts any keyword another client changed since the list loaded.
|
||
- **Verified.**
|
||
- [x] <a id="b5"></a>**B5 · P1 · S: the viewer pager follows the live list, so new mail shifts the
|
||
page under the user.** Fixed in `9c31de2`, `e0b7288`.
|
||
- Where: `src/screens/EmailThreadScreen.tsx:81-102`, `:639`.
|
||
- Repro:
|
||
- Open "Quarterly numbers" (row 2) and deliver 2 new messages.
|
||
- The screen switches by itself to "Drift probe one".
|
||
- Tapping Star then flags "Quarterly numbers", which is no longer displayed. Delete and
|
||
Archive use the same `activeEmailId`.
|
||
- Fix: snapshot the id list when the viewer opens.
|
||
- **Verified.**
|
||
- [x] <a id="b6"></a>**B6 · P1 · S–M: messages vanish after a delta refresh.** Fixed in `0a8686a`.
|
||
- Where: `src/stores/email-store.ts:1953-1990` and `:2058-2060`.
|
||
- Web: `stores/email-store.ts` (`applyEmailDeltaNow`, which runs `Email/changes` against the
|
||
list's own state).
|
||
- Why:
|
||
- `inMailbox` is a mutable filter. Stalwart therefore reports every updated id as removed and
|
||
re-added.
|
||
- RN drops those ids and skips re-fetching them because they "already exist".
|
||
- The account-wide `emailState` token is deleted whenever a query returns no ids.
|
||
- Repro:
|
||
- Open the empty "Projects" folder, go back to Inbox, and flag "Please confirm receipt" from
|
||
another client.
|
||
- The refresh sends only `Email/queryChanges`, and the row disappears.
|
||
- A pull-to-refresh (a full query) brings it back.
|
||
- Before the empty-folder visit the same test worked (`queryChanges` → `changes` → `get`).
|
||
- **Verified.**
|
||
- [x] <a id="b7"></a>**B7 · P1 · S: with "Always send", read receipts go out for messages that were
|
||
never displayed, and a failed one leaves junk in Sent.** Fixed in `5f3f584`, `1fb60a5`.
|
||
- Where: `src/components/email/ReadReceiptBanner.tsx:92-101` (mounted in the neighbouring pages
|
||
the pager pre-renders, `src/screens/EmailThreadScreen.tsx:641` `windowSize={3}`),
|
||
`src/api/email.ts:1456-1486`.
|
||
- Repro:
|
||
- Open "Invitation: Design review". The app uploads, imports and submits an MDN for the next
|
||
message, "Please confirm receipt", which the user never saw.
|
||
- Stalwart rejected this submission (a `.example` address), but the imported "Read: Please
|
||
confirm receipt" stays in Sent. `$mdnsent` is not set and a failed auto-send clears its guard
|
||
(`autoRef.current = null`), so it can run again and import another copy.
|
||
- **Verified.**
|
||
- [x] <a id="b8"></a>**B8 · P1 · S: scheduled send and the undo-send delay are off on Stalwart
|
||
(#57).** Fixed in `e28e8b4`, `4e7ae2f`.
|
||
- Where: `src/api/jmap-client.ts:973-999` reads `session.capabilities[submission]`. Stalwart
|
||
only puts `maxDelayedSend` and FUTURERELEASE in `accountCapabilities`. `:919-925` (attachment
|
||
size cap) has the same problem.
|
||
- Web: `lib/jmap/client.ts` (reads `accountCapabilities`).
|
||
- Repro: the account advertises `maxDelayedSend: 2592000` with FUTURERELEASE. Composer clock
|
||
→ "In 1 hour" → "Scheduling unavailable: This mail server does not support scheduled send".
|
||
- The Scheduled screen and the undo delay use the same check.
|
||
- **Verified.**
|
||
- [x] <a id="b9"></a>**B9 · P1 · S: files shared into the app never upload.** Fixed in `cd4ce9d`,
|
||
`3c7b96e`.
|
||
- Where: `src/api/blob.ts:99` passes the `content://` URI to `createUploadTask`, which
|
||
requires a file path. `src/lib/share-intent.ts:40` names the file after the URI's last
|
||
segment.
|
||
- Repro: `am start -a SEND -t image/png --eu android.intent.extra.STREAM content://media/…/1000000027`
|
||
→ alert "Failed to upload 1000000027 … IOException: Directory for
|
||
'/external/images/media/1000000027' doesn't exist".
|
||
- **Verified.**
|
||
- [x] <a id="b10"></a>**B10 · P1 · S: `ACTION_SENDTO mailto:` opens the inbox, not the composer.**
|
||
Fixed in `06e8942`.
|
||
- Where: `android/app/src/main/java/com/anonymous/bulwarkmobile/ShareIntentStore.kt:25` only
|
||
handles SEND and SEND_MULTIPLE. RN's `Linking` forwards only VIEW. The manifest registers
|
||
SENDTO anyway.
|
||
- Repro: `am start -a android.intent.action.SENDTO -d mailto:bob@partner.example` → no
|
||
composer. The same URI with VIEW works.
|
||
- **Verified.**
|
||
- [x] <a id="b11"></a>**B11 · P2 · S: `mailto:` links with a `+` address do nothing.** Fixed in
|
||
`ee652d3`.
|
||
- Where: `src/lib/mailto.ts:21` turns `+` into a space, validation then fails, and the parser
|
||
returns null.
|
||
- Web: `lib/validation.ts`.
|
||
- Repro: `mailto:alice+news@partner.example` → no composer.
|
||
- **Verified.**
|
||
- [x] <a id="b12"></a>**B12 · P1 · S: tasks can't be completed (#958).** Fixed in `dcdda5e`,
|
||
`a7e4e84`.
|
||
- Where: `src/stores/calendar-store.ts:710-719` sends `progressUpdated`. The checkbox at
|
||
`src/components/calendar/TasksSheet.tsx:442` swallows the error.
|
||
- Web: `6b30e91c`, `stores/task-store.ts`.
|
||
- Repro: tap the task checkbox → `notUpdated: invalidProperties ["progressUpdated"]`. The task
|
||
stays `needs-action` and the UI shows no error.
|
||
- Tasks are off by default (`src/stores/settings-store.ts:381`), which limits the reach.
|
||
- **Verified.**
|
||
- [x] <a id="b13"></a>**B13 · P1 · M: the Files tab stops at 500 nodes (#1069).** Fixed in
|
||
`4eb8c47`, `115e162`.
|
||
- Where: `src/api/files.ts:98-112` (`FileNode/get ids:null`, which Stalwart silently caps at
|
||
`maxObjectsInGet`). The same code runs at `:133-167`.
|
||
- Web: `lib/jmap/client.ts` (`fetchAllFileNodes`, `9893b0a8`).
|
||
- Repro: with 510 root files created before the folders, `file-0490`–`0499` are listed, while
|
||
`file-0500`–`0509` and the Documents and Photos folders (and their files) never appear.
|
||
- **Verified.**
|
||
- [x] <a id="b14"></a>**B14 · P2 · S: bodies over 512 KB render cut off with no notice (#884).**
|
||
Fixed in `4767805`.
|
||
- Where: `src/api/email.ts:493-499` (`maxBodyValueBytes: 512000`, `isTruncated` never read).
|
||
- Web: `lib/jmap/client.ts` (`2cf7427e`, refetch of truncated bodies).
|
||
- Repro: for a 586 KB HTML report, the server returns `isTruncated: true` with 59 characters.
|
||
The viewer shows the heading, one line and "…"; the list preview shows the full text.
|
||
- **Verified.**
|
||
- [x] <a id="b15"></a>**B15 · P1 · S: a redirect that drops the Authorization header signs the user
|
||
out on launch.** Fixed in `859770b`.
|
||
- Where: `src/api/jmap-client.ts:737-763`. The retry keys on `response.redirected`, which RN's
|
||
fetch never sets. An empty session then throws `AuthenticationError`, and
|
||
`src/stores/auth-store.ts:677-683` evicts the account.
|
||
- Web: `40e38d16` (#892).
|
||
- Only affects servers behind a redirecting proxy; direct Stalwart is fine.
|
||
- **Suspected.**
|
||
- [x] <a id="b16"></a>**B16 · P1 · S: "Save draft" discards the text when identities didn't load
|
||
(offline) or there is no Drafts folder.** Fixed in `92b42e4`: the text is no longer dropped and
|
||
the iOS swipe is blocked; saving a draft offline is still impossible.
|
||
- Where: `src/screens/ComposeScreen.tsx:1208` returns early; `:1288-1290` closes anyway.
|
||
- On iOS, the `beforeRemove` listener at `:1340` doesn't stop a native swipe-down; that needs
|
||
`usePreventRemove`.
|
||
- **Suspected.**
|
||
- [x] <a id="b17"></a>**B17 · P1 · S: calendar reminders get cancelled when the calendar view
|
||
moves.** Fixed in `4b903ed`.
|
||
- Where: `src/lib/calendar-notifications.ts:116-134` rebuilds the reminder set from the
|
||
windowed `events` and cancels everything else.
|
||
- Viewing another month drops all pending reminders. Nothing reschedules them in a session
|
||
that never opens the Calendar tab.
|
||
- There is no `setNotificationHandler`, so reminders don't show while the app is in the
|
||
foreground.
|
||
- **Suspected.**
|
||
- [x] <a id="b18"></a>**B18 · P1 · S: push can end up permanently off for users with ACL-shared
|
||
mailboxes.** Fixed in `d8046a8`.
|
||
- Where: `src/lib/push-notifications.ts` (HEAD `:223-229` builds the `emailPush` map from
|
||
every account with mailboxes).
|
||
- Stalwart rejects the whole map with `forbidden` for accounts the user doesn't own and isn't
|
||
a group member of. The refresh path then destroys the working subscription and the recreate
|
||
fails the same way.
|
||
- Web: `lib/web-push.ts:86-94` has the same code.
|
||
- **Suspected.**
|
||
- [x] <a id="b19"></a>**B19 · P2 · S: "Reschedule" drops Cc and Bcc.** Fixed in `e67947a`,
|
||
`7e598c5`.
|
||
- Where: `src/api/email.ts:1393` rebuilds recipients from `scheduled.to`;
|
||
`src/screens/ScheduledScreen.tsx:140`.
|
||
- Web: `lib/jmap/client.ts` (reuses the envelope).
|
||
- Unreachable on Stalwart until B8 is fixed.
|
||
- **Suspected.**
|
||
- [x] <a id="b20"></a>**B20 · P2 · S: "Load more" skips messages in Unread and Starred views.**
|
||
Fixed in `35e0792`.
|
||
- Where: `src/stores/email-store.ts:776` uses `position: emails.length`, which counts rows that
|
||
were kept after they stopped matching the query.
|
||
- **Suspected.**
|
||
- [x] <a id="b21"></a>**B21 · P2 · S: the event editor wipes what the user typed when a calendar push
|
||
or the alias lookup arrives.** Fixed in `15717f1`.
|
||
- Where: `src/components/calendar/EventModal.tsx:231-273` (the reset effect depends on
|
||
`calendars` and `currentUserEmails`).
|
||
- **Suspected.**
|
||
- [x] <a id="b22"></a>**B22 · P2 · S: an invitation's Accept sends no RSVP when the event is outside
|
||
the loaded calendar window, yet says "Added to calendar".** Fixed in `c0582ab`.
|
||
- Where: `src/components/email/CalendarInvitationBanner.tsx:177-188`.
|
||
- **Suspected.**
|
||
- [x] <a id="b23"></a>**B23 · P2 · S: "Send failed" can be shown for mail that was sent, so a retry
|
||
sends it twice.** Fixed in `c51a848`.
|
||
- Where: `src/api/email.ts:1178-1181` throws on any `error` response. Stalwart appends one when
|
||
`onSuccessUpdateEmail` fails after a successful submission.
|
||
- **Suspected.**
|
||
- [x] <a id="b24"></a>**B24 · P3 · S: some writes report success when the server refused them.**
|
||
Fixed in `00ad2f4` (files), `a6ef0ab` (calendar), `ddfe85a` (push).
|
||
- Where: `src/api/files.ts:199` and `:225` (rename, move, delete read `[0][1]` without checking
|
||
for an `error` response); `src/api/calendar.ts:449-459` ignores `notDestroyed`;
|
||
`src/api/push.ts:14,82,105`.
|
||
- **Suspected.**
|
||
|
||
### UI glitches
|
||
|
||
- [x] <a id="u1"></a>**U1 · P2 · S: the cold-start sequence shows a blank screen and jumps twice.**
|
||
Fixed in `b9db376`, `f2dd3a8`.
|
||
- Where: `App.tsx:613-620`; `AppTheme` has no `windowBackground`; `PushOnboardingPrompt`.
|
||
- Frames:
|
||
- The dark splash is followed by about 1 s of blank white.
|
||
- Rows appear without a header, with the search bar under the status bar.
|
||
- The header then pops in and pushes the list down.
|
||
- The notification card lands over the status bar and pushes the list down again.
|
||
- **Verified** (`screenrecord`).
|
||
- [x] <a id="u2"></a>**U2 · P2 · S: opening a message shows a skeleton, then a 120 px strip of body,
|
||
then the full body.** Fixed in `54b169c`, `ac8e3fb`.
|
||
- Where: `src/components/EmailBodyView.tsx:588-591` (the height resets to 120 on every
|
||
message).
|
||
- The header data is already in the list row but isn't used. The quick-reply box jumps as the
|
||
WebView grows. See PF1.
|
||
- **Verified.**
|
||
- [x] <a id="u3"></a>**U3 · P2 · S: sender image and table widths are overridden.** Fixed in
|
||
`7531118`.
|
||
- Where: `src/lib/email-html.ts:452-454`. `img { max-width: 100% !important }` beats inline
|
||
`max-width`.
|
||
- Web: `68341a0b`, `ab4bf91b` (#790).
|
||
- Repro: an image with `max-width:200px; max-height:134px; width:100%` renders full width and
|
||
squashed about 1.9×; a table with `max-width:300px` spans the pane.
|
||
- **Verified.**
|
||
- [x] <a id="u4"></a>**U4 · P2 · S: RTL mail renders left-to-right.** Fixed in `910c503`.
|
||
- Where: `src/lib/email-html.ts:627` and `:686` (no `dir="auto"`).
|
||
- Web: `e2990da5`, `48048f16`.
|
||
- Repro: an Arabic plain-text body is left-aligned and its sentence-final period sits on the
|
||
wrong side.
|
||
- **Verified.**
|
||
- [x] <a id="u5"></a>**U5 · P3 · S: list previews keep the server's newlines.** Fixed in `d45c799`.
|
||
- Rows show a dangling "…" line and have uneven heights ("Q3 revenue is up 12 %.\n\n…").
|
||
- A subject that contains CR/LF also renders over several lines in the list and the viewer.
|
||
- Where: `src/screens/EmailListScreen.tsx` row.
|
||
- **Verified.**
|
||
- [x] <a id="u6"></a>**U6 · P3 · S: the schedule sheet offers presets, then refuses them.** Fixed in
|
||
`802b199`, `4e7ae2f`.
|
||
- Where: `src/screens/ComposeScreen.tsx:1846-1851`. The sheet should hide the presets.
|
||
- **Verified.**
|
||
- [x] <a id="u7"></a>**U7 · P3 · S: Back in Files selection mode switches to the Mail tab instead of
|
||
clearing the selection.** Fixed in `8d7fbfc`, `4fdf39a`.
|
||
- **Verified.**
|
||
- [x] <a id="u8"></a>**U8 · P3 · M: icon buttons and switches have no accessibility label.**
|
||
Fixed in `9c0c35c`, `771558d`, `aab29da`, `d59c571`, `cb5c152`, `c1e5cd6` (mail list, compose
|
||
button, composer, Files, contacts, settings) and `13cba64`, `b3a82f1`, `84fef14`, `b178ee8`
|
||
(viewer, calendar header, calendar sheets and task checkbox, tab bar, unified inbox, drawer).
|
||
- Unlabeled controls:
|
||
- mail list: hamburger, select-all, filter;
|
||
- compose FAB;
|
||
- composer: close, attach, schedule;
|
||
- every calendar header button;
|
||
- per-file menu, settings switches, task checkbox.
|
||
- Web: `4947c6c7`.
|
||
- Repro: `uiautomator dump` shows an empty `content-desc` on all of these.
|
||
- **Verified.**
|
||
- [x] <a id="u9"></a>**U9 · P3 · S: Android notifications use the stock envelope as their small
|
||
icon.** Fixed in `695aa23`.
|
||
- Where: `android/app/src/main/java/com/anonymous/bulwarkmobile/BulwarkFcmModule.kt:128` and
|
||
`:181` (`android.R.drawable.ic_dialog_email`).
|
||
- **Suspected.**
|
||
|
||
## Performance
|
||
|
||
All numbers come from the x86_64 emulator with a release build and a local server. Use them to
|
||
compare against each other, not as phone numbers.
|
||
|
||
- [x] <a id="pf1"></a>**PF1 · P1 · M: opening a message takes 1.65–1.87 s from tap to a settled
|
||
screen.** Fixed in `33e303e`, `ac8e3fb`, `1348e7d`, `01fc6fb`, `6ebadf7`, `54b169c`, `7cdebc4`,
|
||
`b7a5953`; not re-timed on a device yet.
|
||
- Where: `src/screens/EmailThreadScreen.tsx:161-199` (a network-first detail fetch even for
|
||
cached ids, plus a thread fetch with full bodies), `:641` (the pager mounts 3 panes, each
|
||
with its own WebView), `src/components/EmailBodyView.tsx:588-591` (the body is sanitized
|
||
twice per mount).
|
||
- Web: `stores/email-store.ts` and `mail-app.tsx` on `perf/fastest-mail` (header painted from the
|
||
row, prefetch on press).
|
||
- Measured:
|
||
- Plain 1 KB message 1.87 s; newsletter 1.65 s; two-message thread 1.72 s.
|
||
- Plain open, step by step: skeleton; header at +0.41 s; body at +1.02 s (clipped); resize
|
||
and quick-reply jump at +1.69 s.
|
||
- One open from the Unified Inbox made 12 requests: the body twice, the thread twice,
|
||
`Identity/get`, and a forbidden `x:Account/get`.
|
||
- **Verified** (numbers and requests); causes suspected.
|
||
- [x] <a id="pf2"></a>**PF2 · P1 · M: cold start takes 3.6 / 3.8 / 4.1 s from launch to a settled
|
||
inbox (3 runs).** Fixed in `604eb39`, `063432b`, `5bcfe1f`, `82ca359`, `b9db376`, `9c2f819`:
|
||
modules run before the first frame 3,590 → 1,516, Hermes bundle 9.34 → 7.61 MB, a headless push
|
||
start 202 modules (`npx expo export`). The four hydrations were already awaited together
|
||
(`85f23df`). Not re-timed on a device yet.
|
||
- Measured:
|
||
- `am start` reports 451–564 ms to the first frame (the splash).
|
||
- Cached rows at about +1.2 s.
|
||
- The first network request at +1.84 s (`GET /jmap/session`).
|
||
- Then 700 ms of JS work before the 14-request delta burst.
|
||
- Network is not the bottleneck: every request finishes within 0.7 s.
|
||
- Causes:
|
||
- All 27 locale catalogs are imported and built at startup (`src/i18n/index.ts:11-37`), 36 %
|
||
of the 9.13 MB Hermes bundle.
|
||
- `lucide-react-native` is imported through its barrel in 93 files (e.g. `App.tsx:8`), which
|
||
evaluates all 1,694 icons (1.08 MB, 12 %).
|
||
- The date-fns locale barrel (`src/lib/calendar-locale.ts:2-13`) adds 830 KB.
|
||
- 3,586 of 3,993 modules run before the first frame.
|
||
- The session fetch waits for four serial AsyncStorage hydrations
|
||
(`src/stores/auth-store.ts:603-608`).
|
||
- The headless push task imports the whole app (`index.ts:10`).
|
||
- **Verified** (device timings; bundle shares measured by rebuilding the bundle with hermesc and
|
||
removing module groups one at a time).
|
||
- [ ] <a id="pf3"></a>**PF3 · P2 · M: the mail list is heavier than the emulator floor.** Partly:
|
||
`cb9a741`, `aa25075`, `ebd8dac` fix the inline callbacks, the swipe bands and the per-row date
|
||
formatters; the swipe drag still runs on the JS thread, and nothing was re-measured.
|
||
- Measured over already-loaded rows (14 flings): p50 21 ms, p90 32 ms, p95 38 ms, p99 57 ms.
|
||
The same flings in the system Settings list: 17 / 18 / 20 / 36 ms.
|
||
- Causes:
|
||
- Inline `onEndReached` and `onRefresh`, and no `strictMode`, so FlatList re-runs every
|
||
`renderItem` (`src/screens/EmailListScreen.tsx:1285-1288`).
|
||
- SwipeableRow is not memoized, creates 4 new interpolations per render, and keeps both swipe
|
||
bands mounted in every row (`src/components/SwipeableRow.tsx:252-297`).
|
||
- Row dates build two `Intl` formatters per row (`src/lib/date-format.ts:75-97`).
|
||
- The PanResponder drag runs on the JS thread (`src/components/SwipeableRow.tsx:170-172`).
|
||
- **Verified** (numbers); causes suspected.
|
||
- [ ] <a id="pf4"></a>**PF4 · P2 · S: hidden screens keep rendering.** Partly: `4a5d837` adds
|
||
`freezeOnBlur` and a count-only badge selector; the list directly under an open message stays live
|
||
on Fabric, and `fetchMailboxes` still sets a new array on every fetch.
|
||
- Where: there is no `enableFreeze` or `freezeOnBlur` (`App.tsx:634`, `index.ts`).
|
||
- The tab navigator subscribes to the whole `mailboxes` array to compute one badge
|
||
(`App.tsx:127`), and `src/stores/email-store.ts:1855` sets a new array on every mailbox
|
||
fetch.
|
||
- So every refresh also re-renders the mail list under an open message.
|
||
- **Suspected.**
|
||
- [x] <a id="pf5"></a>**PF5 · P2 · S–M: every store write re-serializes the whole persisted cache.**
|
||
Fixed in `48825e9`, `e37621b`.
|
||
- Where: `src/stores/email-store.ts:1723-1731` (the email cache, with the current folder
|
||
stored twice), `src/stores/contacts-store.ts:824-829`,
|
||
`src/stores/calendar-store.ts:769-775`. This includes `loading` flips; there is no debounce.
|
||
- Opening one unread message causes about 5 full writes.
|
||
- **Suspected.** The stringify cost was measured off-device: 1.25 ms for 425 rows in V8.
|
||
- [x] <a id="pf6"></a>**PF6 · P2 · S: requests that can be cut, all seen on the wire.** Fixed in
|
||
`d097ae9`, `668e010`, `1c7c2a3`, `018accd`, `3bf2c30`, `1196ad5`, `9fa931c`.
|
||
- Every drawer open sends 14 `Email/query` calls for tag counts.
|
||
- The Contacts tab re-downloads all cards (58 KB) on every open, although they load at
|
||
startup.
|
||
- Returning to the Unified Inbox reloads page 1 of every account
|
||
(`src/screens/UnifiedInboxScreen.tsx:133-137`).
|
||
- The echo of the app's own writes triggers one or two full refresh cycles (10 requests after
|
||
one send).
|
||
- Starts and message opens send an `x:Account/get` that non-admin users are always refused.
|
||
- The sort-polarity probe runs on every start and is only cached in memory
|
||
(`src/lib/keyword-sort-polarity.ts:61-81`).
|
||
- Resume sends 2 `Core/echo`s, then waits about 0.8 s before syncing.
|
||
- There is a 30 s `Core/echo` on top of the SSE pings (`App.tsx:581-596`).
|
||
- Backgrounding is fine: no requests during 40 s in the background.
|
||
- **Verified.**
|
||
- [x] <a id="pf7"></a>**PF7 · P2 · S: every list page and every search is 3 sequential requests.**
|
||
Fixed in `d987930`, `4f3940f`, `9c2f819`.
|
||
- The chain is `Email/query` → `Email/get` → `Thread/get` (the last one fired after render).
|
||
First login adds two `Mailbox/get` and a 500 ms gap.
|
||
- Where: `src/stores/email-store.ts:2049-2060`, `src/screens/EmailListScreen.tsx:339-358`.
|
||
- Web: `lib/jmap/client.ts` on `perf/fastest-mail` (query, get and `Thread/get` via
|
||
`#ids /list/*/threadId` in one request). RN's `src/api/unified-inbox.ts:383-397` already
|
||
chains query and get.
|
||
- **Verified.**
|
||
- [x] <a id="pf8"></a>**PF8 · P2 · M: each keystroke in the composer re-renders the 3,000-line
|
||
screen twice.** Fixed in `fc00781`.
|
||
- Where: `src/lib/editor-html.ts:160-168` posts the full `innerHTML` on every input and on every
|
||
selection change; `src/screens/ComposeScreen.tsx:2276-2277`.
|
||
- `htmlToPlainText` and a `JSON.stringify` snapshot also run on every render.
|
||
- **Suspected.**
|
||
- [x] <a id="pf9"></a>**PF9 · P2 · M: the APK is 42.8 MB (arm64) with R8 off.** Fixed in
|
||
`5e3cbf2`: on the x86_64 build the APK went from 44.7 to 38.0 MB, dex from 30.4 MB in 4 files
|
||
to 11.3 MB in 2, and classes from 27,631 to 16,724. The rest is native libraries (ML Kit alone
|
||
5.9 MB) and the JS bundle, which R8 doesn't touch.
|
||
- Where: `android/app/build.gradle:72` and `:126-129`.
|
||
- Contents: 25.85 MB of unminified dex, 24,621 classes; the ML Kit barcode scanner accounts for
|
||
5.83 MB. The UnifiedPush work in progress adds 3,009 classes (Tink, protobuf).
|
||
- **Verified** (APK inspection of the published 0.1.62).
|
||
- [ ] <a id="pf10"></a>**PF10 · P3 · S: slow paths that need large data to show.** Partly: contacts
|
||
search (`e6be892`) and bulk read/star (`cb2f03b`) are fixed; upload progress and the offline-cache
|
||
index writes are unchanged.
|
||
- Contacts search re-sorts with `localeCompare` on every keystroke
|
||
(`src/stores/contacts-store.ts:923-925`).
|
||
- Bulk mark-read and star send N requests against a concurrency limit of 4
|
||
(`src/screens/EmailListScreen.tsx:668-681`); `setKeywordForEmails` exists at
|
||
`src/stores/email-store.ts:1530`.
|
||
- Files upload progress is not throttled.
|
||
- The offline-cache index is rewritten once per message.
|
||
- **Suspected.**
|
||
|
||
## Missing vs webmail
|
||
|
||
Status: *missing* (no native equivalent), *partial*, *diverges* (exists but behaves differently or
|
||
wrongly). Items already listed under Bugs are only cross-referenced here.
|
||
|
||
### Auth and accounts
|
||
|
||
- [x] **P2 · S · missing: adding a second OAuth account silently signs in the same user again.**
|
||
Fixed in `ab84578`.
|
||
- Where: `src/lib/oauth-native.ts:127-141` (no `prompt=select_account`, no ephemeral browser
|
||
session).
|
||
- Web: `5771a213`.
|
||
- **Suspected.**
|
||
- [x] **P3 · S · partial: a pasted `…/jmap/session` URL fails discovery with a 404.** Fixed in
|
||
`bd42206`.
|
||
- Where: `src/lib/server-discovery.ts:59-63`.
|
||
- Web: `b26c1746` (#971).
|
||
- **Suspected.**
|
||
- Also see B15 (#892 redirect).
|
||
|
||
### Mail list, folders and search
|
||
|
||
- [x] **P2 · L · diverges: "All folders" search only covers the open folder's account (#1082).**
|
||
Fixed in `ee50418`, `edab181`, `d2066da`.
|
||
- Where: `src/stores/email-store.ts:384-389`.
|
||
- Web: `lib/unified-mailbox.ts` (`searchAcrossAccounts`, `9ef7068b`).
|
||
- Repro: "zephyr" with All folders → `Email/query` on account `c` only → 3 hits. The 5 matches
|
||
in the team inbox are missing.
|
||
- **Verified.**
|
||
- [x] **P2 · M · diverges: the tag view and tag badges only cover one account (#1038).** Fixed in
|
||
`8d5bced`, `d097ae9`.
|
||
- Where: `src/stores/email-store.ts:209` and `:387`, `src/components/SidebarDrawer.tsx:426-436`.
|
||
- Web: `stores/email-store.ts` (`fetchTagEmails`, `fedfc89d`).
|
||
- Repro: 2 own and 2 team messages tagged Red → the Red view shows 2.
|
||
- **Verified.**
|
||
- [x] **P3 · S · missing: folders created on the phone are not subscribed.** Fixed in `30aea63`.
|
||
- Where: `src/api/email.ts:211-221` sends no `isSubscribed: true`, so IMAP clients hide them.
|
||
- Web: `2b186910` (#951).
|
||
- **Suspected.**
|
||
- [x] **P3 · S · diverges: archiving to a role-less "Archives" folder is offered but does nothing.**
|
||
Fixed in `9f75e70`, `ad1f053`.
|
||
- Where: the list matches by name (`src/screens/EmailListScreen.tsx:393-395`), but the store
|
||
needs the role or the exact name (`src/stores/email-store.ts:1202-1205`).
|
||
- Web: `2b32a011` (#578).
|
||
- **Suspected.**
|
||
- [ ] **P3 · S · missing: no unread badge on the app icon.** Partly: `25c0883` badges the icon on
|
||
iOS; Android is left to the launcher (see the fix-pass decisions).
|
||
- expo-notifications `setBadgeCountAsync` is already available.
|
||
- Web: `hooks/use-app-badge.ts` (`e7d37823`).
|
||
- **Suspected.**
|
||
- [x] **P3 · S · missing: list rows show no attachment chips.** Fixed in `b1fb2e9`, `edab181` (not
|
||
yet on Unified Inbox rows).
|
||
- Web: `0e1c34cd`.
|
||
- **Suspected.**
|
||
- [x] **P3 · S · missing: no "clear search when switching folders" setting.** Fixed in `4e64261`.
|
||
- Web: `12f01409`.
|
||
- **Suspected.**
|
||
- [x] **P3 · S · diverges: the Unified section shows even when it can't be populated.** Fixed in
|
||
`af6290f`.
|
||
- Where: `src/components/SidebarDrawer.tsx:639`.
|
||
- Web: `f3f0127c` (#843).
|
||
- **Suspected.**
|
||
- Not applicable: global search (Pro, desktop-only); category tabs (only exist when a plugin
|
||
registers them).
|
||
|
||
### Viewer
|
||
|
||
- [x] **P2 · M · missing: no per-message HTML/plain-text toggle, and reopened drafts are forced into
|
||
the global format.** Fixed in `472c4d8`.
|
||
- Where: `src/screens/ComposeScreen.tsx:531-536`. With plain text on, an HTML draft is
|
||
flattened on its first autosave.
|
||
- Web: `f6ad8a59` (#1022), `components/email/compose-format.ts`.
|
||
- **Suspected.**
|
||
- [x] **P2 · S · missing: sender avatars ignore address-book photos (#54).** Fixed in `eca3871`.
|
||
- Web: `components/ui/avatar.tsx` (looks up the sender's contact photo by email; the photo wins
|
||
over favicons and initials).
|
||
- Repro: Alice Nguyen's contact photo shows in Contacts; her mail shows "AN" initials.
|
||
- **Verified.**
|
||
- [x] **P2 · S · diverges: the viewer's Spam button is a plain move.** Fixed in `5e42c67`.
|
||
- It sends no `$junk`/`$notjunk` flip and doesn't honour trash-and-read.
|
||
- Where: `src/screens/EmailThreadScreen.tsx:395-407`. The list and swipe paths are right
|
||
(`src/stores/email-store.ts:1058-1146`).
|
||
- Web: `150c25ca` (#695).
|
||
- **Suspected.**
|
||
- [x] **P2 · S · diverges: viewer Archive silently does nothing for messages that are not in the
|
||
loaded list.** Fixed in `33d2674`.
|
||
- This covers the unified inbox, notification taps and deep links.
|
||
- Where: `src/stores/email-store.ts:1196-1197`.
|
||
- Web: `150c25ca`.
|
||
- **Suspected.**
|
||
- [x] **P3 · S · partial: failed actions and sends give no feedback.** Fixed in `9e95273`,
|
||
`cd911ad`.
|
||
- The store actions are called with `void`, and there is no "Email sent" toast.
|
||
- Web: `ec67ab99`, `b03a0c1d`, `lib/email-action-toast.ts`.
|
||
- **Verified** (no toast after send); failure path suspected.
|
||
- [x] **P3 · S · missing: body-embedded `cid:` parts typed octet-stream appear as nameless chips.**
|
||
Fixed in `1d1d6a5`.
|
||
- Where: `src/lib/attachment-display.ts:91-104`.
|
||
- Web: `1c00812a`.
|
||
- **Suspected.**
|
||
- Also see B14 (truncation), U3 (image sizing), U4 (RTL), B5, B3.
|
||
|
||
### Composer
|
||
|
||
- [x] **P2 · S · diverges: replies go out from the default identity unless "Reply from received
|
||
address" is on.** Fixed in `a0c2bd7`.
|
||
- Forward also rewrites the From address, and replies to your own sent mail resolve from the
|
||
other party.
|
||
- Where: `src/screens/ComposeScreen.tsx:836-845`, `src/lib/reply-identity.ts:46-77,170`.
|
||
- Web: `4fc7b8b5`, `lib/reply-identity.ts`.
|
||
- **Suspected.**
|
||
- [x] **P3 · S · missing: a new message in a shared folder doesn't default to that mailbox's
|
||
identity.** Fixed in `0ffdcac`.
|
||
- Where: `findComposeIdentityId` is ported but never called.
|
||
- Web: `129f545a`.
|
||
- **Suspected.**
|
||
- [x] **P3 · S · missing: no exact-address vs same-domain choice for "reply from received".** Fixed
|
||
in `2d907ff`.
|
||
- Web: `565d5c10` (#1000).
|
||
- **Suspected.**
|
||
- [ ] **P3 · M · partial: no font-size picker or background colour; 8 text colours instead of 16.**
|
||
Partly: `548b647` adds the background colour and the 16 text colours; the font-size picker is not
|
||
ported.
|
||
- Where: `src/components/RichTextEditor.tsx:8-26`.
|
||
- Web: `7e0a86a6`, `3957b358`.
|
||
- **Suspected.**
|
||
- [x] **P3 · S · diverges: quick replies carry no signature.** Fixed in `3faac98`.
|
||
- Where: `src/components/email/QuickReplyBox.tsx:79-90`.
|
||
- **Suspected.**
|
||
- [x] **P3 · S · partial: the undo-send snackbar exists only on the list screen.** Fixed in
|
||
`e3501f9`: the bar is hosted once per stack screen, and quick replies record their hold.
|
||
- A reply sent from the viewer can't be undone.
|
||
- Where: `src/screens/EmailListScreen.tsx:1563`.
|
||
- **Suspected.**
|
||
- Also see B8 (#57), B16.
|
||
|
||
### Calendar
|
||
|
||
- [x] **P2 · S · missing: CalDAV tasks with `COMPLETED`, `NEEDS_ACTION` or `canceled` count as
|
||
pending and toggle the wrong way.** Fixed in `1b64c14`.
|
||
- Web: `5f1d2795` (`normalizeCalendarTask`).
|
||
- **Suspected.**
|
||
- [x] **P3 · L · missing: no free-scrolling month, week or day views (#759) and no setting for
|
||
them.** Fixed in `caf61ec`, `de3d697`.
|
||
- Where: `src/screens/CalendarScreen.tsx:89,169-171,360-374`.
|
||
- Web: `15a5497a`, `ae403b06`, `hooks/use-scroll-window.ts`.
|
||
- **Suspected.**
|
||
- [x] **P3 · M · missing: the agenda is a fixed 30-day page instead of infinite scroll.** Fixed in
|
||
`71522b9`.
|
||
- Where: `src/components/calendar/AgendaView.tsx:46`.
|
||
- Web: `60e05c00`.
|
||
- **Suspected.**
|
||
- [x] **P3 · L · diverges: recurring events are always expanded client-side.** Fixed in `8454889`,
|
||
`f397ee3`, `96c5a3f`.
|
||
- Where: `src/api/calendar.ts:267-304`, `src/lib/recurrence-expansion.ts`.
|
||
- This matches the webmail's pre-0.16.20 fallback, but there is no server-side expansion and no
|
||
per-occurrence edit through synthetic ids.
|
||
- Web: `lib/recurrence-instances.ts`.
|
||
- **Suspected.**
|
||
- [x] **P3 · S · partial: participants aren't deduplicated or matched to contact names, and the
|
||
organizer's status dot is wrong.** Fixed in `1d755fc`.
|
||
- Where: `src/lib/calendar-participants.ts:130-167`.
|
||
- Web: `8cbc5bc5`, `6f99c017`.
|
||
- **Suspected.**
|
||
- [x] **P3 · S · diverges: a task's due time shows the raw Europe/Berlin wall time (17:00), while
|
||
events are converted to the device zone.** Fixed in `6a975b8`, `1a651a9`, `a8e6f6e`.
|
||
- **Suspected.**
|
||
- Links in event descriptions are fixed on GitHub `main` (`c2584c2`) but not in this checkout.
|
||
- Also see B12, B17, B21, B22.
|
||
|
||
### Contacts
|
||
|
||
- [x] **P2 · S · partial: the vCard FN (`name.full`) is missing or stale on form, "add sender" and
|
||
import writes (#430).** Fixed in `a734d0a`.
|
||
- Where: `src/screens/ContactFormScreen.tsx:288-293`, `src/lib/vcard.ts:575-603`.
|
||
- Web: `a210b22e`.
|
||
- **Suspected.**
|
||
- [x] **P3 · S · missing: no sort by last name (#963).** Fixed in `1827748`.
|
||
- Where: `src/stores/contacts-store.ts:923-925`.
|
||
- Web: `fb24ef5d`.
|
||
- **Suspected.**
|
||
- [x] **P3 · S · missing: a new contact made while viewing address book X lands in the default
|
||
book.** Fixed in `c888e95` (groups: `a3193eb`).
|
||
- Where: `src/screens/ContactsScreen.tsx:238,396`.
|
||
- Web: `494adb9b`.
|
||
- **Suspected.**
|
||
|
||
### Files
|
||
|
||
- [x] **P2 · S · diverges: sharing with other users is never offered.** Fixed in `caea3d0`.
|
||
- Where: `src/api/files.ts:30-35` and `src/components/calendar/CalendarShareSheet.tsx:86` gate
|
||
on `principals:owner`, which Stalwart advertises nowhere. The session only has `principals`
|
||
and `principals:availability`.
|
||
- Web: `components/files/files-app.tsx` (gates on `urn:ietf:params:jmap:principals`).
|
||
- Repro: the per-file menu shows Preview/Share (OS), Download, Move, Duplicate, Rename,
|
||
Delete, and no "Share with…".
|
||
- **Verified.**
|
||
- [x] **P3 · M · missing: no in-app file preview.** Fixed in `409cdb1` (images, text, and PDFs on
|
||
iOS; Android PDFs, audio and video still go to another app).
|
||
- Tapping a file downloads it and opens the OS "Open with" chooser.
|
||
- **Verified.**
|
||
- Also see B13 (500-node cap). WOPI editing (#425) is listed under open questions.
|
||
|
||
### Filters and vacation
|
||
|
||
- [x] **P2 · S · missing: "Keep" is still written as `keep;`.** Fixed in `f63d739`.
|
||
- Saving filters on the phone therefore undoes the webmail fix for allow-list rules.
|
||
- Where: `src/lib/sieve/generator.ts:131-132`.
|
||
- Web: `62465e1e` (#1027).
|
||
- **Suspected.**
|
||
- [x] **P3 · M · missing: no filters or vacation for shared/group accounts.** Fixed in `a86cd1d`.
|
||
- Where: `src/api/sieve.ts:20-23`, `src/api/vacation.ts:26-27`.
|
||
- **Suspected.**
|
||
|
||
### Settings, themes and i18n
|
||
|
||
- [x] **P2 · M · partial: locale catalogs are stale.** Fixed in `55f1ec4`, `008bff2`, `67935d2`.
|
||
- Last synced 2026-08-29 (`990cd84`): 2,959 keys against the webmail's 3,065. The German
|
||
update changed 59 values.
|
||
- `email_viewer.forward_as_attachment` is still English in about 25 locales.
|
||
- The 567 RN-only keys are English in every locale (`src/i18n/index.ts:98-100`).
|
||
- Fix: run `scripts/sync-locales.mjs` and translate the RN overlay.
|
||
- **Suspected.**
|
||
- [x] **P3 · S · partial: the Reading pane hard-codes English.** Fixed in `2511df1`.
|
||
- Labels such as "Message Spacing", "Read Receipts" and "Always send" don't go through `t()`
|
||
(`src/components/settings/ReadingSettings.tsx:305-327`).
|
||
- **Verified.**
|
||
- [x] **P3 · S · diverges: plural forms probably fall back to English rules.** Fixed in `e6549d3`.
|
||
- Where: `src/i18n/format.ts:18-47`. Hermes on Android has no `Intl.PluralRules`, so Russian
|
||
shows "2 писем".
|
||
- **Suspected.**
|
||
- [ ] **P3 · M · missing: only 4 of the webmail's 7 built-in themes, and no "Flat fields".** Partly:
|
||
`7964e64` adds Roundcube Elastic and Aurora Glass; "Flat fields" is not ported (a product
|
||
decision).
|
||
- Where: `src/theme/builtin-themes.ts`.
|
||
- Web: `ab67d9b0`, `lib/theme-flat-fields.ts`.
|
||
- **Suspected.**
|
||
- [x] **P3 · M · missing: no settings search.** Fixed in `46e285b`.
|
||
- Web: `lib/settings-search.ts`.
|
||
- **Suspected.**
|
||
- [ ] **P3 · L · diverges: native still uses Lucide icons; the webmail moved to Tabler (`d7548a4b`).**
|
||
- This is a product decision, not a bug.
|
||
- **Suspected.**
|
||
|
||
### Push
|
||
|
||
- [x] **P1 · S · diverges: tapping a group-mailbox notification opens the message against the wrong
|
||
account.** Fixed in `8618abf`.
|
||
- The tap payload carries only the registry account, not the JMAP account.
|
||
- Where: `src/lib/push-background-task.ts:448-460`, `App.tsx:85-89`.
|
||
- Web: `b4f12b67` (#839).
|
||
- **Suspected.**
|
||
- [x] **P2 · S · diverges: `emailPush` is never read back, so the subscription is re-patched on
|
||
every launch.** Fixed in `ccaa24e`.
|
||
- `using` also lacks the emailpush capability.
|
||
- Where: `src/api/push.ts:11` (`PushSubscription/get` with default properties).
|
||
- Web: `lib/jmap/client.ts` (explicit properties).
|
||
- **Suspected.**
|
||
- [x] **P2 · S · diverges: revoking a device or disabling one account doesn't stick.** Fixed in
|
||
`a3d3af7`.
|
||
- Setup re-runs for the active account on every launch (HEAD `App.tsx:447-453`).
|
||
- **Suspected.**
|
||
- UnifiedPush (#44, #48) exists only as uncommitted work in this tree. Also see B18.
|
||
|
||
### Security
|
||
|
||
- [x] <a id="s1"></a>**S1 · P0 · S · missing: the GHSA-w38p fix (read-receipt header injection) was
|
||
never ported.** Fixed in `cb6ddd6`.
|
||
- Where: `src/lib/mdn.ts:77-81` (`encodeHeaderWord` passes pure-ASCII values, CR/LF
|
||
included, through raw), `:111-113`, `:140-141`.
|
||
- Web: `lib/mdn.ts` (`stripHeaderControls`, `headerAtom`; `53a6670e`).
|
||
- Repro:
|
||
- Subject `=?UTF-8?B?…?=` decoding to `Contract update\r\nX-Injected: yes\r\nReply-To: attacker@evil.example`.
|
||
- Tap "Send receipt". The MDN stored in Sent and submitted has `X-Injected: yes` and
|
||
`Reply-To: attacker@evil.example` as real header lines.
|
||
- A blank line in the payload would give the attacker the body too.
|
||
- With B7 this becomes zero-click: the malicious message only needs to sit next to one the
|
||
user opens.
|
||
- **Verified.**
|
||
- [x] <a id="s2"></a>**S2 · P0 · S–M: releases are signed with the public React Native debug key.**
|
||
Fixed in `6f0abb4`, `746572c`, `5237848`; the release key, rotation lineage and secrets still have
|
||
to be created before the next release (`docs/android-release.md`).
|
||
- Where: `android/app/build.gradle:103-125`; `.github/workflows/release-android.yml:69-91` falls
|
||
back to it silently when `ANDROID_KEYSTORE_BASE64` is empty; `android/app/build.gradle:98`
|
||
(`versionCode 1`).
|
||
- Evidence: GitHub release 0.1.62 `bulwark-mobile-0.1.62-6c82130.apk` is signed by
|
||
`CN=Android Debug`, SHA-1 `5e8f1606…f625`. That is the stock template key, and its SHA-256
|
||
matches the committed `android/app/debug.keystore` (password `android`).
|
||
- Risk: anyone can sign an "update" that installs over the app and reads its SecureStore
|
||
credentials.
|
||
- Fix: a private key, a workflow that fails without it, v3 key rotation for existing
|
||
installs, and a `versionCode` derived from `VERSION`.
|
||
- **Verified.**
|
||
- [x] **P2 · S · diverges: "Sync trusted senders to address book" trusts every contact in every
|
||
book.** Fixed in `d89e331`.
|
||
- Remote images and tracking pixels then load for any contact, including a spoofed From.
|
||
- The toggle switches itself on the first time the Content & Senders pane is opened on a
|
||
contacts-capable account (`src/components/settings/ContentSendersSettings.tsx:54-57`).
|
||
- Where: `src/components/EmailBodyView.tsx:533-544,562-566`.
|
||
- Web: `components/email/email-viewer.tsx` (only the Trusted Senders book counts).
|
||
- **Suspected.**
|
||
- [x] **P2 · S · missing: replying or forwarding loads the remote images the viewer blocked.** Fixed
|
||
in `235f4ea`.
|
||
- The editor page has no CSP (`src/components/RichTextEditor.tsx:223-253`,
|
||
`src/lib/editor-html.ts:29-35`).
|
||
- **Suspected.**
|
||
- [ ] **P2 · S · diverges: the webmail-to-app sign-in hand-off puts the password in the
|
||
`bulwarkmobile://…#password=` fragment.** Open: needs a one-time code exchange on the webmail
|
||
side; no app change in this pass.
|
||
- Where: `src/lib/oauth.ts:139-146`. The webmail side is `app/(main)/[locale]/login/page.tsx`.
|
||
- **Suspected.**
|
||
- [ ] **P3 · S · diverges: list and notification favicons are fetched from icons.duckduckgo.com on
|
||
the device.** Partly: `95c24a4` makes notifications honour the Sender Favicons switch; with it on
|
||
(still the default) the phone fetches from DuckDuckGo.
|
||
- This leaks sender domains and the device IP to a third party.
|
||
- Where: `src/lib/avatar-utils.ts:138-139`.
|
||
- Web: server-side `/api/favicon`.
|
||
- **Suspected.**
|
||
- Body WebView hardening is at parity: `about:blank` origin, no file access, DOM storage or
|
||
cookies, a `default-src 'none'` CSP, and the link-scheme gate.
|
||
|
||
## Stale parity checklist
|
||
|
||
Of the 418 items, 362 are ticked and 56 open; the counts match `PARITY_CHECKLIST.md`. The
|
||
spot-check covered 35 ticks plus the ones re-tested on the device.
|
||
|
||
### Ticked, but broken
|
||
|
||
- [x] `docs/parity/03-email-viewer.md:107`, read receipts: sent unsanitized, and sent for unseen
|
||
neighbour panes (S1, B7). **Verified.** Fixed in `cb6ddd6`, `5f3f584`.
|
||
- [x] `docs/parity/02-mail-list-folders.md:112` and `docs/parity/03-email-viewer.md:15`, pager and
|
||
wrong-message fixes: the body is right, but actions still hit the wrong account and message
|
||
(B3, B5). **Verified.** Fixed in `5c7301b`, `9c31de2`.
|
||
- [x] `docs/parity/05-calendar.md:268`, task edit/toggle: completing fails (B12). **Verified.**
|
||
Fixed in `dcdda5e`.
|
||
- [x] `docs/parity/08-settings-push-i18n-ui.md:297`, deep links, mailto and share targets: SENDTO,
|
||
shared files and `+` addresses all fail (B9–B11). **Verified.** Fixed in `cd4ce9d`, `06e8942`,
|
||
`ee652d3`.
|
||
- [x] `docs/parity/07-filters-vacation-files.md:177` ("Verified at parity: `FileNode/get
|
||
ids:null`"): capped at 500 (B13). Its claim that `/query` drops folders has been outdated since
|
||
Stalwart 0.16.6. **Verified.** Fixed in `4eb8c47`; the parity line is corrected.
|
||
- [x] `docs/parity/02-mail-list-folders.md:244`, search all folders (#788): scope is one account
|
||
(#1082 above). **Verified.** Fixed in `ee50418`.
|
||
- [x] `docs/parity/02-mail-list-folders.md:187`, `$junk`/`$notjunk` flip: the viewer's buttons
|
||
still do a plain move. **Suspected.** Fixed in `5e42c67`.
|
||
- [x] `docs/parity/01-auth-accounts.md:57` and `docs/parity/09-jmap-core-sync-security.md:99`,
|
||
redirect with a dropped Authorization header (B15). **Suspected.** Fixed in `859770b`.
|
||
- [x] `docs/parity/04-composer-send.md:12`, draft autosave: loses text offline, and iOS swipe-down
|
||
bypasses the dialog (B16). **Suspected.** Fixed in `92b42e4`.
|
||
- [x] `docs/parity/04-composer-send.md:224`, reschedule: drops Cc and Bcc (B19). **Suspected.**
|
||
Fixed in `e67947a`, `7e598c5`.
|
||
- [x] `docs/parity/05-calendar.md:261`, reminders: cancelled when the view moves (B17).
|
||
**Suspected.** Fixed in `4b903ed`.
|
||
- [x] `docs/parity/06-contacts.md:259`, trusted-senders book: trusts every contact (Security).
|
||
**Suspected.** Fixed in `d89e331`.
|
||
- [x] `docs/parity/08-settings-push-i18n-ui.md:139` and `:154`, push types and the `emailPush`
|
||
filter: never read back, and breaks for ACL-shared accounts (B18). **Suspected.** Fixed in
|
||
`ccaa24e`, `d8046a8`.
|
||
- [ ] `docs/parity/02-mail-list-folders.md:217`, date-locale: the tick claims a `dateLocale` setting
|
||
that doesn't exist. **Suspected.** Open: there is still no `dateLocale` setting; the parity item
|
||
is unticked again.
|
||
|
||
### Unticked, but done or obsolete
|
||
|
||
Applied to `docs/parity/` in the 2026-09-24 fix pass: the done items are ticked, the obsolete
|
||
ones moved to the N/A lists, and the wrong text corrected.
|
||
|
||
- **Done (tick them):**
|
||
|
||
| Item | Where it landed |
|
||
|---|---|
|
||
| `01:109` remove a non-active account | `6ae21d5`, `490355c` |
|
||
| `01:114` switcher error state | `6ae21d5` |
|
||
| `01:129` account cap | now 10, `src/lib/account-utils.ts:4` (`6ae21d5`) |
|
||
| `01:139` unified shared-account filter | `6ae21d5` |
|
||
| `02:163` permanent-delete confirm | `src/screens/EmailThreadScreen.tsx:361-386` (`d2ed27f`) |
|
||
| `02:168` "never" mark-as-read | `d2ed27f` |
|
||
| `04:256` mailto/share intents | Android; broken per B9–B11 (`5802cae`; fixed in `cd4ce9d`, `06e8942`, `ee652d3`) |
|
||
| `06:200` add sender to contacts | `d2ed27f` |
|
||
| `06:281` auto-trust reply recipients | `5b72c4d` |
|
||
| `07:164` quota | shown in Account settings (`490355c`) |
|
||
| `09:131` contact state changes for shared accounts | `26a34d5` |
|
||
- **Obsolete (drop them):**
|
||
- `05:91` natural-language event input: the webmail has no parser.
|
||
- `06:294` print contact: not applicable on mobile.
|
||
- `07:45` Sieve editor gutter/Tab: not applicable on mobile.
|
||
- **Text that is now wrong:**
|
||
- `05:254`: push types are `['EmailDelivery']`.
|
||
- `05:222`, `06:215`, `07:152`: "no linking config" (untrue since `5802cae`).
|
||
- `08:15`, `08:235`: `useTypography` has 1 caller, not 3.
|
||
- `08:223`: "no RTL" (untrue since `990cd84`).
|
||
- `08:122`: cites `lib/error-reporting.ts`, which the webmail deleted.
|
||
- `07` "Verified at parity" Sieve generator: the webmail changed "Keep" (`62465e1e`).
|
||
|
||
## Open issues on bulwarkmail/native
|
||
|
||
| Issue | Status after this audit |
|
||
|---|---|
|
||
| #57 scheduled sending unsupported | Root cause found and reproduced (B8). Fixed in `e28e8b4`. |
|
||
| #54 contact photos not used as avatars | Reproduced (Viewer, "sender avatars"). Fixed in `eca3871`. |
|
||
| #50 crash opening any mail | Not reproduced on 0.1.62; likely fixed by `f228257`. Ask the reporter, then close. |
|
||
| #47 account security not accessible | Fixed: Security → Account Security works on 0.16.23. Close. |
|
||
| #46 email preview shows CSS | Not reproduced: HTML-only mail with `<style>` shows clean previews, and the viewer routes by type (`06742ef`). Ask for a sample, then close. |
|
||
| #44, #48 push without FCM / UnifiedPush | Uncommitted work in this tree; not in any release. Committed in `ee04e95`; not released yet. |
|
||
| #3 TLS client auth | Partial: Android has a certificate picker (Settings → Security); iOS doesn't. |
|
||
| #34 sync adapters | Done on Android: contacts and calendars sync both ways with the device's apps ([design](device-sync.md)). |
|
||
| #58 encryption, #52 widgets, #51 IMAP/SMTP, #1 settings sync | Feature requests; see open questions. |
|
||
|
||
No open webmail issue is about the native app. The six mobile-titled ones (#813, #838, #915,
|
||
#969, #999, #1020) concern the web and PWA.
|
||
|
||
## Open questions (product decisions)
|
||
|
||
- **Tasks** are off by default (`enableCalendarTasks: false`). Should they be on, now that the
|
||
webmail ships them?
|
||
- **Design language:** should native follow the webmail's "Flat fields" theme and Tabler icons,
|
||
or keep its own look?
|
||
- **Office documents:** WOPI editing (#425) needs the webmail as the WOPI host. Should native open
|
||
office files in Collabora through the webmail, or keep handing them to the OS?
|
||
- **Barcode scanner:** the bundled ML Kit scanner costs 5.8 MB and starts with every process,
|
||
including headless pushes. Google's code scanner needs Play services, which conflicts with the
|
||
Google-free UnifiedPush audience.
|
||
- **Package name:** `com.anonymous.bulwarkmobile` (Android) vs `org.bulwarkmail.mobile` (iOS).
|
||
This must be decided before any store listing, because changing it later means a new app.
|
||
- **Read receipts and the pager:** with neighbour pages pre-rendered, should "Always send" wait
|
||
for the page to be visible for some time, or apply only to the message that was actually
|
||
opened? Decided in `5f3f584`: only the page on screen auto-sends, once per message per session.
|
||
- **Settings sync (#1):** needs a server-side store, which neither client has today.
|
||
- **Scope of the feature requests:** encryption (#58), widgets (#52) and IMAP/SMTP (#51).
|
||
|
||
## Reproducing
|
||
|
||
- **Test server:** the container `stw-native-audit` (Stalwart 0.16.23, `127.0.0.1:18095`) is
|
||
stopped with the seeded data kept.
|
||
- Start it with `docker start stw-native-audit`.
|
||
- Test accounts `usera@example.org` and `userb@example.org` use the same passwords as on
|
||
`stw-test19`; the recovery admin password is in the maintainer's notes, not here.
|
||
- To seed, raise `x:Jmap/set` `maxUploadCount` and `uploadQuota` first: the default of 1000
|
||
uploads or 50 MB per hour runs out.
|
||
- Email/import does not resolve a `#creationId` blobId from `Blob/upload` in the same request.
|
||
- **Emulator:** the server is `http://10.0.2.2:<port>`, which needs `android:usesCleartextTraffic`.
|
||
- Build from PowerShell in `C:\rn\android` with `NODE_PRESERVE_SYMLINKS=1`, JDK 17 and
|
||
`-PreactNativeArchitectures=x86_64`.
|
||
- For request traces, put a logging reverse proxy in front of the server. Pick a free local
|
||
port; 18096 and 18097 were already taken on this machine.
|
||
- **Incidental, webmail (suspected):**
|
||
- `lib/jmap/client.ts:3621` (and 3895, 4075, 4229, 8403, 8490) tests
|
||
`methodName.endsWith('/error')`. That never matches JMAP's `error` response, so a method-level
|
||
`EmailSubmission/set` error comes back as a successful send.
|
||
- `lib/web-push.ts:86-94` has the same shared-account `emailPush` problem as B18.
|