Commit Graph
1405 Commits
Author SHA1 Message Date
Linus Rath ac2f7cba2b test: allow keystore as the one bare plugin permission, since it has no actions and the S/MIME manifest uses the name 2026-11-03 19:49:56 +01:00
Linus Rath 1e9afb733a fix: let the Docker image build see the script its Next config imports 2026-11-03 19:19:35 +01:00
Linus Rath 5849f9dc42 Merge branch 'main' into feat/desktop-offline 2026-11-03 19:07:04 +01:00
Linus Rath 151cedf992 Merge branch 'feat/do-app-lock' into feat/desktop-offline 2026-11-03 18:46:05 +01:00
Linus Rath deb73cc6e2 Merge branch 'feat/do-native-ui' into feat/desktop-offline 2026-11-03 18:28:49 +01:00
Linus Rath 335e1edc10 Merge branch 'feat/desktop-offline' into feat/do-app-lock
# Conflicts:
#	components/settings/general-settings.tsx
#	hooks/use-calendar-alerts.ts
#	lib/desktop/app-lock-state.ts
#	lib/desktop/policy-schema.ts
#	lib/desktop/types.ts
2026-11-03 17:52:30 +01:00
Linus Rath 3d5b02b75f fix: stop a page reporting once consent goes, and let nothing on disk aim the desktop app's telemetry
A review found that an open page kept sending to a DSN after consent was
taken back, that a state file or config file could name where telemetry
goes, and that `BULWARK_TELEMETRY=off` did nothing on the desktop build.
A due heartbeat now waits a random 30 s to 10 min, a refusal from the
collector isn't retried, crash files go at sign-out and account switch,
and server builds parse stacks as they did.

Existing assertions changed in the desktop telemetry tests: the catch-up
wait is now a range, the endpoint comes from the shell, and the platform
is `desktop` against the collector's patched branch.
2026-11-03 17:38:04 +01:00
Linus Rath 3f264367af fix: harden the app lock: forget in an order that can't leave a sign-in without a PIN, hear wipes and clear subjects while locked, and refuse obvious PINs 2026-11-03 17:08:02 +01:00
Linus Rath 383e579eec feat: run the app to the top of the window, with the window's controls in its own toolbars
The desktop app and an installed PWA had a row across the whole window
with the title and the window controls. That row is gone. The rail,
sidebar, list and reader run to the top of the window in their own
colours, and the controls sit in the top row of whatever is rightmost.

- `lib/window-controls-overlay.ts` finds and marks those rows before
  paint, and they reserve the controls' width through
  `--titlebar-right-inset`
- the rows and the rail are the drag area, everything clickable is
  `no-drag`, and an invisible band lets a page with no row still move
- settings and the admin shell get a row only under the overlay, Pro's
  tab strip and Mountain View's bar match the controls' height
- Alt floats the menu over the top left, in `window-menu-bar.tsx`
- skeletons carry the same marks, so nothing shifts when the app loads
2026-11-03 16:52:42 +01:00
Linus Rath 03cdf69bf2 feat: let desktop users opt in to usage info and crash reports, each with its own switch 2026-11-03 15:55:17 +01:00
Linus Rath fbda1e3401 fix: keep the Docker build within Node's default heap #1510 2026-11-03 15:40:09 +01:00
Linus Rath 306b249c07 chore: merge the offline and desktop base into the app lock branch 2026-11-03 14:40:25 +01:00
Linus Rath 7aac946a9d feat: merge managed deployment policies and enforcement
# Conflicts:
#	lib/desktop/types.ts
#	locales/ar/common.json
#	locales/ca/common.json
#	locales/cs/common.json
#	locales/da/common.json
#	locales/de/common.json
#	locales/en/common.json
#	locales/es/common.json
#	locales/et/common.json
#	locales/fa/common.json
#	locales/fr/common.json
#	locales/he/common.json
#	locales/hu/common.json
#	locales/it/common.json
#	locales/ja/common.json
#	locales/ko/common.json
#	locales/lv/common.json
#	locales/mn/common.json
#	locales/nb/common.json
#	locales/nl/common.json
#	locales/pl/common.json
#	locales/pt/common.json
#	locales/ro/common.json
#	locales/ru/common.json
#	locales/sk/common.json
#	locales/th/common.json
#	locales/tr/common.json
#	locales/uk/common.json
#	locales/vi/common.json
#	locales/zh-TW/common.json
#	locales/zh/common.json
2026-11-03 14:08:20 +01:00
Linus Rath 52a2b458a0 feat: merge decrypting once per message and keeping external content off unverified mail 2026-11-03 13:38:40 +01:00
Linus Rath a74543c137 feat: take back certificate grants at sign-out and account switch 2026-11-03 13:24:11 +01:00
Linus Rath fb3c82eed5 feat: lock the app with a PIN, a passkey or Windows Hello, with mail out of memory and the local store closed
The lock is off by default, and an OS policy can require it and set the
timeout. It locks after a time without use counted across all windows and
tabs, on OS screen lock in the desktop app, and from Lock now.

