Commit Graph
1019 Commits
Author SHA1 Message Date
Linus Rath 989385f4f6 Merge branch 'feat/desktop-offline' into feat/do-ux-compose-fixes
# Conflicts:
#	components/email/email-viewer.tsx
#	components/settings/accessibility-settings.tsx
#	lib/__tests__/settings-search.test.ts
#	lib/settings-search.ts
2026-11-04 09:54:25 +01:00
Linus Rath aa62855759 Merge branch 'feat/do-ux-settings-fixes' into feat/desktop-offline
# Conflicts:
#	components/layout/__tests__/account-switcher-mountain-view.test.tsx
#	components/layout/account-switcher.tsx
#	components/settings/accessibility-settings.tsx
#	components/settings/reading-settings.tsx
2026-11-04 09:44:51 +01:00
Linus Rath 4a764150b2 Merge branch 'feat/desktop-offline' into feat/do-ux-settings-fixes 2026-11-04 09:22:44 +01:00
Linus Rath ceaceb7f2a Merge branch 'feat/desktop-offline' into feat/do-ux-compose-fixes
# Conflicts:
#	components/email/email-composer.tsx
#	components/email/email-viewer.tsx
#	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 09:20:50 +01:00
Linus Rath fcc9fc0cc0 Merge branch 'feat/desktop-offline' into feat/do-ux-mail-fixes
# Conflicts:
#	components/email/email-viewer.tsx
#	components/mail/mail-app.tsx
#	components/offline/offline-host.tsx
#	components/ui/welcome-banner.tsx
#	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 09:11:09 +01:00
Linus Rath 044647ff34 fix: keep the desktop layout in a half-screen window and at 150 % zoom
A window snapped to half the screen (640-800 px) or a 1366x768 laptop
at 150 % (911 px) got the phone and tablet layout: bottom tab bar,
folders in a drawer, Settings as a phone list with a Back arrow. The
layout switched at the 64rem (1024 px) breakpoint.

Now the phone layout starts below 600 px. From 600 to 1024 px the app
is a compact desktop: app rail at the side, folder sidebar folded to
its icons (the existing collapsed state, opened per session with its
toggle), Settings with its two panes and a narrower page list. Below
840 px the split layout opens a message in the list's place, as the
focused layout does. This holds in the browser and the desktop app,
in the Pro shell (which now runs from 600 px) and under Mountain View.

- `hooks/use-media-query.ts`: classifyWidth, PHONE_BELOW 600,
  FULL_DESKTOP 1024; the tablet class (`isTablet`) is gone, the store
  has `isCompact` instead. Plugins still see "tablet" for it.
- `app/globals.css`: a `desk:` breakpoint at 37.5rem for the layout
  classes, and a `tap:` variant for 44px rows on phones and touch
  screens below 1024 px (was every window below 1024 px).
- Touch screens below 1024 px keep swiping list rows (was phones only).
- The boot skeletons, boot script, mail app, sidebar, reader, admin
  shell and calendar, contacts and files follow the new widths.

