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`.
The import dialog in the desktop app can now find Thunderbird profiles and
Outlook PST files, show what it found and import what the user confirms.
Browsers behave as before.
- The bridge's `localImport` gives opaque handles and range URLs. The shell
asks natively before it looks, before it reads and before it goes on after
a restart.
- `lib/mail-import` gets a Maildir source and a per-file `folder`, so
Thunderbird's `.sbd` tree maps onto the account's folders. The PST worker
reads through a synchronous range XHR when there is no Blob.
- What was imported is remembered by Message-ID and size per folder, because
compacting a folder changes its checkpoint key and Email/import doesn't
dedupe. It runs from the dialog and on a resume.
- The job's record lives in IndexedDB, since Chromium writes localStorage too
late to survive a kill. Windows wait for the job's lock, so one takes over
when another goes.
- The desktop dialog says how long an import takes at the upload limit, warns
when Thunderbird is running, and turns down .ost files with the reason.
The engine used to be able to lose an import while offline: uploadBlob kept
the file on this device under a stand-in id and reported success. It now
passes `noHold`, Email/import is network only, an offline spell pauses without
using up a message's tries, and a device-only client stops at once. The same
fix applies to imports in the browser.