Commit Graph
243 Commits
Author SHA1 Message Date
Linus Rath 22a783efe0 fix: check an invitation's sender also when it claims one of the user's own addresses 2026-11-03 00:43:16 +01:00
Linus Rath 6ef1397a53 fix: refuse to switch the out-of-office on for a plugin while it forwards mail or that can't be read 2026-11-03 00:42:30 +01:00
Linus Rath 23c410ddea fix: ask the server whether a folder is shared before a plugin's move, filter or archive files mail there 2026-11-03 00:29:02 +01:00
Linus Rath 267ca40522 Merge branch 'feat/assistant-everywhere' into feat/assistant-c 2026-11-03 00:24:12 +01:00
Linus Rath 82c3c969be fix: keep calendar.deleteEvent working for callers whose client names no account 2026-11-03 00:00:17 +01:00
Linus Rath 363c797178 feat: let a plugin dialog fill the window's visible height, so a chat fits above a phone's keyboard 2026-11-02 23:59:57 +01:00
Linus Rath ad36f8d382 fix: show the side panel only where the plugin does, and keep it steady
- The button and the panel of Calendar, Contacts, Files and Settings
  show only when the plugin's shouldShow says yes, so a plugin switched
  off for these views (admin, user or no service) leaves no button that
  opens an empty panel, and no extra rows in Contacts and Settings.
- Files: while the details are open the panel steps aside there only;
  the other views keep it as the user left it. With both stored open the
  details fold when the page loads.
- Resizing stops where the view keeps its 480 px, so a drag or the arrow
  keys no longer fold the panel away (and the handle keeps the focus).
- Settings and Contacts render the same element with or without the
  panel, so a form keeps what was typed when the panel starts docking.
- Contacts on a phone: the panel button sits over an open contact too.
- The panel buttons of the reader and the views have one name, "Plugin
  panel", with aria-pressed, and name the plugin's panel shortcut in
  their title; the open panels are named landmarks.

The reader tests look the button up by its new name and pressed state
(viewer-toolbar-over-side-panel, reader-plugin-surfaces,
viewer-plugin-panel-width), since "Show/Hide plugin panel" is gone.
2026-11-02 23:54:53 +01:00
Linus Rath a256beb918 fix: tell each plugin whether it may show the side panel
api.host.features.sidePanel now says whether this plugin may offer the
app-side-panel (ui:side-panel granted), not only that the webmail has
it. A plugin enabled before an update added the permission can then say
so instead of offering a switch that does nothing. The host computes the
assistant's flags per plugin for a frame's init and for each push.

plugin-host-features.test.ts: the host bridge assertions take the
assistant's flags as the test plugin sees them (it lacks ui:side-panel,
so sidePanel is false there).
2026-11-02 23:54:52 +01:00
Linus Rath ecaad2b3d9 Merge branch 'feat/assistant-everywhere' into feat/assistant-f 2026-11-02 23:37:41 +01:00
Linus Rath 89df82adce fix: answer an invitation whose event lies outside the calendar's loaded weeks 2026-11-02 23:29:43 +01:00
Linus Rath d3d150d2c4 fix: let plugins answer invitations found by a search of the user's own mail 2026-11-02 23:20:54 +01:00
Linus Rath fd30fdcd27 feat: let plugins answer invitations, add filter rules, change many messages and change settings, with undo
The AI assistant's "more actions" need host calls today's plugin API
lacks. Area c of lib/plugin-sandbox/assistant adds them:

- `calendar.respond(emailId, status)`, new permission calendar:respond:
  answers an invitation of the user's own account through
  lib/calendar-rsvp.ts, like the reader's banner
- `calendar.deleteEvent` now returns an undo token for a single event;
  `calendar.undoDelete` creates it again from the host's copy
- `filters.list/create/undo` over Settings > Filters' model; forward,
  redirect, reject, discard and keep are refused, and so are folders
  that are shared or not the account's own
- `email.batch/undoBatch`: up to 200 messages through the store's
  batch functions, restore points first, one entry in the undo history
- `vacation.get` also says whether the out-of-office forwards mail
- `app.describeSettings`, `app.setSettings` and `app.undoSettings`, new
  permission settings:change, over the allow-list in
  lib/settings-schema.ts, refusing locked, hidden, restricted and gated
  settings when the change is made

Undo tokens stay in the host for 30 minutes, bound to the plugin and
the account. app.getSettings now shows every setting setSettings may
change. settings:write's text is reworded to what it gates (tags).
plugin-host-api-app-info gains a test; its other assertions are as
they were.
2026-11-02 23:14:23 +01:00
Linus Rath adb32aba3a fix: turn off a host flag the webmail stops naming 2026-11-02 23:03:49 +01:00
Linus Rath 8fcd647215 feat: let plugins show a side panel next to Calendar, Contacts, Files and Settings 2026-11-02 21:59:36 +01:00
Linus Rath dda31538af fix: hand the assistant's host flags to plugins in api.host.features
The sandbox runtime copied only the three policy flags from the host's
features, so a flag an assistant area adds (sidePanel, files, ...) never
reached api.host.features and the plugin hid the feature. The runtime
now copies every other flag the host names as true or false.

