Commit Graph
2249 Commits
Author SHA1 Message Date
Linus Rath 88cd1e4cc4 fix: ask about a forgotten PIN on the lock screen itself, where it can be seen 2026-11-03 15:18:58 +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 bcd97d0d6e feat: merge offline contacts and files 2026-11-03 14:23:36 +01:00
Linus Rath 3cefd91dab fix: close the keystore bridge type the policies merge left open 2026-11-03 14:23:34 +01:00
Linus Rath 54852df738 test: stop a pinned download at the organisation limit when the size is missing 2026-11-03 14:12:19 +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 b9b31ebc5b Merge branch 'feat/desktop-offline' into feat/do-store-contacts-files
# Conflicts:
#	lib/store/area.ts
#	lib/store/engine/account-engine.ts
#	lib/store/engine/transport.ts
#	lib/store/files/index.ts
#	lib/store/files/keep.ts
2026-11-03 13:52:30 +01:00
Linus Rath 418bda4642 fix: stream kept files to the store, keep the old copy until the new one is in, and apply a tighter scope at once 2026-11-03 13:41:03 +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 9c36801f5e fix: keep composers mounted while the app lock saves them, and tell the service worker nothing with the lock off 2026-11-03 13:37:15 +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 832775a392 fix: render an encrypted message once per opening, and let its plugin keep external content off
A plugin's render can be a key operation, and mark-read replacing the
message object ran it twice: a second PIN dialog, and two cancels lock the
key. The effects now depend on what the hook sees and on the explicit
re-render nonce, not on the object. The list row and the full message still
differ, as a row has no headers for a plugin to recognise its mail by.

onRenderEmailBody can return `blockExternalContent: true` (flag
`api.host.features.blockExternalContent`) for a body it can't vouch for.
External content then stays off whatever the settings and trusted senders
allow, in the reader and in conversations, with no way to load it. The S/MIME
plugin uses it against EFAIL.
2026-11-03 13:24:10 +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 1c6720c3a8 test: give the wipe test a subject and count waiting changes per account
Both follow the review's fixes: a policy without a subject no longer decides,
and the fallback count of changes is per account.
2026-11-03 12:40:28 +01:00
Linus Rath 8d802e4601 fix: decide on offline policy only with the whole policy and the user's groups, and ask before losing edits
A persisted account id with no subject yet read as decided: Everyone's
noOffline wiped mail a group scope allowed, and a wider scope went out at
every reload. A public, partial policy counted as the organisation's word,
and turned on the desktop default in a forbidding organisation.

- an organisation's cap on one file now binds a pin
- Clear and the off switch read what would be lost when asked, messages,
  changes and drafts, and Clear waits for a connection
- turning one account off survives the last one going off, and off then on
  no longer warns of an eviction
- a tab that takes over the store sends no scope until it has heard the policy
- a dropped account is judged by what applies to anyone
- a choice reaches the store once it settles; progress announces phases only
- restored Pro tabs ask for their titles
2026-11-03 12:33:53 +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 8512e68d12 feat: merge offline drafts and kept attachments 2026-11-03 11:46:03 +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 e215423b5b fix: notice a lost store also when the first try to open it had no session yet 2026-11-03 11:24:39 +01:00
Linus Rath e98741d604 fix: say a store was reset also when a new owner took over the open, and slow raw downloads to one a second 2026-11-03 11:17:53 +01:00
Linus Rath fee9fec085 fix: say that the store was lost also when it opened before the page could show a notice 2026-11-03 11:06:19 +01:00
Linus Rath 333c758efa test: fail when the desktop repo's policy schema differs from the webmail's 2026-11-03 11:02:52 +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 d9c9fc03f7 feat: give the desktop app a native menu bar and native right-click menus built from the webmail's own 2026-11-03 10:44:03 +01:00
Linus Rath b2931ced3e feat: bring desktop windows back at the next start, open pop-outs from a warm spare and ask natively before discarding a draft 2026-11-03 10:44:02 +01:00
Linus Rath 60eef9c1ef chore: merge the reviewed mail branch, pop-outs and desktop core into the offline drafts branch
The flush keeps one `outbox.dropOrphans` caller, the mail branch's daily
one with its 7 days, and drafts' own query says what's needed, so
`addHeldFileSource` and its test are gone. `unsentCount` counts local
drafts, and a draft a waiting send names counts once. The scope check in
`recordBlob` and the permanent-status rule moved to
`lib/store/mail/attachments.ts`, with tests.
2026-11-03 10:40:52 +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 84c73ab15a test: let the store's own sync finish before failing the outbox lookup
With contacts and files in the store a pass runs longer, and the failure meant
for the lookup could go to the sync's own Email/query.
2026-11-03 10:19:28 +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 4fd82437ad fix: keep attachments alive through a flush and never destroy a copy that isn't ours
Review of the offline drafts found these.