Locking saves open composers, stops the outbox at its next message, closes
the store and stops its worker with every key, then reloads the page, so
nothing of the old page stays in reach. The new page holds the app back at a
Suspense boundary until someone unlocks. Pop-outs get no session while
locked, push notifications name nobody, and subjects leave plain storage.

One random lock key wraps each account's key as a layer. The PIN wraps the
lock key with PBKDF2 at 600,000 iterations, with a wait after wrong tries
that a reload doesn't reset. A passkey with PRF and the desktop's OS storage
wrap it too. Details and limits are in the decisions file.
2026-11-03 13:08:37 +01:00
Linus Rath e20aded171 fix: keep restored desktop composers off sent mail and on their own From, check links from the native menu, and fetch no dictionary without asking
A restored composer opened whatever the id named. Sending keeps the id, so its autosave would have destroyed the Sent copy, Send mailed it twice and Discard deleted it. It now needs the login in its address and a real draft, keeps its From with several accounts, and stays on the list if the server doesn't answer. A message from a bulwark:// link no longer marks itself read or sends a receipt. Native menus speak all 30 languages. Spellcheck dictionaries come from Google only after a yes, never in no-third-party mode.
2026-11-03 12:53:40 +01:00
Linus Rath 426078e6ef Merge branch 'feat/desktop-offline' into feat/do-native-ui 2026-11-03 12:26:59 +01:00
Linus Rath 2edd6b5ea7 fix: hold forced policies against spoofed plugins, recommended values and stopped servers
A forced value could be got around from inside the app. Only a forced
value may now decide what's allowed: the keys that do are forced-only,
so a recommended one no longer acts as forced. With a plugin list a
package can name any id, so uploads are refused and only managed
plugins can match. A server taken off AllowedJmapServers stops its
accounts without signing out: client, push and sync stop, the switcher
drops them, restore and refresh answer 403 with the cookie kept, and
listing it again resumes with no sign-in. OAuth and SSO starts check
the list too.

- `policy.onWipeOfflineData` is gone from the bridge: main runs the wipe
- `KeystoreAllowedOrigins` added, and `POLICY_CONSUMERS` names who acts
  on each key or who still owes it
- a Bulwark server's name must be plain after parsing, a wildcard in the
  auth lists needs a domain of two labels
2026-11-03 12:06:21 +01:00
Linus Rath b635986506 chore: merge drafts and kept attachments into the settings branch
One attachment scope: the kept-attachments code, which takes chunks with a
file and spares pinned mail. A draft only on this device stays through a
tightened window. Mail's budget still gives way to what Files keeps.
2026-11-03 12:04:47 +01:00
Linus Rath aebce87776 Merge branch 'feat/do-native-ui' into feat/do-app-lock 2026-11-03 12:01:57 +01:00
Linus Rath efa6fa4ae4 Merge branch 'feat/do-store-settings' into feat/do-policies 2026-11-03 11:44:34 +01:00
Linus Rath 2c08b10fef feat: merge the plugin keystore capability 2026-11-03 11:41:30 +01:00
Linus Rath fb6ff162ff Merge branch 'feat/desktop-offline' into feat/do-policies 2026-11-03 11:40:53 +01:00
Linus Rath 1d4587f7ea fix: stop a stuck certificate call holding mail rendering, and let a server's certificate access be taken back 2026-11-03 11:31:14 +01:00
Linus Rath 4044351611 test: keep what was chosen for a store that waits for a new sign-in 2026-11-03 10:51:36 +01:00
Linus Rath fb0eeca9c5 fix: put the focus on the desktop menu strip once its names are on screen 2026-11-03 10:49:25 +01:00
Linus Rath fcf31df928 fix: keep one policy object per change so the login page renders in the desktop app 2026-11-03 10:45:45 +01:00
Linus Rath e0abc7fa18 chore: merge the reviewed store, pop-outs and desktop core into the settings branch
Eviction now goes by the base's signal: `storeLost` and the `store-reset`
notice, with `bulwark-store-opened` as the marker and `prune-files` for files
with no key. The storage page and the lost count hang off that. What a user
picks is stored under `bulwark-offline-scope`, the key `offlineScopeFor`
reads, and `email-snapshot` follows the base's rule. `OfflineEnabled` is read
from the desktop bridge when it's there.
2026-11-03 10:40:47 +01:00
Linus Rath eb365938a2 test: line the merged branches' tests up with the store's wipe rules
The token route now answers 503 when the provider can't be reached, in every build, and a refused renewal for a background account now removes it as evicted, which keeps its data unless the server said invalid_grant. Two assertions from the desktop and pop-out branches change for that. Also drops a duplicate import from the same merge.
2026-11-03 10:26:00 +01:00
Linus Rath 6e262bf1c5 feat: move offline mode onto the encrypted store, with a merged outbox and conflicts
# Conflicts:
#	app/api/auth/token/route.ts
#	lib/offline/offline-mode.ts
#	stores/account-store.ts
#	stores/auth-store.ts
2026-11-03 10:21:34 +01:00
Linus Rath a9b6a05841 feat: enforce the desktop app's OS policies for plugins, mail servers and offline mail 2026-11-03 10:15:15 +01:00
Linus Rath 2db3c4b217 feat: let privileged plugins sign and decrypt with the OS certificate store in the desktop app 2026-11-03 09:58:48 +01:00
Linus Rath b001cbf109 feat: show what each account keeps on the device and let it be changed and cleared
The offline section of Settings > Backup & reset now shows, per account, the
space each part takes, the last sync, a first sync's progress with the time
left, the window and attachment choices, and clearing the account. All
conflicts, calendar and contacts too, are listed there and counted in the
banner.

