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.
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.
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.
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.
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.
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.
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`.