Commit Graph
116 Commits
Author SHA1 Message Date
Linus Rath 2f46eb6b46 Merge branch 'feat/do-menu-fixes' into feat/do-drop-overlay 2026-11-04 06:20:02 +01:00
Linus Rath a80c44b776 Merge branch 'feat/desktop-offline' into feat/do-drop-overlay 2026-11-04 06:19:39 +01:00
Linus Rath 277e1aa55d Merge branch 'feat/do-menu-altbar' into feat/do-menu-fixes 2026-11-04 06:19:29 +01:00
Linus Rath 8bcf5248a4 fix: translate Lock now in the desktop File menu 2026-11-04 06:17:12 +01:00
Linus Rath 6efa589448 Merge branch 'feat/do-menu-pos' into feat/do-menu-fixes 2026-11-04 06:12:08 +01:00
Linus Rath 1cf03ff0e4 feat: add a developer tools switch to the desktop app's troubleshooting settings 2026-11-04 05:38:59 +01:00
Linus Rath 1f4b0669ca feat: ask between a new message and Files for a file opened with the desktop app 2026-11-04 05:38:43 +01:00
Linus Rath e211ae905e feat: offer attach or upload when files are dragged onto any screen
Files dragged into the window from outside now bring up a full-window
overlay with large drop zones: attach to a new message and upload to
Files, with the open composer first, "Upload to <folder>" on the Files
screen, and add to calendar, add to contacts, open or import for .ics,
.vcf and .eml files. At most four zones, the one under the pointer
highlights, Esc or dragging out cancels, a drop beside the zones does
nothing. Drags that start in the app never show it. A file pasted
outside a text field gets the same choices as a keyboard dialog.

The desktop app used to turn any file dropped on the window into a new
draft (lib/desktop/window-files.ts). That handling moved to the
overlay, so the "files on the window" tests in os-window.test.tsx went
with it: the same cases are now in components/file-drop/__tests__.
2026-11-04 05:37:35 +01:00
Linus Rath 4d404929e2 fix: show the message's own folders in Move to, in mail-client order, with recents and a filter
The Move lists (message menu, reader Move button and its phone panel,
selection toolbar, Move dialog) now share one model in lib/move-menu.ts.
In the desktop app the native copy of the menu lost the shared accounts'
headers and every folder's nesting, so Inbox, Sent, Spam and Trash
showed once per account with nothing to tell them apart.

- Only the message's own account at the top; other accounts a move can
  reach are submenus under their names.
- System folders first (Inbox, Sent, Archive, Spam, Trash), then the
  user's folders by name; subfolders nest, as submenus natively.
- The open folder shows greyed instead of missing.
- A Recent group, a filter field on long lists, and "Choose folder…"
  for the dialog, which has the filter too.

Tests changed: the open folder is now shown disabled rather than left
out, the own account of a shared-mailbox message is a submenu, and the
selection toolbar's filter field has its own label.
2026-11-04 05:20:14 +01:00
Linus Rath ff0e16c755 feat: show the desktop app's menu bar as a row at the top on Alt, or always 2026-11-04 05:20:02 +01:00
Linus Rath 3fa82849a8 fix: open menus next to what opened them and show their keys apart from the label 2026-11-04 05:00:02 +01:00
Linus Rath 0aa0f21955 Merge branch 'feat/desktop-offline' into feat/do-key-fixes
# Conflicts:
#	lib/desktop/types.ts
2026-11-04 04:53:26 +01:00
Linus Rath ce3e3b9db2 fix: let a remote wipe and a wipe from settings reach what only the desktop app's main process can delete 2026-11-04 04:47:38 +01:00
Linus Rath f9503570ca Merge branch 'feat/desktop-offline' into feat/do-key-fixes
# Conflicts:
#	components/settings/app-lock-settings.tsx
#	stores/auth-store.ts
2026-11-04 04:16:14 +01:00
Linus Rath 88d64b7f95 fix: protect the keys to the mail on this device for real
The review found the store key readable from a copy of the disk. The
desktop app only added its OS layer with the lock on, the record from
before a lock stayed in LevelDB and opened the mail with no PIN, any
frame could unwrap the PIN record and guess at it, a lock left the key
in the renderer's memory, and a store waiting after a refused login went
to whoever signed in next under the same username@host.

- The desktop app wraps the store key with safeStorage, lock or no lock.
  A record without the layer gets it at its next open. A refusal is said
  out loud, and only a computer with no key store goes without, which
  settings say. So do they for a browser, which has no such layer.
- A record that gained layers, and every record after a changed PIN, asks
  for a new key: `sqlite3_backup` into a new file under a new key while
  the store keeps working, one write to the record, then the old file
  goes. A changed PIN makes a new lock key, since the old PIN's record
  stays on disk too. The passkey and OS unlock go with the old key.
- `wrap` and `unwrap` come through `claim()`, and the key under the PIN
  takes a secret from main, one counted trip per guess.
- A lock on the desktop ends the renderer rather than reload in it.
- A sign-in compares the waiting store's JMAP account and server first,
  and wipes on a mismatch. The account id keeps its form: every store,
  setting and server record of an account is keyed by it.
