99 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 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 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 1c7f81bcc6 feat: pick a classification label in the composer and stamp it on sent mail 2026-11-01 05:33:12 +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
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 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
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 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 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 553f1e9149 feat: add a switch that stops all third-party traffic #903 2026-10-28 02:19:06 +01:00
Linus Rath e120ac6361 fix: let plugins read the message of the account they name, as ids repeat across accounts 2026-10-26 15:15:58 +01:00
Linus Rath cf2c27157d Merge branch 'main' into feat/stalwart-1.0
# Conflicts:
#	components/contacts/contacts-app.tsx
#	components/email/__tests__/composer-discard.test.tsx
#	components/email/calendar-invitation-banner.tsx
#	components/email/email-composer.tsx
#	components/mail/mail-app.tsx
#	components/pro/pro-compose-tab-body.tsx
#	components/pro/pro-email-tab-body.tsx
#	components/ui/__tests__/avatar.test.tsx
#	components/ui/avatar.tsx
#	hooks/use-mailbox-drop.ts
#	lib/__tests__/calendar-alerts.test.ts
#	lib/__tests__/jmap-calendar-tasks.test.ts
#	lib/__tests__/jmap-send-threading.test.ts
#	lib/demo/demo-client.ts
#	lib/demo/fixtures/calendars.ts
#	lib/jmap/client-interface.ts
#	lib/jmap/client.ts
#	lib/web-push.ts
#	stores/__tests__/contact-store.test.ts
#	stores/__tests__/email-store-thread-key.test.ts
#	stores/calendar-store.ts
#	stores/contact-store.ts
#	stores/email-store.ts
2026-10-26 10:14:12 +01:00
Jacob ZeitzandClaude Opus 5.5 372fc9ec71 feat: let plugins update and remove the tags they added
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>
2026-10-24 08:54:23 +02:00
Linus Rath d9d6eb9be7 feat: let plugins search the settings and read the user's preferences 2026-10-20 20:27:08 +02:00
Linus Rath 647c24132c feat: let plugins read the app's name and version with api.app.getInfo 2026-10-20 20:19:38 +02:00
Linus Rath a191ef8b02 feat: let plugins search mail with api.email.search 2026-10-20 18:16:29 +02:00
Linus Rath 2bb5a62ac4 feat: let a plugin open, close and toggle the panel its slot sits in 2026-10-20 17:21:13 +02:00
Linus Rath 685ce7f469 fix: keep plugin POSTs off the plugin proxy, secrets, agenda, ical and translate routes 2026-10-19 01:04:45 +02:00
Linus Rath 41a0716c50 Merge branch 'main' into feat/stalwart-1.0 2026-10-16 01:41:48 +02:00
Linus Rath 9d8f762b1d feat: let a plugin title its sidebar section 2026-10-14 11:47:25 +02:00
Linus Rath 5d83d76436 feat: let plugins open a reply to the open message with text to start from 2026-10-14 11:37:51 +02:00
Linus Rath cbeeca3ea5 feat: let plugins edit the message being written from composer buttons and menus 2026-10-14 11:35:00 +02:00
Linus Rath 7f7f6bf949 fix: check the name, type and size of blobs plugins upload, and the email id of email.setKeyword and email.removeKeyword 2026-10-14 10:00:03 +02:00
Linus Rath 78f72e0c03 fix: give plugins settings that change after they started, let their frames copy to the clipboard and tell them the user's time zone 2026-10-14 09:01:03 +02:00
Linus Rath 85c6cbcac8 feat: let plugins move messages between folders and archive them 2026-10-14 06:31:07 +02:00
Linus Rath 72be961383 feat: let plugins create repeating events under a stable id of their own 2026-10-14 06:29:15 +02:00
Linus Rath 1f89e89ce5 feat: let plugins refresh their message-list badges, which now need only the read permission 2026-10-14 06:26:10 +02:00
Linus Rath 49511dff38 feat: let plugins read their own non-secret admin settings without the admin permission 2026-10-14 06:23:10 +02:00
Linus Rath 8ad8f44e78 Merge branch 'feat/plugin-host-apis' into feat/plugin-system-audit
# Conflicts:
#	lib/plugin-sandbox/permission-text.ts
#	lib/plugin-sandbox/permissions.ts
#	lib/protocol-handlers/mailto.ts
#	locales/en/common.json
2026-10-14 04:34:14 +02:00
Linus Rath 5d9268c923 feat: let plugins add buttons, calendar day labels and pages, and use connected accounts from the sandbox 2026-10-14 04:31:43 +02:00
Linus Rath 3a65964d26 feat: let plugins show notifications that name the plugin 2026-10-14 04:26:16 +02:00
Linus Rath ead925c1b8 feat: let plugins open a filled-in composer and save drafts 2026-10-14 04:23:17 +02:00
Linus Rath 08e2d65944 feat: let untrusted plugins save attachments and message sources of a given message 2026-10-14 04:16:31 +02:00
Linus Rath 8fd6bc42c3 feat: let plugins read any message, list a folder and see the folders 2026-10-14 04:13:33 +02:00
Linus Rath 121d8ba30c feat: let plugins manage templates, sender signatures and the out-of-office reply 2026-10-14 04:06:06 +02:00
Linus Rath 3df031edd1 feat: let plugins read and change calendar events and tasks 2026-10-14 04:02:47 +02:00
Linus Rath 8ef7ec022f fix: deliver a plugin OAuth callback only to the plugin that started the flow 2026-10-14 03:04:17 +02:00
Linus Rath cef2438c33 fix: let untrusted plugins upload attachments for the composer again 2026-10-14 02:34:31 +02:00
Linus Rath a491eab317 fix: hash WKD names with WebCrypto instead of shipping a Node crypto polyfill to the browser 2026-10-14 02:31:39 +02:00
Linus Rath 23c4c654d7 fix: limit what a plugin can store, download, toast and send to a WKD lookup, and report refused admin config writes 2026-10-14 02:30:54 +02:00
Linus Rath 22846b4a9b fix: keep blob uploads privileged-only and derive the WebAuthn salt from the calling plugin 2026-10-14 02:28:11 +02:00