Tests changed with the behaviour: the tablet cases in pro-gate,
mail-app-mv-composer-move, mail-app-compose-handoff,
mail-app-thread-expand-mark-read, mobile-tour-end and
thread-list-item-columns now run on a phone or a compact desktop, as
those widths no longer exist as a tablet. mail-app-short-window-tab-bar
is replaced by mail-app-compact-layout: a phone on its side is a
compact desktop with the rail at the side, so the short-window tab bar
rule is gone. Class and media-query assertions follow desk:/tap: and
37.5rem; the 32px-text test uses 1100 px, since 1440 px at 32px is now
a compact desktop.
2026-11-04 09:07:06 +01:00
Linus Rath ceaa81cd9e Merge branch 'feat/do-uxs-plugins' into feat/do-ux-settings-fixes 2026-11-04 08:34:57 +01:00
Linus Rath ce34617147 feat: turn plugins an admin installs on for every user, who can turn them off
The #1327 store test now serves its optional plugin with defaultOn false:
it checks that every plugin is listed when IndexedDB fails, and a plugin
the admin didn't force is on by default now.
2026-11-04 08:01:38 +01:00
Linus Rath 17bf3c4e6f Merge branch 'feat/do-ux-compose-send' into feat/do-ux-compose-fixes 2026-11-04 07:41:26 +01:00
Linus Rath b8646eefc4 Merge branch 'feat/desktop-offline' into feat/do-menu-fixes 2026-11-04 07:11:07 +01:00
Linus Rath 5c3a12f91c feat: use the plain wording for everyone and drop the plain language switch
The plain-language test asserted that the normal strings show while the switch is off and that changing the switch swaps them live. With no switch left, it now asserts the plain strings show from the first render and through a language change.
2026-11-04 07:04:42 +01:00
Linus Rath e8d8486b85 fix: cut a row's right-click menu to the usual actions, with Restore in Trash and read and unread for mixed selections 2026-11-04 07:00:54 +01:00
Linus Rath 317f3b40dd fix: suggest recipients whose name starts with what you type first 2026-11-04 06:42:47 +01:00
Linus Rath 781013cc44 feat: close the composer as soon as Send is pressed and show Sending, then Sent with View 2026-11-04 06:41:51 +01:00
Linus Rath b425f4b211 feat: turn undo send on by default with 5 seconds
The policy-defaults test now expects the new build default of 5 seconds
for an invalid admin value, and the settings-search tests look for the
row under its new label "Undo send".
2026-11-04 06:18:29 +01:00
Linus Rath 60a97303c2 fix: open the folder when its unread count is clicked, and search all folders again on a new search
Two sidebar tests asserted the old unread-count button (it filtered the
folder to unread without opening it, in a 24px target); they now check
that the count is plain text and opens the folder.
2026-11-04 06:17:05 +01:00
Linus Rath 9ce7ac8172 feat: show unread per account in the switcher and count every account on the app badge and tray
The default layout's switcher listed accounts without their unread, so an
existing test asserted that (account-switcher-mountain-view, 'unread-free');
it now expects each account's count. The idle-outside-Pro hook test now
uses one account, since a second connected account loads in every layout.
2026-11-04 06:06:37 +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 bce0c0b744 Merge branch 'feat/desktop-offline' into feat/do-keystore-fixes 2026-11-04 03:52:09 +01:00
Linus Rath be8aba2ae7 Merge remote-tracking branch 'origin/main' into feat/desktop-offline
# Conflicts:
#	stores/auth-store.ts
2026-11-04 03:42:18 +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 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 f0e874103d fix: take back certificate grants when a session ends without a sign-out 2026-11-04 03:23:45 +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 8cb218ce3c fix: apply email notifications off and do not disturb per login, not to every login's mail 2026-11-04 03:13:59 +01:00
Linus Rath f0517ecd08 test: type the sign-out push test's fake client 2026-11-04 03:13:59 +01:00
Linus Rath 248f9c261b fix: take down push for logins with no connection when signing out of everything 2026-11-04 03:13:59 +01:00
Linus Rath 80084457be fix: end a removed login's push before its client disconnects 2026-11-04 03:13:58 +01:00
Linus Rath a6ef818149 fix: end a login's push registration when it signs out, is removed or wiped 2026-11-04 03:13:57 +01:00
Linus Rath fc6a209727 fix: keep push notifications off screen with email notifications off or do not disturb on 2026-11-04 03:13:56 +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 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 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 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 fafdb0cdc0 fix: stop every search re-rendering the mail list when nothing is ticked 2026-11-03 21:06:48 +01:00
Linus Rath 529d334496 Merge branch 'fix/ui-bug-hunt' 2026-11-03 19:44:46 +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 99bc974f8b Merge branch 'feat/do-telemetry' into feat/desktop-offline
# Conflicts:
#	lib/desktop/types.ts
2026-11-03 17:42:32 +01:00
Linus Rath dcfe25c754 Merge branch 'feat/do-store-export' into feat/desktop-offline 2026-11-03 17:39:54 +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 289cce9bd5 Merge branch 'feat/do-reminders' into feat/desktop-offline
# Conflicts:
#	lib/offline/offline-mode.ts
2026-11-03 16:58:11 +01:00
Linus Rath af6cdfb997 fix: keep unsent mail through the idle and session limits, and mark a signed-out account in the switcher
The timers used to delete everything an account kept, unsent mail included.
They now follow the refused-login rule: sign out and hide, the next sign-in
brings it back, otherwise it goes after 30 days. A login without "Remember
me" that ends with the browser, an admin wipe and a wipe from settings don't
change.

- `wipeOnRefusedLogin` is now `wipeOnForcedSignOut`, since it also covers the
  limits. The server's `SESSION_TOO_OLD` goes the same way, as `session`.
- A refused account beside one that stays signed in shows in the switcher as
  needing sign-in, with the count and the date, and gets one toast.

Existing tests changed because the rule did: the session timer tests expected
a plain `logoutAll`, and the auth store tests expected no hook at the session
limit.
2026-11-03 16:36:13 +01:00
Linus Rath f21198b804 Merge branch 'feat/desktop-offline' into feat/do-app-lock 2026-11-03 16:24:34 +01:00
Linus Rath 71bd7af93d Merge branch 'feat/do-remote-wipe' into feat/desktop-offline 2026-11-03 16:20:09 +01:00
Linus Rath 3c3f6016b9 test: catch a wipe heard twice, a row put back and a mark left by a recovered refusal 2026-11-03 16:04:14 +01:00