The host bridge assertions compare against withAssistant(...) like the
snapshot ones already did, because the init payload and host-change
carry the assistant's flags on top of the three policy flags.
2026-11-02 21:59:35 +01:00
Linus Rath e044299dea chore: drop lint directives the assistant's area files don't need 2026-11-02 21:21:39 +01:00
Linus Rath f7ff8212a3 refactor: give the AI assistant's host APIs one set of files per area
The assistant's new host calls come in four areas (context, actions,
Files, the panel in every view), built side by side. Each area gets its
own files under lib/plugin-sandbox/assistant for its permissions, method
table, dispatcher, host feature flags and sandbox methods; the shared
files only read the merged result. No area adds anything yet. The host
features test now compares against the core flags plus the areas' flags.
2026-11-02 21:21:06 +01:00
Linus Rath 55f1875f20 refactor: take quick reply plugin recipients from the quick reply envelope #1445 2026-11-01 18:18:48 +01:00
Linus Rath 5caf3378b2 Merge pull request #1445 from illuwa-soft/feat/upstream-quick-reply-actions
feat: offer composer plugin actions in quick reply
2026-11-01 18:15:18 +01:00
Linus Rath dd94a2b7c8 refactor: keep composer snapshot comments in English #1443 2026-11-01 18:15:13 +01:00
Linus Rath 4a597eb0dc Merge pull request #1443 from illuwa-soft/feat/upstream-composer-read-api
feat: add composer snapshots and conditional edits

# Conflicts:
#	lib/__tests__/plugin-sandbox-runtime-host-gate.test.ts
#	lib/plugin-sandbox/host-api.ts
2026-11-01 18:13:10 +01:00
Linus Rath 4cd75f4d07 refactor: keep the toast limits named and comments in English #1442 2026-11-01 18:12:42 +01:00
Linus Rath 6915813262 Merge pull request #1442 from illuwa-soft/feat/upstream-progress-toasts
feat: expose frame-scoped plugin progress toasts

# Conflicts:
#	lib/plugin-sandbox/host-bridge.ts
2026-11-01 18:11:12 +01:00
Linus Rath b903ba1e7a Merge pull request #1482 from HOLOEYE-JZ/feat/plugin-task-links
feat: let plugins set task links and related mail
2026-11-01 18:10:57 +01:00
Linus Rath cc6b8899d8 Merge pull request #1481 from HOLOEYE-JZ/feat/plugin-task-alerts
feat: let plugins set task alerts
2026-11-01 18:10:56 +01:00
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
Jacob Zeitz d68959d3b6 feat(plugins): tasks carry links and related mail
api.tasks.create and update take links by id (an http(s) URL, a mailto:
address or a path of this Bulwark, stored as its full URL; at most ten)
and relatedTo (by a mail's Message-ID or another object's UID, with its
relation types; at most ten). They merge on update as alerts do: an
entry given is set, one given null is removed, and entries the plugin did
not name, such as an attachment's link another app wrote, stay as the
server holds them.

TaskView returns both as any app wrote them. Lengths, ids and URL
schemes are checked as the rest of the calendar API checks them. Task
reads now ask the server for links too.
2026-10-31 19:56:15 +01:00
Jacob Zeitz e2fc2c3c90 feat(plugins): tasks carry alerts
api.tasks.create and update take alerts by id: a trigger from the task's
due (or start) as an ISO 8601 duration, or at an instant in UTC; display
or email; at most five. An update merges by id, each alert written by its
JMAP patch path: an alert given is set, one given null is removed, and
alerts the plugin did not name, such as one another app wrote, stay as
the server holds them; null for the whole map removes every alert.
Whether the map is patched or written whole is decided on the task as the
server holds it, not an older copy. An alert kept with the same trigger
and action stays acknowledged, so a snooze is an update of its trigger,
which then fires again. While a task uses its calendar's default alerts,
a change makes them the task's own under new ids, as the task form does,
an alert under a default's id is refused rather than dropped, and
removing every alert turns the defaults off too.

TaskView returns the alerts, each with when it was acknowledged, and
useDefaultAlerts. api.tasks.acknowledgeAlert dismisses one alert (or
undoes that with null) by writing only its acknowledged. A plugin's
writes to one task go one after another, since Stalwart refuses a set
while another is under way on the same object. The task store applies
patch paths to the task the caller read, so a task the list does not
hold is shown and reported whole.
2026-10-31 19:52:02 +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
yontani dab71061f8 feat: offer composer plugin actions in quick reply 2026-10-30 08:50:15 +09:00