213 Commits
Author SHA1 Message Date
Linus Rath 4b60fcc1e6 fix: make the offline outbox never send twice, never lose mail silently and never skip a plugin #523
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
2026-11-01 14:19:54 +01:00
Linus Rath db5ac82f53 feat: keep recent mail on this device and send mail written offline #523
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.
2026-11-01 11:46:11 +01:00
Linus Rath 0401352e6e Merge branch 'feat/enterprise-pack-d'
# Conflicts:
#	components/email/thread-conversation-view.tsx
#	lib/admin/outgoing-hosts-registry.ts
2026-11-01 10:48:21 +01:00
Linus Rath 6b3a220d44 fix: let only a server the admin typed through with no third-party traffic 2026-11-01 10:06:00 +01:00
Linus Rath 0c8d4ad93e fix: hold sends until required send checks have started, and check what really goes out
- 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.
2026-11-01 10:02:58 +01:00
Linus Rath fbf85972c5 fix: check links in plain-text mail and the address the browser really opens
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.
2026-11-01 09:51:00 +01:00
Linus Rath e80f5ade81 feat: let admins turn off downloads, export, printing and the install prompt, and add a watermark
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.
2026-11-01 08:52:34 +01:00
Linus Rath 50bb4e4c37 fix: keep secret plugin settings out of plugins when the user is also signed in as admin 2026-11-01 08:40:32 +01:00
Linus Rath e73a1b375f feat: let link-check plugins reach an admin-set service with no third-party traffic
The plugin host tests compared api.host.features with an object of two
flags; they now include the new noThirdPartyTraffic flag.
2026-11-01 08:39:35 +01:00
Linus Rath b3369c7e78 feat: send every link click through the link check 2026-11-01 08:31:27 +01:00
Linus Rath c004587158 feat: keep sending blocked while a required send check isn't running 2026-11-01 08:22:32 +01:00
Linus Rath edf619a19d feat: give send-check plugins the authored text, external recipients and attachment types 2026-11-01 08:16:39 +01:00
Linus Rath 85deb006a8 fix: keep plugin raw sends from dropping or forging the classification label
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.
2026-11-01 06:52:44 +01:00
Linus Rath 3ef16f602e fix: keep a message's label through Undo send and take over labels only from internal senders
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.
2026-11-01 06:48:01 +01:00
Linus Rath 1c7f81bcc6 feat: pick a classification label in the composer and stamp it on sent mail 2026-11-01 05:33:12 +01:00
Linus Rath 136e6be8fd refactor: drop the unused draft autosave interval setting 2026-10-31 18:07:16 +01:00
Linus Rath 7341e981c3 fix: limit how often a plugin can open a message in the reader #1466 2026-10-31 16:25:07 +01:00
Linus Rath b8223a4492 Merge pull request #1466 from HOLOEYE-JZ/feat/plugin-open-email
feat(plugins): api.ui.openEmail opens a message in the reader
2026-10-31 16:25:05 +01:00
Linus Rath c09be1cdd3 Merge pull request #1456 from HOLOEYE-JZ/feat/plugin-frame-activity
feat(plugins): input in a plugin frame counts as activity for the idle sign-out
2026-10-31 16:25:03 +01:00
Linus Rath 2daffe1364 fix: complete the vietnamese strings and register them for plugin controls #1448 2026-10-31 16:24:49 +01:00
Jacob Zeitz 54ad6390b3 feat(plugins): api.ui.openEmail opens a message in the reader
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.
2026-10-31 14:14:48 +01:00
Jacob Zeitz df8734f412 feat(plugins): input in a plugin frame counts as activity for the idle sign-out
Input inside an iframe never reaches the host window, so the idle timeout
signed users out while they worked in a plugin page, dialog or panel. The
sandbox runtime now reports trusted input in its frame (the guard's own
events, focus left out) through a MessagePort it hands the host with
sandbox-ready, before any plugin code runs, at most once per 15 s. The
host bridge passes it to a small notifier the session timeout guard
counts like its own window's input, so it reaches the other tabs too.
2026-10-30 16:37:42 +01:00
Linus Rath 392d17ab71 fix: show plugin badges on message rows drawn before the plugin started 2026-10-30 00:23:11 +01:00
Linus Rath fb751a30df feat: add a table to the plugin UI kit for line items and totals 2026-10-29 23:33:25 +01:00
Linus Rath 23767b9c80 feat: let plugins print a page they made, such as a readable invoice 2026-10-29 23:30:43 +01:00
Linus Rath f6b641c94e feat: let plugins forward a message with its attachments after the user allows the address once 2026-10-29 23:27:15 +01:00
Linus Rath 2e6dcceebb Merge pull request #1358 from illuwa-soft/fix/full-height-app-page
fix: honor full-height app-page slots
2026-10-29 20:27:00 +01:00
Linus Rath 35be105a73 fix: size small text in rem so it follows the browser and app text size 2026-10-29 19:38:45 +01:00
Linus Rath c4dfa6f3e4 fix: draw the focus outline on plugin kit buttons, fields and switches 2026-10-29 19:38:44 +01:00
Linus Rath 1111dd6709 fix: show the focus outline on menu items, grid cells and other roving-focus controls 2026-10-29 19:38:44 +01:00
Linus Rath 01a6ab22d2 fix: name the plugin kit's buttons in the app language and read field hints and errors with the field 2026-10-29 18:02:58 +01:00
Linus Rath a7ce265ac8 feat: keep plugin notifications off the desktop while do not disturb is on 2026-10-29 17:53:48 +01:00
Linus Rath 8008393e59 feat: add an Accessibility page to Settings
Settings get a page of their own for accessibility, with presets on top
(screen reader, low vision, dyslexia, focus, keyboard and motor, motion
and seizure safe, deaf and hard of hearing) that change several options
at once and can be undone, then the options by topic, and a reset.

