X, Escape, clicking another message and Escape in a pop-out window
now all save the draft and close, with a "Draft saved" toast that has
Discard (with Undo). The Save or discard question shows only when that
save fails. A Reply or Forward closed before the user changed anything
leaves no draft. Undo send being on by default no longer asks "Send
without a delay?" on servers without delayed sending; only a delay the
user picked does.
Changed test assertions: the close-guard, priority, account-switch,
threading, sign-out, mention, draft-attachments, discard-uploads and
save-pending-attachments tests reached saving or discarding through the
Save or discard dialog on close, which now only appears after a failed
save. They now close directly, or use the Discard button, or check the
failed-save question. The send-delay test marks its delay as chosen.
The autocomplete, chip-drag and paste tests found chips by their old
"Name (address)" text; they now find them by the "Name <address>"
tooltip, which is where the address went.
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".
Tests changed: the "asked once" tests reached the second check through
a second dialog, which no longer exists, so they now stop the send at
"Send this message?" instead. The empty-body test asserted the error
under the editor; an empty body now asks instead of blocking Send.
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 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.