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 reader's more menu no longer offers Star while the star sits by the
subject, so the tests that expected Star in the menu and the phone panel
now expect it gone. Import mail moved to the command palette (it stays in
the folder menu and Settings); Keyboard shortcuts stay behind ?, the rail
and Help.
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 app and an installed PWA had a row across the whole window
with the title and the window controls. That row is gone. The rail,
sidebar, list and reader run to the top of the window in their own
colours, and the controls sit in the top row of whatever is rightmost.
- `lib/window-controls-overlay.ts` finds and marks those rows before
paint, and they reserve the controls' width through
`--titlebar-right-inset`
- the rows and the rail are the drag area, everything clickable is
`no-drag`, and an invisible band lets a page with no row still move
- settings and the admin shell get a row only under the overlay, Pro's
tab strip and Mountain View's bar match the controls' height
- Alt floats the menu over the top left, in `window-menu-bar.tsx`
- skeletons carry the same marks, so nothing shifts when the app loads
A plugin's render can be a key operation, and mark-read replacing the
message object ran it twice: a second PIN dialog, and two cancels lock the
key. The effects now depend on what the hook sees and on the explicit
re-render nonce, not on the object. The list row and the full message still
differ, as a row has no headers for a plugin to recognise its mail by.
onRenderEmailBody can return `blockExternalContent: true` (flag
`api.host.features.blockExternalContent`) for a body it can't vouch for.
External content then stays off whatever the settings and trusted senders
allow, in the reader and in conversations, with no way to load it. The S/MIME
plugin uses it against EFAIL.
The lock is off by default, and an OS policy can require it and set the
timeout. It locks after a time without use counted across all windows and
tabs, on OS screen lock in the desktop app, and from Lock now.
Locking saves open composers, stops the outbox at its next message, closes
the store and stops its worker with every key, then reloads the page, so
nothing of the old page stays in reach. The new page holds the app back at a
Suspense boundary until someone unlocks. Pop-outs get no session while
locked, push notifications name nobody, and subjects leave plain storage.
One random lock key wraps each account's key as a layer. The PIN wraps the
lock key with PBKDF2 at 600,000 iterations, with a wait after wrong tries
that a reload doesn't reset. A passkey with PRF and the desktop's OS storage
wrap it too. Details and limits are in the decisions file.
A restored composer opened whatever the id named. Sending keeps the id, so its autosave would have destroyed the Sent copy, Send mailed it twice and Discard deleted it. It now needs the login in its address and a real draft, keeps its From with several accounts, and stays on the list if the server doesn't answer. A message from a bulwark:// link no longer marks itself read or sends a receipt. Native menus speak all 30 languages. Spellcheck dictionaries come from Google only after a yes, never in no-third-party mode.
The flush keeps one `outbox.dropOrphans` caller, the mail branch's daily
one with its 7 days, and drafts' own query says what's needed, so
`addHeldFileSource` and its test are gone. `unsentCount` counts local
drafts, and a draft a waiting send names counts once. The scope check in
`recordBlob` and the permanent-status rule moved to
`lib/store/mail/attachments.ts`, with tests.
Review of the offline drafts found these.
- Every new message gets new part ids and old ones die with it, so an
open composer failed with `blobNotFound` after a flush. The client and
the flush re-read the draft and re-point parts once. The fake now
gives new part ids, which is why `drafts.test.ts` and
`drafts-client.test.ts` compare file contents instead of blob ids.
- A Message-ID alone could destroy another client's message. A copy now
has to be solely in Drafts, with `$draft`, close to the attempt, with
the same subject and size, and what the lookup saw is the destroy's
base. Ghosts ride on the replacing row and only queue once it's up.
`drafts.created` no longer queues a copy that arrived for a replaced
row, so its test now expects the next flush to do it.
- Turning offline off asks about device-only drafts too.
- A kept file can't read back empty: chunks of a kept file are left
alone, and `keptBlob` and `heldFile` refuse pieces that don't add up.
- A flush stopped by a passing failure tries again, the banner shows
waiting drafts, and under the flush lock a `sending` row counts as
left by a dead tab at once. A lookup refused for good no longer stops
the outbox.
- `FetchTransport` streams downloads and reports a broken transfer as
interrupted, so one bad file doesn't end the pass.
- Raw messages come after every file, only into free room, and a file
needing room pushes them out first.
- An old version in another login stays until the new one is up. An
online send of an unflushed stand-in still removes its submission.