- The 30 days also run in an app that stays open.

Changes an existing assertion: turning the lock off now keeps the
safeStorage layer, by decision, so that test expects it to stay.
2026-11-04 04:15:31 +01:00
Linus Rath c49c68ffbd Merge branch 'feat/desktop-offline' into feat/do-proxy-fixes 2026-11-04 04:12:23 +01:00
Linus Rath 8cd9d72558 Merge branch 'feat/desktop-offline' into feat/do-proxy-fixes 2026-11-04 03:55:52 +01:00
Linus Rath bce0c0b744 Merge branch 'feat/desktop-offline' into feat/do-keystore-fixes 2026-11-04 03:52:09 +01:00
Linus Rath 92e6ee1bfa Merge branch 'feat/do-trust-fixes' into feat/desktop-offline 2026-11-04 03:43:04 +01:00
Linus Rath d3308e7729 Merge branch 'feat/desktop-offline' into feat/do-keystore-fixes
# Conflicts:
#	lib/desktop/__tests__/policy-schema.test.ts
2026-11-04 03:31:22 +01:00
Linus Rath 66c6b61853 fix: keep downloaded mail out of the browser's HTTP cache 2026-11-04 03:31:16 +01:00
Linus Rath 9b61b79d5c fix: sign only CMS signed attributes through api.keystore, and ask a user about a managed plugin's keystore
The shell now takes a message's signed attributes where it took a digest,
so a page can't get a login or TLS digest signed. A quiet option lets a
plugin answer from what's allowed, for anything the user didn't ask for.

An admin's install no longer covers the certificates on someone's own
computer: a managed plugin gets `keystore` when that person says yes, also
when an update adds it, and it's forgotten once everyone has signed out.

Changed test assertions: the fixture plugin needs a grant now, as managed
alone is no longer enough, and the bridge gets an options argument.
2026-11-04 03:23:45 +01:00
Linus Rath b1f26a93bf feat: let an organisation turn the system certificate store off by policy 2026-11-04 03:23:44 +01:00
Linus Rath 484ce34f5d Merge branch 'feat/do-browser-signin' into feat/desktop-offline 2026-11-04 03:19:32 +01:00
Linus Rath cb9a5ac810 Merge branch 'feat/do-edit-attachments' into feat/desktop-offline 2026-11-04 03:16:35 +01:00
Linus Rath 466fa7f974 fix: keep the desktop app's session secret and admin password out of its server's environment 2026-11-04 03:15:42 +01:00
Linus Rath 37d0200e43 Merge branch 'feat/desktop-offline' into feat/do-browser-signin 2026-11-04 03:11:34 +01:00
Linus Rath a048cb3553 fix: stop an approved mail server sending the desktop app to a server nobody approved 2026-11-04 03:10:26 +01:00
Linus Rath 7f50113b90 fix: ask to clear a desktop app lock this profile has no PIN for 2026-11-04 02:57:04 +01:00
Linus Rath 7f3d9c5073 fix: let npm run build pass its guard, with the build run in CI 2026-11-04 02:45:46 +01:00
Linus Rath 97c0bd633a Merge branch 'feat/desktop-offline' into feat/do-edit-attachments
# Conflicts:
#	lib/desktop/languages.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-04 02:40:47 +01:00
Linus Rath ab21bf2df3 fix: keep a desktop sign-in in the browser's page until the user says yes
The hand-off parked a password before its question, for up to five
minutes, and on a shared computer another local user could ask for
someone's sign-in with a link of their own. Now nothing leaves the page
before the yes, and the instance has a switch for such machines.

- `/check` tests a password and keeps nothing, `/park` runs on the yes and
  hands over tokens wherever the server allows, a password as a last
  resort
- `/discard` and a timer take out what SSO has to park first, and a no
  revokes the refresh token, and the shared store's TTL is the real expiry
- `DESKTOP_BROWSER_SIGN_IN`, default on, closes the routes, the page's
  hand-off and the app's button; the question says which device asked
