503 Commits
Author SHA1 Message Date
Linus Rath 724c3b80ee chore: update version number to 0.1.67 0.1.67 2026-09-27 10:15:08 +02:00
Linus Rath a27d014c59 fix: ask before a sign-in link from another app signs in
Any web page (one tap) or installed app (no tap) could fire a
`bulwarkmail://pair` link at the app, which then silently added the
sender's mailbox and switched to it, so the user might send or save mail
into an attacker's account. A `connect` link likewise opened any
server's sign-in page.

- Links that arrive from outside the app now ask first, naming the
  webmail's host and whether the link adds an account. Cancel discards
  the link for good; a link delivered again while the question is open
  is dropped. Codes scanned or pasted inside the app still go straight
  through.
- With the account registry full, a link shows the limit instead of
  opening "Add account".
- A pairing link for a plain-http webmail says the connection would be
  unencrypted instead of "That code isn't a Bulwark sign-in code", or
  nothing at all for a tapped link.
2026-09-27 05:11:07 +02:00
Linus Rath 26bb947940 fix: keep the account limit from leaving the app signed in as the wrong account
Signing in connects the shared client to the new account and stores its
credentials before the account registry takes it. With ten accounts the
registry refused, but nothing undid the connect: every request then went
out as the new account while the app still showed the old one. A
sign-in link made this easy to reach, since it opened "Add account"
without the limit check the menus have.

- A failed registration now restores the previous connection and drops
  credentials the registry never took.
- Adding an account to a full registry is refused before a sign-in code
  is spent or a browser sign-in starts, with "Remove an account in
  Settings to add another one."
2026-09-27 05:10:50 +02:00
Linus Rath 1131073323 feat: open sign-in links from the camera app and paste them into the app
The webmail's QR encodes a `bulwarkmail://pair?...` link, but nothing
registered that scheme: scanning it with the system camera or Google
Lens did nothing, and without a camera there was no way in at all.

- `bulwarkmail` is registered in `app.config.js` (iOS) and the committed
  AndroidManifest. Links are parked in a small store and picked up by
  the sign-in screen, or by "Add account" when already signed in, once
  per launch.
