The review of offline mode found ways to send a message twice, to lose
unsent mail without a word, and to send mail without its signing,
encryption or check plugin. This fixes them and the smaller findings:
- each outbox message is taken for sending in one database transaction
under a Web Lock per account; only a failure before the submission
request (SendNotStartedError from the client) puts it back, any other
one marks it "may have gone out"; read-backs after the submission no
longer fail a send; a send of a closed tab turns uncertain after a minute
- every sign-out the user didn't confirm names the deleted messages on
the sign-in page, or in a toast when another account stays signed in
- nothing is queued while an enabled plugin isn't running (after a start
without the network none can); the composer says why
- the outbox waits for a fresh policy and holds messages the rules have
since made stricter (a required label, a required send check)
- another tab switching offline mode off sticks: only switching it on
may create the database
- only attachments of kept messages are kept, filed with their message,
deleted with it and under the cap between syncs; file names and types
are sealed; folders that went away leave the scope, and a folder that
couldn't be asked keeps its mail without holding the others back
- a group scope that forbids offline mode waits until the account's
groups are known; one server down no longer holds another's mail
- an offline start only when the server is really unreachable, and the
real sign-in follows by itself once it answers
- the service worker files pages and chunks under the build that served
them, and the shell list of another build isn't kept under this one
- a raw send counts as the composer's only from the plugin running its
takeover, for that identity
- errors and refusals are in all languages; the banner tells "offline"
from "server can't be reached"; outbox buttons name their message;
changes and messages go in the order they were made
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.
Plain-text bodies render in the page, not in the body iframe, so their
links opened without the link check. Plugins now get each link as the
browser parses it (new URL(href).href) and that same string opens, so a
link check can't judge one host while another opens. With no link check
listening, clicks and middle-clicks stay with the browser as before.
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.
Raw sends (api.jmap.sendRaw and submitRaw) now always lose label headers
a plugin wrote while the organisation uses labels. A crypto takeover is
matched to its raw send by a labelToken the composer puts in the
onComposeSend request (plugins that don't pass it back are matched by
identity, and refused when two takeovers are open), not by comparing
subjects. A raw send outside a takeover gets the default label, or is
refused when a label is required. The extra header is folded on the raw
path. The takeover code moves to lib/outgoing/classification-raw.ts;
the old test that let a raw send outside a takeover through unlabelled
now expects the forged header to be removed.
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.
- `directory:read` for api.user.findPeople, with at least 3 characters,
at most 20 ids, 20 results and 10 lookups a minute, in the directory
of the account the call names; one load in flight per client.
- Plugins hear `group` only for accounts the server confirmed are group
principals; the policy's fallback stays for the admin's scopes.
- The email.* keyword methods refuse $draft, $junk, $notjunk and
$phishing.
- A plugin keeps at most 4 KB in a mailbox and reads it 30 times a
minute; the email:write consent text says others in a team mailbox
can read it.
- email.updateKeywords also changes the rows of expanded conversations.
What the team inbox plugin needs, kept general for other plugins:
- `api.ui.setListFilters`: keyword filters drawn as chips in the list
header of the folders they apply to (by account kind and folder
role). A chip the user turns on is ANDed into the folder's query and
comes off with one click; a keyword change a filter tests loads the
list again (ui:message-list).
- Actions get `accounts`, `hasKeyword` and `notKeyword`, so a button
shows only on messages of group accounts, or of a keyword state; the
click of message-row-actions and reader-actions carries the message's
`accountId`.
- `api.email.updateKeywords`: several keywords in one change, through
the list for a listed message (email:write).
- `api.user.getSelf`, `getMailAccounts` and `findPeople`: the user's own
account id (Stalwart's principal id), which accounts are group
accounts, and people from the directory (account:read).
- `api.accountData`: a plugin's own entry in a mail account's shared
Inbox metadata on Stalwart 1.0 (email:read / email:write).
- Row badges take an `icon`.
keywords.add now records the plugin on each definition it adds
(addedBy). keywords.update changes the label, colour or visibility of
those definitions, never their id; keywords.remove deletes them, as
Settings > Tags does, and leaves the keywords on messages alone. Both
need settings:write and skip every other definition, so a plugin cannot
rename or delete the user's own tags or another plugin's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>