Text size used to be a fixed 14/16/18px on the root, which threw away
the size people set in their browser. It is now a percentage of the
browser's size, from 87.5% to 200%. Motion, contrast and transparency
follow the system by default and can be overridden either way; the
old Animations switch becomes Reduce motion. The visual options are
applied before the first paint, so the page no longer flashes at the
wrong size.

- `lib/accessibility-settings.ts`: the new settings, their allowed
  values, and the code that writes them onto `<html>`
- `app/accessibility.css`: bundled fonts (Atkinson Hyperlegible Next,
  Lexend, OpenDyslexic), text spacing, a focus outline that survives
  Windows contrast themes, more-contrast colours on any theme, reduced
  transparency, a large pointer
- admins can preset every new option but lock only shortcuts, send
  confirmation and the notification duration; a lock on text size or
  another reading aid works as a default
- always-light mail and toolbar labels now sync, and imports drop
  values the page doesn't know
- font size and animations move off Appearance, the plain text font
  off Reading; everyday settings such as conversation view and undo
  send show on both their own page and Accessibility
2026-10-29 17:53:32 +01:00
Linus Rath 8b35962efb fix: show section labels in sentence case instead of all caps 2026-10-28 22:26:39 +01:00
Linus Rath 8108249a35 Merge pull request #1406 from HOLOEYE-JZ/feat/plugin-ui-navigate
# Conflicts:
#	app/(main)/[locale]/layout.tsx
2026-10-28 17:02:42 +01:00
Linus Rath 96797597c5 Merge branch 'main' into feat/org-features 2026-10-28 15:14:34 +01:00
Jacob Zeitz ba45d0f8b9 feat(plugins): api.ui.openPage moves the tab to one of the plugin's own pages 2026-10-28 13:50:12 +01:00
Linus Rath 3a03bc363b Merge branch 'feat/org-templates' into feat/org-features 2026-10-28 09:01:42 +01:00
Linus Rath c00c7080a1 fix: give plugins the directory only with their own permission, and only confirmed team mailboxes
- `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.
2026-10-28 08:16:52 +01:00
Linus Rath 4aa4606b84 fix: give plugins no personal templates when the admin hid them 2026-10-28 07:34:23 +01:00
Linus Rath ccf9061b67 fix: make plugin switches that are off as visible as the app's 2026-10-28 07:16:53 +01:00
Linus Rath b5e92ba176 Merge branch 'feat/org-features' into feat/org-templates
# Conflicts:
#	lib/admin/restriction-keys.ts
2026-10-28 07:10:25 +01:00
Linus Rath a69d7eee33 feat: let plugins work with team mailboxes: list filters, row badge icons, account info and message conditions
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`.
2026-10-28 06:39:57 +01:00
Linus Rath 47c47b4f08 feat: keep personal templates in the account on Stalwart 1.0 2026-10-28 06:13:47 +01:00
Linus Rath 795faceee7 Merge branch 'feat/org-thirdparty' into feat/org-features 2026-10-28 05:51:22 +01:00
Linus Rath 9e809a31db Merge branch 'feat/org-features' into feat/org-thirdparty 2026-10-28 05:27:58 +01:00
Linus Rath 823da8c810 Merge branch 'feat/org-features' into feat/org-external
# Conflicts:
#	components/email/email-viewer.tsx
#	components/mail/mail-app.tsx
2026-10-28 05:19:21 +01:00
Linus Rath 3e1024957f fix: let every settings search result land on its row, match every word of a query, and turn on Advanced only for advanced rows 2026-10-28 04:31:53 +01:00
Linus Rath 1f1319d46e fix: hand the retired plugin's parts over one switch at a time and tell plugins about each switch 2026-10-28 03:54:47 +01:00
Linus Rath 553f1e9149 feat: add a switch that stops all third-party traffic #903 2026-10-28 02:19:06 +01:00