- "Paste sign-in link" on the sign-in screen (and next to the server
  step's scan hint) takes a link copied from the webmail.
- While pairing, the waiting screen names the webmail being paired with.
2026-09-27 04:23:01 +02:00
Linus Rath 59cfe50793 fix: say why a sign-in code failed and accept password pairing from the webmail
Every failed redeem read "That code has expired": a code already used,
a webmail without pairing (404), a rate limit, a server error and a
network failure all looked the same, and a non-JSON answer surfaced as a
raw parse error. The app now reads the webmail's `invalid_code`,
`expired_code` and `used_code` answers (webmail 1.11's catch-all 400
reads as "expired or already used"), gives up after 15 s, and names the
host involved; a failure after the code was redeemed says the phone was
signed in but could not reach the mail server.

- `loginViaPairing` accepts `flow: 'password'` bundles (an app password,
  or the account password from servers without OAuth) in normal and
  add-account mode, and never redeems the same code twice.
- A `pair` link must point at an https webmail (loopback http for
  development); `pair/?` is accepted too.
- A token refresh that answers 200 with a non-JSON body keeps the
  account instead of signing it out.
2026-09-27 04:22:33 +02:00
Linus Rath db07acc37c chore: update version number to 0.1.66 0.1.66 2026-09-27 00:34:53 +02:00
Linus Rath 39628d84dc fix: keep widget corners and border even when the launcher crops the widget 2026-09-27 00:34:29 +02:00
Linus Rath 78901f2375 chore: update version number to 0.1.65 0.1.65 2026-09-26 20:30:38 +02:00
Linus Rath a4985a63a8 fix: open replies over the message so widget links work offline 2026-09-26 20:25:21 +02:00
Linus Rath f7ec09942d chore: update version number to 0.1.64 0.1.64 2026-09-26 18:53:38 +02:00
Linus Rath c165fef588 feat: add android home-screen widgets for mail, calendar, tasks and files 2026-09-26 18:53:25 +02:00
Linus Rath 4052523ba9 ci: read the apk signer from newer apksigner output in the release check 2026-09-24 22:45:52 +02:00
Linus Rath e30749a50e ci: stop the android release failing on the runner's new sdk tools 2026-09-24 22:31:01 +02:00
Linus Rath 53c0bc0fc2 chore: update version number to 0.1.63 0.1.63 2026-09-24 22:18:42 +02:00
Linus Rath a8e74c8b6d Merge branch 'main' of https://github.com/bulwarkmail/native 2026-09-24 22:18:06 +02:00
Linus Rath 949bb10ac5 docs: record the device run, the r8 and label fixes and the push crash it found 2026-09-24 22:16:46 +02:00
Linus Rath e5659ed5e8 ci: attach the r8 mapping to android releases
Release APKs are obfuscated since R8 was turned on, and nothing kept
the mapping that decodes their stack traces. The workflow now copies
`android/app/build/outputs/mapping/release/mapping.txt` to
`bulwark-mobile-<version>-<commit>-mapping.txt`, uploads it as a
workflow artifact and attaches it to the release next to the APK. The
in-app updater only picks `.apk` assets, so it ignores the new file.

If R8 is on (`android.enableMinifyInReleaseBuilds=true`) and no mapping
was written, the step fails instead of publishing an APK whose crashes
cannot be read. docs/android-release.md explains how to decode a trace
with `retrace`.
2026-09-24 22:14:35 +02:00
Linus Rath a764a07c65 fix: show a white splash in light mode
The splash background was #09090b in both modes, so in light mode a
cold start went from a near-black splash to the white first frame.
It is now #ffffff by day and #09090b at night (`values-night`), as
`splash.backgroundColor` and `splash.dark.backgroundColor` in
app.config.js already ask for, so a later `expo prebuild` writes the
same colours. Checked on the API 35 emulator: light mode stays white
from splash to login screen, dark mode is unchanged.
2026-09-24 22:14:35 +02:00
Linus Rath 5ddc2df64b fix: stop holding a wake lock for 30 seconds after every push
Every background push kept the app awake for the full 30 s timeout of
the BulwarkPushTask headless task, however quickly the task was done.
AppRegistry reports a finished task through the native
HeadlessJsTaskSupport module, and React Native 0.81 registers that
module only for the old bridge (CoreModulesPackage); the bridgeless
runtime's CoreReactPackage leaves it out. JS could never say it was
done, so the service, or the job, and its wake lock waited for the
timeout. BulwarkHeadlessJsTaskSupportModule provides the module under
the same name when the app runs bridgeless.

The push path also leaked a wake lock now and then. startHeadlessTask
called HeadlessJsTaskService.acquireWakeLockNow on the FCM worker
thread while the service took the same static lock on the main thread.
The method is not thread-safe, so both could create a lock, and the one
that was overwritten stayed held. The FCM thread now takes its own
wake lock, timed at 10 s, and the service drops it as soon as it holds
React Native's.

Measured on the API 35 emulator with the R8 release build:

- push handled by the job: 30,000 ms before, 75-155 ms now
- push handled by the service: the service ran 31 s before, it is
  gone within a second now
- `com.facebook.react.i` wake lock still held 5 s after a push: 2 of
  4 pushes before, 0 of 8 now
2026-09-24 22:14:35 +02:00
Linus Rath fef23e4c33 fix: stop a background push from crashing the app on android 12 and later
A push that arrived while the app was in the background started
BulwarkPushTaskService with `startService`. Android 8+ allows that only
while the app is temporarily allowlisted, which a high-priority FCM
message grants. The relay does send FCM data messages at HIGH priority,
but FCM delivers some at normal priority anyway (it downgrades apps
whose high-priority messages rarely end in a notification), and a
UnifiedPush distributor may not raise the app at all. The call then
threw `BackgroundServiceStartNotAllowedException` and the app crashed,
from the FCM thread or, for UnifiedPush, on the main thread.

The service start is kept as the first choice, so a high-priority push
works as before. When Android refuses it, the same headless task now
runs from a JobScheduler job (BulwarkPushJobService): expedited on
Android 12+, which runs at once while the app has expedited-job quota
and as a regular job after that, and a regular job on Android 8-11.
JobScheduler is a platform API, so nothing is added to the build;
WorkManager is not in the dependency tree. A generic "new mail"
notification built natively was the other option, but the relay's
payload cannot tell new mail from other changes and the native side
knows neither the notification settings nor what was already shown.

Checked on the API 35 emulator with the R8 release build, the process
killed and the push delivered past the allowlist:

- FCM data message: before, FATAL EXCEPTION on
  Firebase-Messaging-Intent-Handle; now the job is scheduled 18 ms
  after the refusal and runs the task
- UnifiedPush message (connector 3.3.5, sent the way a distributor
  does): before, FATAL EXCEPTION on main; now the same job path
- allowlisted FCM message: still starts BulwarkPushTaskService
2026-09-24 22:14:35 +02:00
Linus Rath 4a3bce154c feat: translate the new app strings and screen-reader labels into all 26 languages 2026-09-24 22:06:28 +02:00
Linus Rath 13a3e95a0a chore: drop unused app strings and retranslate the ones that became plurals 2026-09-24 21:45:29 +02:00
Linus Rath fd55e16492 fix: translate the last export errors, dates and participant counts
A sweep of src for what the earlier passes missed:
- The message export errors ("Sharing is not available on this
  device", "Download failed: 404") were English; they come from the
  catalog now.
- Event cards and details glued the participant count to a lowercased
  "Participants"; they use the webmail's participant plural.
- Contact activity dates ignored the app language.
- Imported filter rules from an unknown tool showed the parser's
  English "External" instead of the translated label.
- The contact field remove button and the vacation editor's
  bold/italic/underline/list buttons get screen reader labels.
2026-09-24 21:44:47 +02:00
Linus Rath 5e557266d1 fix: count the files selection and delete prompt with plurals
The files screen filled `files.selected_count` and
`files.delete_items_title` with its own `{name}` substitution, a
leftover from before t() took arguments, so they could not be
plurals. Both are ICU plurals now and every message on the screen gets
its values through t(). The upload progress line is one message
instead of "Uploading..." followed by the numbers.
2026-09-24 21:44:47 +02:00
Linus Rath b178ee8d07 fix: translate the tab bar and label the unified inbox and drawer buttons
The bottom tabs showed their route names ("Mail", "Calendar", …) in
every language; they now use the webmail's navigation names, and an
unavailable tab says so with one translated message instead of an
English "(unavailable)" suffix. The first loading screen is
translated as well.

The Unified Inbox selection toolbar, its back and clear-search buttons,
and the drawer's close, expand/collapse and remove-account buttons get
screen reader labels. Its unknown-sender fallback, selection count and
"account(s) could not be loaded" banner are proper translations and
plurals now.

The key harvest and the translation coverage test did not look at
App.tsx, which sits next to src/; they do now.
2026-09-24 21:44:47 +02:00
Linus Rath 1cbf763a36 fix: send calendar reminders in the app language
Reminder notifications are worded when they are scheduled, outside
React, and were always English ("Starts in 15 min", "(No title)",
"Task due"). They now come from the catalog through the locale store,
with ICU plurals for the minutes, hours and days. Each scheduled
reminder records its language, so switching languages rewords the
pending ones, and the Android channel ("Calendar reminders", shown in
the system settings) is named and renamed in the app language.

The reminder labels picked the English form whenever a catalog entry
was an ICU plural, which is all of them; they now pass the count to
t(), and "minutes after" is one plural message instead of a number
glued to a fragment. The import refusal error, the exported alarm
text and the "sharing is not available" error are translated too.
2026-09-24 21:44:46 +02:00
Linus Rath 84fef142b9 fix: translate the calendar sheets and label their buttons
- The custom recurrence editor and its summary were English throughout
  (labels, units, weekday and month names, "on the third Thursday");
  they now use the webmail's recurrence fragments and the app's date
  locale. The interval and occurrence counts are ICU plurals, also in
  the event details, which filled "Every {count} days" with
  `.replace`.
- The iCal subscription sheet is translated with the webmail's
  subscription strings, and its "last updated" time is relative in the
  app language. The feed errors it shows come from the catalog too.
- The calendar share sheet asked for `calendar.share.role_*` keys no
  catalog has; it now uses the webmail's sharing presets and title.
- Close, remove, refresh, unsubscribe, delete and new-task buttons get
  screen reader labels, the event editor's switches are named, color
  swatches read out their color, and task checkboxes and calendar rows
  report whether they are checked.

The calendar i18n test now covers the sheets.
2026-09-24 21:44:46 +02:00
Linus Rath b3a82f1241 fix: translate the week view and label the calendar toolbar
The week view's "all-day" label and its "Untitled" fallback were
hard-coded; they now use the catalog's "All day" and "(No title)" like
the scrolling time grid. The calendar's menu, tasks, view switch, new
event and previous/next buttons get screen reader labels (the view
buttons also say which one is selected), and a calendar i18n test
covers the calendar views.
2026-09-24 21:44:46 +02:00
Linus Rath 13cba64854 fix: translate the viewer's leftovers and label its icon buttons
The message details panel built its received line, attachment summary
and spam score by gluing strings together; each is now one ICU message
(the attachment count is a proper plural). The SPF/DKIM/DMARC results
use the webmail's translated result names, and an invitation's date is
formatted in the app language instead of always in English.

The quick reply box wrote its own "date, name <email>:" quote header;
it now uses the composer's translated "On {date}, {from} wrote:" line
so both kinds of reply look the same.

The back button, the subject and header star buttons, the sender
avatar and the More/Tag sheet close buttons get screen reader labels,
and the screens i18n test covers the viewer files.
2026-09-24 21:44:46 +02:00
Linus Rath e3501f939d fix: offer undo for replies sent from the viewer
A quick reply held by the undo-send delay could not be undone: the
undo bar lived only on the mail list, and the quick reply never
recorded the held send for it.

- The undo bar is now hosted on every stack screen through the
  navigator's `screenLayout`, next to the toasts, and only the focused
  screen's renders, so it shows wherever the send started and never
  twice. The list's own instance is gone; archive, delete, move and
  spam undo use the same bar as before. The composer gets none, since
  its Undo reopens the composer.
- The quick reply records a held send through the same store action as
  the composer, including the account it went out of, so Undo and
  Send now reach a shared account's submission and Undo restores the
  reply into that account's Drafts.
- The quick reply dismisses the keyboard after sending, which would
  otherwise cover the bar.
2026-09-24 21:16:59 +02:00
Linus Rath 5e3cbf2419 fix: shrink the android release with r8
Release builds had R8 and resource shrinking off, so every class of
every dependency went into the APK unminified: the published 0.1.62 is
42.8 MB for arm64, 25.85 MB of it dex (PF9 in the audit).

Turn both on through the properties build.gradle already reads
(`android.enableMinifyInReleaseBuilds` and
`android.enableShrinkResourcesInReleaseBuilds`). The consumer rules of
React Native, Expo, Firebase, ML Kit and the other libraries cover what
they load by reflection. The app's own package is kept whole, which
costs a few KB and keeps its frames readable in an obfuscated stack
trace.

Measured on a local x86_64 release build:

- APK: 44.7 MB -> 38.0 MB (download size 23.8 MB -> 17.2 MB)
- dex: 30.4 MB in 4 files -> 11.3 MB in 2 files
- classes: 27,631 -> 16,724

With `-dontobfuscate` the APK would be 38.8 MB (13.5 MB of dex), so
obfuscation stays on. Library frames in a release stack trace are now
obfuscated; retrace them with that build's
`android/app/build/outputs/mapping/release/mapping.txt`.

Checked on the emulator: cold start in light and dark mode, German and
Russian strings, SENDTO and VIEW mailto and SEND share intents, every
sign-in screen, the QR scanner with ML Kit, and an FCM data message
starting the headless push task.
2026-09-24 21:13:42 +02:00
Linus Rath 9c031c8761 docs: update the fix-pass notes for the labels and strings that landed since 2026-09-24 21:02:09 +02:00
Linus Rath c81dc5179f docs: tick the parity items this pass completed
Tick the parity items the fix pass finished (server-side recurrence,
filters and vacation for shared accounts, in-app file preview, the
trusted-senders toggle, UnifiedPush, sender taps in the viewer), and
note on the already ticked ones what this pass changed, such as the tag
view and search reaching shared accounts or contact photos as avatars.
Items that only moved forward say what is left.

Fix hints and "verified" lines that named removed functions
(`setEmailKeywords`, `getEmailDetail`, `fetchEmailsChunked`,
`buildRecurrenceOverridePatch`) or the old whole-map keyword writes now
point at what replaced them.

PARITY_CHECKLIST.md counts done and open items per area: 379 of 415 are
done, 36 open.
2026-09-24 21:02:09 +02:00
Linus Rath d72aab2519 docs: correct the stale parity checklist entries
Apply the audit's "Stale parity checklist" section to docs/parity:
- ticked items the audit found broken keep their tick and name the
  commit that fixed them; the date-locale item is unticked again, since
  there is still no `dateLocale` setting
- the items that were done but unticked are ticked with where they
  landed
- natural-language event input, print contact and the Sieve editor
  gutter move to the N/A lists
- the text about linking, push types, RTL, `useTypography`, the Sieve
  "Keep" and `FileNode/get ids:null` now describes main
2026-09-24 21:02:09 +02:00
Linus Rath d4ac1e1df4 docs: record what still needs a device check and a decision after the fix pass
The audit gains a "Fix pass, 2026-09-24" section after the summary. It
says how the pass was done and tested, and lists what the fix agents
could not settle from the code alone:
- the device and server checks that matter before a release
- the product calls, starting with the release signing key and secrets
  that have to exist before the next version bump
- the follow-ups the agents left, one line each
2026-09-24 21:02:08 +02:00
Linus Rath 06ce86e31a docs: tick the audit items fixed in this pass
Every audit item a commit on main fixed is ticked with the commit that
fixed it, including the "fix these ten first" list and the stale parity
entries that the fixes resolved. Items fixed only in part keep their box
and say what is left; U8, PF9, B2 and the product calls stay open.
2026-09-24 21:02:08 +02:00
Linus Rath 6fe907b4bc fix: translate the Themes and Updates settings tab names 2026-09-24 20:49:56 +02:00
Linus Rath c1e5cd6c8b fix: label the remaining settings icon buttons for screen readers
The filter rule reorder, delete and remove-condition/action buttons,
the filter, Sieve and folder editor close buttons and the attachment
keyword remove buttons had no label. The Sieve editor's line count now
uses an ICU plural, and the settings i18n test covers every pane.
2026-09-24 20:49:56 +02:00
Linus Rath 455608f9f0 fix: translate push setup errors and the open-link prompt
The Notifications pane shows a push setup failure as "<phase>: <message>"
with a translated phase but an English message, and the open-link
confirmation and the new-mail notification title were English too.
They now go through the locale store's t() (translate() in the
background task), with the technical detail passed as an argument.
2026-09-24 20:49:55 +02:00
Linus Rath 5801d6c97a fix: show a translated notice when the sign-in session expired
The sign-in screen showed the auth store's raw English error ("Session
expired", "Invalid username or password") as the notice title. It now
runs it through describeLoginError like every other sign-in failure,
which also learns to explain an expired session with the webmail's
login.session_expired copy. The back and close buttons of the sign-in
steps get screen reader labels, and the email placeholders use the
webmail's translated example address.
2026-09-24 20:49:55 +02:00
Linus Rath 771558d8c7 fix: translate the composer's leftovers and label its header buttons
Group recipient chips, the group size in suggestions, the upload
progress and the upload/send failure fallbacks were English or glued
together from a translated word and a number; they now use ICU
messages. The close, attach, schedule and sender-options buttons, the
recipient and attachment remove buttons and the From override switch
get screen reader labels (audit U8).
2026-09-24 20:49:55 +02:00
Linus Rath 9c0c35c7ba fix: translate the mail list and label its icon buttons
The filter sheet, the .eml import result, the selection count, the
folder totals and the offline sync banner were English literals or
built by gluing translated words to numbers. They now use t() with ICU
arguments and plurals; filter chips go through a "{field}: {value}"
message so translators control the order. The invitation trust
warnings used keys that no catalog had, so they are now literal t()
calls and harvested.

For screen readers (audit U8) the hamburger, select-all checkbox,
filter, sort and compose buttons, the selection toolbar, search and
chip clear buttons, and the close buttons of the move, tag, template,
identity, QR and action sheets get translated labels and roles.
2026-09-24 20:49:55 +02:00
Linus Rath ccdc4dee56 fix: add the template settings strings to the catalog
TemplateSettings calls the translate function `tr` because `t` is a
template variable there, and both the key harvester and the coverage
test only looked for `t('...')`. Ten of its keys were never added to
locales/rn/en.json, so they could not be translated and the test did
not notice. Both now also match `tr('...')`, and the missing keys are
added.
2026-09-24 20:49:55 +02:00
Linus Rath aab29da148 fix: translate the file sharing roles and label the files toolbar
The share sheet asked for files.share_role_* keys that no catalog has,
so Viewer, Editor and Manager stayed English everywhere; it now uses
the webmail's sharing.preset names and its "Share \"{name}\"" title.
The per-file menu, back and clear-selection buttons get screen reader
labels, and the "sharing is not available" error is translated.
2026-09-24 20:49:55 +02:00
Linus Rath d59c571d93 fix: translate the contacts screens
The contact list, detail and edit screens, the group screen, the
import, picker and tag sheets and the contacts drawer were written in
English, including the activity panel ("Unknown", "Upcoming events"),
alert titles and every form label and option. They now go through
t(), reusing the webmail's contacts.* keys where the meaning matches,
with ICU plurals for member, contact and import counts instead of
`contact${n === 1 ? '' : 's'}`. JSContact context, phone feature,
personal-info and gender values on the detail screen are shown with
their translated labels instead of the raw keys.

Icon-only buttons on these screens (back, edit, more, bulk actions,
remove, close, clear search) get screen reader labels, and chips,
checkboxes and collapsible sections report their state.
2026-09-24 20:49:55 +02:00
Linus Rath cb5c152cb3 fix: name the settings switches for screen readers
A settings switch had a role and a checked state but no label, so
TalkBack only announced "switch, off". A switch inside a SettingItem now
takes that row's label, and the switches outside one (filter rules,
expanded view, template and sidebar app editors, filter rule options)
get their visible label passed explicitly.
2026-09-24 20:49:55 +02:00
Linus Rath 6e6fd95e5e fix: translate the contacts and content settings
The Contacts and Content & Senders panes, the swipe behaviour row in
Layout, the device time zone option in Calendar and the identity editor
placeholders were written as English literals, so they stayed English
in every language. They now go through t(), reusing the webmail's
settings.contacts, contacts.address_books and
settings.email_behavior.trusted_senders keys where the wording matches.
Counts use ICU plurals instead of `contact${n === 1 ? '' : 's'}`, and the
address book icon buttons get screen reader labels.

The settings i18n test now parses each pane with the TypeScript compiler
and also flags JSX text, alerts and accessibility labels, and covers
the newly cleaned panes.
2026-09-24 20:49:55 +02:00
Linus Rath 8af163a6a3 fix: show today and the now-line in the calendar time zone
The calendar placed events by the calendar time-zone setting, but
"today", the now-line and the "Today" button still went by the device
clock. With the setting away from the device zone, the grid could
mark yesterday as today, draw the now-line hours off and jump to the
wrong day.

- the views, the agenda's "Today"/"Tomorrow" headers and today anchor,
  the selected day, the scroll window's start and the "Today" button
  use `displayNow`, `isDisplayToday`, `isDisplayTomorrow` and
  `displayNowMinutes`
- a long press at an hour already hands the editor a calendar-zone
  time, which it converts when it saves
- changing the setting re-buckets the days and reloads the loaded
  range, since the server reads floating times and the range bounds
  in that zone
2026-09-24 20:40:51 +02:00
Linus Rath b7f711050e fix: translate the calendar import sheet 2026-09-24 20:34:35 +02:00
Linus Rath 9282ee6d72 fix: open the event a calendar link points at 2026-09-24 20:34:35 +02:00