- tokens from a typed password stay a password account under the name
  typed (#1432)
- redeem refuses a server of the user's own when the device's rule says
  no
- the park budget is reserved before going upstream, and only failures
  count
- "Not now" ends the flow in the app at once, and the question names the
  account

`desktop-handoff-routes.test.ts` now tests the two-step flow against the
real budgets. `sso-start-route.test.ts`'s config mock answers the new
setting, and the SSO test's expected answer has the account in it.
2026-11-04 02:40:37 +01:00
Linus Rath d31d0af560 fix: say what an open program still holds, keep forwarded parts under their rules, and let main decide what a sent draft leaves 2026-11-04 02:36:48 +01:00
Linus Rath 02a900ef07 Merge commit 'dc8482ace' into feat/do-browser-signin 2026-11-04 02:11:07 +01:00
Linus Rath f570277a61 chore: merge the desktop offline branch into quick compose 2026-11-04 01:45:09 +01:00
Linus Rath 1c98e74149 Merge branch 'feat/desktop-offline' into feat/do-browser-signin
# Conflicts:
#	lib/desktop/languages.ts
2026-11-04 01:40:31 +01:00
Linus Rath 71aaadef50 fix: run plugins' compose check in quick compose, keep a composer with one owner, and bring it back after a crash
The first version of quick compose skipped `onBeforeCompose`, a composer
moved back could end up open in both windows, a reply moved back under
Pro flagged the selected message, and a crashed pop-out came back blank.

- A new message in a pop-out runs `onBeforeCompose` with the same result
  as the main window: a plugin that stops it closes the window.
- The way back has two steps, so a pop-out that gave up can't leave a
  second owner. A handoff behind a plugin's gate makes it say busy.
- A Pro tab made from a composer moved back answers its own message.
- Each save writes `draft`, `account` and `owner` into the pop-out's
  address, which a reload after a crash starts from. What was typed since
  the last save, up to the 2 s autosave delay, is still lost.
- The recorder refuses keys that type a character on this keyboard, and
  what the shell refuses from a page, and says why.
2026-11-04 01:31:43 +01:00
Linus Rath c8c98e79d4 feat: sign in to the desktop app in your browser, with your password manager
The desktop login page gets "Sign in with your browser…" as its main
button. On an instance it opens the instance's real login page in the
system browser, with a PKCE challenge. The page checks the password or
runs SSO, parks the sign-in under a one-time code, asks once, and sends
the code to the app's loopback listener. This browser gets no session
out of it, and the app's own page redeems the code, so SameSite=Strict
is fine. Standalone runs the mail server's OAuth against the listener,
with a page from main for a server with no OAuth at all.

- `/api/auth/desktop-handoff/park` and `redeem`, and `sso/complete` parks
  tokens the same way for a loopback target
- `auth.claim()`: the calls are taken once per document, in the first
  render, in `WindowControlsHost`
- `BrowserSignIn` policy, default on, with its ADMX and samples
- strings in all 30 languages, and the old button's are gone

`runDesktopBrowserSignIn`'s tests changed because the code no longer goes
through the callback page's address: it's redeemed where it arrives.
2026-11-04 01:09:12 +01:00
Linus Rath 889f6ffbd1 fix: fail requests under way when the desktop app closes its connections, and forget client certificates per account 2026-11-04 00:56:09 +01:00
Linus Rath e696fb1610 fix: ask before sending an older attachment also when the save was a moment ago 2026-11-04 00:17:15 +01:00
Linus Rath e74248b37b feat: edit attachments in their own program from the desktop app and take the saved file back into mail 2026-11-03 23:37:40 +01:00
Linus Rath 3da18cc2c0 feat: set the quick compose keys in the desktop settings and move a composer back into the main window 2026-11-03 23:25:10 +01:00
Linus Rath 8549e3c4c6 feat: translate the desktop app's client certificate picker and forget chosen certificates at sign-out and account switch 2026-11-03 23:21:56 +01:00
Linus Rath 54bda3ef3d Merge branch 'feat/do-packaging' into feat/desktop-offline
# Conflicts:
#	components/desktop/desktop-host.tsx
#	lib/desktop/languages.ts
2026-11-03 22:02:10 +01:00
Linus Rath 43ba229c7d Merge branch 'feat/do-perf-guard-2' into feat/desktop-offline 2026-11-03 21:50:37 +01:00
Linus Rath 2b4a1171ab fix: show the spelling and unsent-mail labels in the desktop app instead of their raw keys 2026-11-03 21:50:09 +01:00
Linus Rath 26c009c302 feat: send the updater's translated text with the desktop menu labels 2026-11-03 19:45:23 +01:00
Linus Rath b8560579a4 Merge branch 'feat/do-import' into feat/desktop-offline
# Conflicts:
#	lib/desktop/types.ts
2026-11-03 19:44:43 +01:00
Linus Rath a96c66fa5e Merge branch 'main' into feat/do-packaging
# Conflicts:
#	components/settings/general-settings.tsx
#	lib/desktop/types.ts
2026-11-03 19:37:41 +01:00
Linus Rath b92b488e39 fix: stop a local import losing messages on a held file and name what a resume leaves out
Fixes from the review of the desktop import.

- A message another program holds is no longer recorded as deleted. Only a
  real absence moves the import past it; anything else is tried again and
  then reported as a failure, and the next run reads it.
- A resume that can't read some folders names them in a toast with a choice
  to stop trying, keeps the job open and retries only those at the next start.
  Files chosen by hand in the same dialog are named, since they can't come
  back by themselves.
- A wipe or sign-out releases the job in the shell before the record goes.
  Leaving the dialog without starting releases the confirmed files. Job
  records are per account, and a start is refused while another window holds
  that account's job.
- Dedupe goes by the bytes without state headers: a reused Message-ID of the
  same length dropped a real message. Older records still count, so nothing
  goes in twice.
- A lost connection on a folder or Email/import pauses instead of counting
  towards the failures that stop an import.
- The taskbar goes through `lib/desktop/taskbar-progress.ts`.

Test changes: the taskbar test now renders the host, because its helper in
`local-import-job.ts` is gone, and the dedupe tests use `check`, which replaced
`keyFor` and `has`.
2026-11-03 19:17:38 +01:00