- Every new message gets new part ids and old ones die with it, so an
  open composer failed with `blobNotFound` after a flush. The client and
  the flush re-read the draft and re-point parts once. The fake now
  gives new part ids, which is why `drafts.test.ts` and
  `drafts-client.test.ts` compare file contents instead of blob ids.
- A Message-ID alone could destroy another client's message. A copy now
  has to be solely in Drafts, with `$draft`, close to the attempt, with
  the same subject and size, and what the lookup saw is the destroy's
  base. Ghosts ride on the replacing row and only queue once it's up.
  `drafts.created` no longer queues a copy that arrived for a replaced
  row, so its test now expects the next flush to do it.
- Turning offline off asks about device-only drafts too.
- A kept file can't read back empty: chunks of a kept file are left
  alone, and `keptBlob` and `heldFile` refuse pieces that don't add up.
- A flush stopped by a passing failure tries again, the banner shows
  waiting drafts, and under the flush lock a `sending` row counts as
  left by a dead tab at once. A lookup refused for good no longer stops
  the outbox.
- `FetchTransport` streams downloads and reports a broken transfer as
  interrupted, so one bad file doesn't end the pass.
- Raw messages come after every file, only into free room, and a file
  needing room pushes them out first.
- An old version in another login stays until the new one is up. An
  online send of an unflushed stand-in still removes its submission.
2026-11-03 10:06:17 +01:00
Linus Rath e8696094e4 fix: open an account again in the owner tab after another tab cleared it 2026-11-03 10:05:43 +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 a2740fa9e9 feat: generate the desktop app's policy templates from one schema 2026-11-03 09:56:09 +01:00
Linus Rath 7dc4c775cd fix: don't wipe a store just because this window can't open it, and don't send a stand-in when a file's upload is held again 2026-11-03 09:45:03 +01:00
Linus Rath fb40944c2d feat: hold kept files to the scope at once, and count them towards what each account takes 2026-11-03 09:28:31 +01:00
Linus Rath ebec09212e fix: say plainly when a private window, old stranded mail or a silent server is the problem, and clean the domain of a legacy Message-ID 2026-11-03 09:25:07 +01:00
Linus Rath 024078c597 fix: clear files held for nothing a week after a flush, leaving what other parts of the store still hold 2026-11-03 09:25:06 +01:00
Linus Rath 3dc2fc2c3c fix: give up on a kept attachment only when the server says it won't come 2026-11-03 09:25:05 +01:00
Linus Rath f2aedd2948 chore: merge contacts and files into the settings branch 2026-11-03 09:22:04 +01:00
Linus Rath 340ad27e32 fix: send waiting mail when a client connects after the first flush, and come back to stale sends and servers that were down 2026-11-03 09:18:42 +01:00
Linus Rath 9b3bbc6070 fix: delete a store file whose key is gone, say that it happened, and stop waiting on an owner tab that doesn't answer 2026-11-03 09:18:41 +01:00