- the organisation's caps beat the user's choice and the desktop default, and
  every path that takes a scope goes through `effectiveScope`
- the desktop app keeps mail without asking, says so once, and stays off for
  an account where the user turned it off
- `navigator.storage.persist()` is asked on turning it on, and a refusal
  warns with a way to ask again; Safari gets the 7-day hint when not installed
- a store the browser deleted is noticed, says what was lost and syncs again
- a full disk says so, with what to do
- those who had offline mode keep it, with the 30 days they had
- Keep offline in the Files app's menu
2026-11-03 08:57:41 +01:00
Linus Rath 3b1fa089aa test: give the stand-in desktop bridge its version and platform 2026-11-03 08:54:05 +01:00
Linus Rath 2b48047bcc feat: merge the webmail side of the desktop app
# Conflicts:
#	lib/desktop/types.ts
#	locales/ar/common.json
#	locales/ca/common.json
#	locales/cs/common.json
#	locales/da/common.json
#	locales/de/common.json
#	locales/en/common.json
#	locales/es/common.json
#	locales/et/common.json
#	locales/fa/common.json
#	locales/fr/common.json
#	locales/he/common.json
#	locales/hu/common.json
#	locales/it/common.json
#	locales/ja/common.json
#	locales/ko/common.json
#	locales/lv/common.json
#	locales/mn/common.json
#	locales/nb/common.json
#	locales/nl/common.json
#	locales/pl/common.json
#	locales/pt/common.json
#	locales/ro/common.json
#	locales/ru/common.json
#	locales/sk/common.json
#	locales/th/common.json
#	locales/tr/common.json
#	locales/uk/common.json
#	locales/vi/common.json
#	locales/zh-TW/common.json
#	locales/zh/common.json
#	stores/account-store.ts
2026-11-03 08:48:10 +01:00
Linus Rath b79781cc94 fix: never lose a draft moving between windows and keep credentials off the window channel
Review of the pop-out work found ways to lose text, a wrong sign-out and
changes reaching ordinary browser tabs.

- Drafts: a closing window hands text over only to a main window holding
  its login, and asks instead when the last save failed, the browser is
  offline, a save is running or plugins could still have a say. A failed
  rescue goes to "Continue draft". The opening window lets go only once
  the composer is on screen, also for Edit draft in a message window.
  Send options travel with the draft. Closing during a send asks first.
