Review of the notification actions. With a plugin that may stop a move
or read change, or a silent tab holding the login, the message now
opens instead of the route acting. The route answers 401 under a
session limit, and neither path touches a message since moved to Trash
or Junk. A tab that thawed declines an old click, Archive follows the
gate of the notification's login and files into a shared-out Archive
as the app's button does, Reply opens a composer in the Pro shell, the
route retries once when another client made the folder first and
leaves no empty folder behind, and logins match however the server
address is written.
Two assertions of this feature's own tests change: the route answers
401, not 403, when a login's scopes can't be worked out, since that now
also covers session limits; a tab's message to the page now carries
`at`.
The Pro shell's mail surface now reads a parked link's query from the
handoff: the address bar may have none by its first render, which lost
`?slot=` and `?owner=` there too.
A notification for one message gets buttons where the browser has room
for them, and a click on its body works as before. The service worker
has no login, so an open tab takes the action first, through the store
actions. With no tab open a new same-origin route, /api/push/action,
does Mark read and Archive with the context cookie. When the cookie is
gone, the token has expired or the login can't be told apart, the
message opens in the app instead. Reply opens the composer through a
`?reply=1` link, with any text typed on the notification.
- `public/sw.js`: buttons, asking open tabs, the route, `sync` handling
- `app/api/push/action`: same-origin, checks the login and the account,
honours the admin's Archive gate, files by the mirrored archive mode
- `lib/pwa-notify`: the page's side, the mirror for the worker, and
`requestOutboxSync`, which the offline queue calls
- Background Sync, thin version: a `sync` event tells open tabs to flush
Two push-preview-route assertions change: they match the whole answer
for a named message, which now also carries `login`, what the action
route checks so a stale notification can't act on whoever holds the
cookie slot now.
Automatic signature insertion was unconditional: any identity with a
signature got it embedded in new mail and appended to replies, forwards
and quick replies. Users who want a signature only in specific contexts
had no way to opt out.
Add two per-account synced settings, both defaulting to off:
- signatureNewMailEnabled: insert the signature in new messages (and
re-opened drafts, which open in compose mode)
- signatureReplyForwardEnabled: append it in replies, forwards and the
quick reply box
With a toggle off the composer neither embeds the signature into the
body, nor appends one at send or draft-save time, nor shows the
read-only preview under the editor - the preview would promise a
signature that never goes out. A signature pasted in by hand is never
stripped (bodyCarriesSignature still short-circuits the append).
Also flip signatureSeparatorEnabled's default from true to false: the
RFC 3676 "-- " delimiter surprised users more than it helped. Only the
default changes - no migration, so users who already set it keep their
choice.
Co-Authored-By: Claude Code <noreply@anthropic.com>
A frame slot `side-panel`: a plugin's panel right of the mail list and
reader, opened from a button in the panel rail (Mountain View's, or a slim
rail in the default layout while a plugin offers one). `ui.setSidePanel`
names its button; `ui.openPanel('side-panel')` opens it.
`ui.setSidePanel` takes `rail: false`: the rail draws no button for the
plugin's panel, which then opens only on the plugin's request
(openPanel / togglePanel('side-panel')), e.g. from a top bar action. A
rail left without buttons is not drawn, in the default layout and in
Mountain View's side panel alike.
Settings > Backup & reset gets "Keep mail on this device", off until the
user turns it on in this browser. While it is on:
- the JMAP client hands the server's answers to an encrypted cache in
IndexedDB (AES-GCM, non-extractable key) and answers from it when the
network is gone: the folder list, the last 30 days of the Inbox and of
the folders the user opened, bodies and attachments, at most 250 MB,
oldest out first; a background sync keeps it current
- read, flag, tags and moves made offline are queued and sent in order
when the server answers again; deleting for good and emptying a folder
are refused offline
- the composer, quick reply and the S/MIME and PGP plugins' sends that
complete the composer's go to an outbox once every step up to the
transport ran (checks, label, signature, plugin send checks); only the
finished send waits, with files attached offline
- the service worker keeps this build's app shell, so the app also
starts offline from the kept session and the last policy
- a full sign-out, the idle sign-out and signing one account out delete
it, a sign-out the user starts asks first when mail is unsent, and the
login page names what an automatic sign-out deleted; a policy fetch
that forbids it (devices.noOffline) wipes that account
Not in Lite.
- The message for send checks is built after waiting for required
checks that are still starting: a send right after sign-in gave them
no external recipients and no file types.
- A plugin's forward is held too while a required check is down, also
with no plugin listening.
- A plugin the admin forces on that hasn't reached this browser yet (a
new device, a failed plugin sync) counts as starting.
- A check that stopped working says to reload or tell the admin.
- The files that were checked are the ones sent, and recipients a
plugin adds after the check are checked again.
- A file Bulwark couldn't read is marked unreadable; a text file
starting with "MZ" is text, Java and Android apps are told from ZIPs.
Changed assertions: runPluginSendCheck takes a builder and reports
guard-loading or guard-failed instead of guard-down; the composer test
of an auto-disabled guard expects the stopped_working text; the sniff
tests give programs a zero byte, as real ones have, and expect
unreadable on a file that can't be read.
Organisation > Devices now sets policy.json `organisation.devices`, each
switch off by default and scoped per domain or group:
- No downloads: attachments and files only preview (reader, conversation
view, Pro tab, list chips, .eml previews, the Files app). Download,
Download all, Open in new tab, Save to Files, drag-out, the browser's
PDF viewer (pdf.js instead) and media download controls go away.
- No export: no .eml export, View source, message drag-out, or folder and
account export; a running export stops and its file is deleted when
the switch turns on. Import stays.
- No printing: no Print buttons in mail, calendar and contacts; Ctrl+P
prints a note; the reader frame prints blank.
- No install prompt: no card, and the browser's own banner is held back.
- Watermark: the user's address and the date over the reader,
previews and every printout (mail, calendar, contacts, plugins).
A message follows its own account's policy; what isn't about one message
follows the strictest signed-in account. Every user path hides its
button, and the anchor and new-tab helpers refuse as a backstop
(lib/download-guard.ts), never the blob readers. The plugin APIs
`ui.downloadFile` and `ui.print` refuse too, and the office editor frame
loses `allow-downloads`.
Two assertions in components/mail/__tests__/blob-routing.test.tsx now
expect `{ kind: 'export' }` as the fifth argument of downloadBlob for an
.eml export: the export says what it is, so the backstop checks "No
export" rather than "No downloads" for it.
- Pro tabs stamp a message of another login or a shared account with
where it came from, so the report and the move use its account.
- A selection across accounts names every address its reports go to,
and Undo says how many messages came back.
- A message the list doesn't hold runs the plugins' spam hooks too, and
every open view shows a reported message as reported at once.
- A reported message hides its unsubscribe banner, and links are off in
its attachment previews.
- Search results and All mail don't offer reporting the user's own sent
mail; the Mountain View selection bar offers it.
- The focus goes back to the button that opened the menu.
- ARF names the delivered-to address (or leaves it out), says binary for
bare line breaks and long lines, and refuses a message bigger than the
server's upload limit before downloading it.
The phishing tests that compared one address now compare the list of
addresses, and the ARF builder test passes the encoding, not a flag.
A sent message brought back by Undo, Cancel and edit or edit-as-new
reopened with the default label, because only drafts carried the
X-Bulwark-Label marker; it now falls back to the SIO-Label it went out
with. A reply, quick reply or plugin forward takes over the original's
label only when an internal sender set it and never one below the
default, since anyone outside can write the header. The tests written
for the earlier rule (any received label counts) now pass an internal
sender, which is why their originals changed.
Message ids repeat across accounts, so a hook payload naming only an
emailId could not tell a plugin which mailbox the message lies in when
it is a shared or group account's.
onBeforeEmailStarToggle and onEmailStarToggle now carry accountId when
the starred message is not in the user's own account, as the
message-row-actions click context does. Every star (the row's button,
the reader's star, the key, the menu) goes through toggleStar, one
message at a time, so one accountId names it; the account is resolved
before the plugins are asked. An own message's payload is unchanged.
onAfterEmailSend now carries accountId, the JMAP account the sent
message lies in (a mail sent from a shared or group identity lies in
that account), so api.email.get finds it. sendEmail returns that account
as emailAccountId, the S/MIME send too. The hook also carries
composerId, the id the sending composer's slots and actions gave
plugins, so a plugin can act on what the user chose in that composer.
A plugin opens a message as a click on a list row or on a search result
entry naming one does, also a message of a shared or group account,
reached through the login that reads it. On the mail page it opens in
place, in the Pro shell in an email tab as a message link does there;
elsewhere the tab loads the message's link, at most five times a minute.
A message no login reads, or one its account does not have, is refused
with an error the plugin sees, as is one the mail page could not open.
Needs email:read.