- Sessions: answers are encrypted for the window that asked, and only a
  window holding the login answers. A renewal queued on the lock can be
  called off by a sign-out, signs nobody out once its account is gone, and
  takes the cached token after waiting (Rauthy `nbf`, #552).
- Ordinary tabs run none of the host. No lock is held for life.
- A message window hides after a refused session and waits for its
  folders; a composer stays on screen with its text. Another tab dropping a
  login it couldn't restore no longer closes pop-outs.
- Plain-text mail stays selectable in the desktop app. Folder save renames
  reserved and shell names and skips one refused name. All display-mode
  checks know the overlay. The attachment bar keeps to one row.

The earlier test that accepted text being gone after a failed rescue now
asserts that it reaches the "Continue draft" slot.
2026-11-03 08:37:10 +01:00
Linus Rath 706550daf4 fix: keep local mail and the outbox when an automatic sign-out wasn't a real refusal of the login, for 30 days at most 2026-11-03 08:33:03 +01:00
Linus Rath c76ba0fee2 feat: let organisations cap what devices keep offline 2026-11-03 08:11:43 +01:00
Linus Rath 46766ac8aa fix: let a server build pass its guard, and let the desktop app save drafts and forget a server on sign-out 2026-11-03 07:57:35 +01:00
Linus Rath a365ba9041 Merge branch 'feat/desktop-offline' into feat/do-desktop-core 2026-11-03 07:34:32 +01:00
Linus Rath dccf4e8b9f chore: merge feat/desktop-offline into the windows branch 2026-11-03 07:31:32 +01:00
Linus Rath b1c4bb00d9 chore: merge opening files and sharing into the mail branch 2026-11-03 07:30:08 +01:00
Linus Rath d6ae3d172b feat: merge opening files and sharing into the installed app
# Conflicts:
#	components/mail/mail-app.tsx
#	lib/__tests__/sw-harness.ts
2026-11-03 07:11:39 +01:00
Linus Rath f716ea66ee fix: ask before a share starts a message and save nothing until the first edit
Any site could POST a form to the share target on load: with no clicks
the composer opened with its subject and file, and autosave stored a
real draft on the server 2 seconds later. The worker can't see where a
POST came from, and Chromium's link capturing accepts form submissions,
so an installed-app window proves nothing either.

- every share landing now asks first, in every window and browser; a
  text landing address gets the same question. Nothing goes up before
  the answer.
- handed-in text and files are the composer's starting state: no
  autosave, page-close save or sign-out flush before the first real
  edit.
- the worker ignores frames, keeps at most 5 entries and 100 MB for 15
  minutes, sweeps on activate, on each share and from a page timer,
  and seals what it keeps with a key passed in the landing fragment.
  Offline wipes clear it too.
- the server fallback uses the fragment and caps by encoded length:
  3000 Japanese characters made a 27 KB `Location` and a 431.
- a second compose handoff waits until the composer closes, also one
  set aside by reading a message, where it used to replace the first.
- a launch leaving the mail page saves what was just typed first.
- an expired handoff or a missing stored file now says so; shares get
  the control-character stripping and limits of `parseMailto`.

Existing tests changed because the landing moved from the query to the
fragment, stored entries gained a sealed part, and a share now asks
first.
2026-11-03 07:08:38 +01:00
Linus Rath 20b2e7054e chore: merge search, notification actions and calendar into the mail branch 2026-11-03 07:02:39 +01:00
Linus Rath 7ac582f803 fix: compile the desktop trust path out of every other build, and fail a build that keeps it 2026-11-03 06:55:22 +01:00
Linus Rath bb2393cf04 feat: open messages and drafts in their own windows and save all attachments to a folder
An installed app had no title bar of its own, no way to save attachments
except a zip, and "pop out" stayed inside the page. A new window was a
second copy of the app that had to restore the session itself, and on
start-up that evicts any login made without "Remember me" from the list
every window shares.

- Title bar: `display_override` with `window-controls-overlay`, one
  `--titlebar-inset` variable at the page roots and skeletons, and a drag
  strip with the window title. 0 without an overlay, so nothing moves.
- Save all to a folder: `showDirectoryPicker`, asking the device policy
  before the picker and before each file, with the zip as the fallback.
- Pop-out routes `/popout/message/<id>` and `/popout/compose`, on offer in
  an installed app or when the desktop bridge has windows. Hidden in Lite.
- A pop-out borrows the main window's session and never evicts or starts a
  token timer. Every renewal now takes a Web Lock, so two windows can't
  send the same refresh token, which Stalwart 1.0 may treat as a replay.
- A draft moves to its window under the same id. A closing window hands
  unsaved text to the main window to save, since its own request wouldn't
  survive the close.
- Read state, drafts, theme and language follow across windows, and
  `html[data-desktop]` gates the app-like behaviour in `app/desktop.css`.
2026-11-03 06:50:21 +01:00
Linus Rath a30f989c57 feat: add a desktop build of the webmail that no server build can switch on 2026-11-03 06:32:27 +01:00
Linus Rath 51c09a1a69 fix: never let a notification's buttons go round plugins, session limits or Trash
Review of the notification actions. With a plugin that may stop a move
or read change, or a silent tab holding the login, the message now
opens instead of the route acting. The route answers 401 under a
session limit, and neither path touches a message since moved to Trash
or Junk. A tab that thawed declines an old click, Archive follows the
gate of the notification's login and files into a shared-out Archive
as the app's button does, Reply opens a composer in the Pro shell, the
route retries once when another client made the folder first and
leaves no empty folder behind, and logins match however the server
address is written.

Two assertions of this feature's own tests change: the route answers
401, not 403, when a login's scopes can't be worked out, since that now
also covers session limits; a tab's message to the page now carries
`at`.

The Pro shell's mail surface now reads a parked link's query from the
handoff: the address bar may have none by its first render, which lost
`?slot=` and `?owner=` there too.
2026-11-03 06:28:04 +01:00