Commit Graph
79 Commits
Author SHA1 Message Date
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
Linus Rath 9bff740a89 fix: keep plugin network access to the origins in its manifest 2026-10-14 01:27:02 +02:00
Linus Rath c633185cda Merge branch 'main' into feat/stalwart-1.0
# Conflicts:
#	CONTRIBUTING.md
#	LICENSE
#	README.md
#	app/api/calendar-agenda/route.ts
#	app/api/wopi/files/[fileId]/contents/route.ts
#	app/api/wopi/launch/route.ts
#	components/calendar/calendar-app.tsx
#	components/calendar/calendar-day-view.tsx
#	components/calendar/calendar-month-view.tsx
#	components/calendar/calendar-week-view.tsx
#	components/calendar/event-modal.tsx
#	components/contacts/contact-detail.tsx
#	components/email/email-composer.tsx
#	components/email/email-list.tsx
#	components/files/file-browser.tsx
#	components/files/files-app.tsx
#	components/files/folder-tree-sidebar.tsx
#	components/filters/filter-rule-modal.tsx
#	components/mail/mail-app.tsx
#	hooks/use-mailbox-drop.ts
#	integration/run-tests.sh
#	lib/__tests__/calendar-agenda-route.test.ts
#	lib/__tests__/jmap-draft-lifecycle.test.ts
#	lib/__tests__/web-push.test.ts
#	lib/account-state-manager.ts
#	lib/jmap/client-interface.ts
#	lib/jmap/client.ts
#	lib/jmap/types.ts
#	lib/message-list-order.ts
#	lib/sieve/generator.ts
#	lib/sieve/parser.ts
#	lib/telemetry/payload.ts
#	lib/thread-utils.ts
#	lib/unified-mailbox.ts
#	lib/web-push.ts
#	lib/wopi/token.ts
#	locales/ar/common.json
#	locales/ca/common.json
#	locales/cs/common.json
#	locales/da/common.json
#	locales/de/common.json
#	locales/en/common.json
#	locales/es/common.json
#	locales/fa/common.json
#	locales/fr/common.json
#	locales/he/common.json
#	locales/hu/common.json
#	locales/it/common.json
#	locales/ja/common.json
#	locales/ko/common.json
#	locales/lv/common.json
#	locales/mn/common.json
#	locales/nb/common.json
#	locales/nl/common.json
#	locales/pl/common.json
#	locales/pt/common.json
#	locales/ro/common.json
#	locales/ru/common.json
#	locales/sk/common.json
#	locales/tr/common.json
#	locales/uk/common.json
#	locales/zh-TW/common.json
#	locales/zh/common.json
#	scripts/lite/lib.mjs
#	stores/__tests__/calendar-store-synthetic-ids.test.ts
#	stores/__tests__/email-store-shared-folder-keywords.test.ts
#	stores/__tests__/email-store-unified-actions.test.ts
#	stores/__tests__/file-store.test.ts
#	stores/email-store.ts
#	stores/file-store.ts
#	stores/message-list-tabs-store.ts
2026-10-12 08:35:08 +02:00
Linus Rath 22ffc36ca9 feat: let plugins run a message-list search with a JMAP filter #1024 2026-10-11 19:39:58 +02:00
Linus Rath 2522dbd804 fix: keep the plugin denylist and unit tests from breaking Lite and Docker builds 2026-09-26 21:38:57 +02:00
Linus Rath 0040e5daca fix: keep plugin storage per account and delete it on sign-out
`api.storage` kept one `plugin:<id>:` namespace for everyone signed in
to the browser and left it behind after signing out, so a plugin read
what it had stored for the previous account, such as quick-notes'
notes on someone else's mail.

Keys now carry the account (`plugin:<id>:@<account>:<key>`). Signing an
account out or removing it deletes its keys, and a full sign-out
deletes all plugin storage. Keys written before this change are handed
to the first account that uses the plugin. Uninstalling a plugin still
removes everything under its prefix.
2026-09-26 16:47:29 +02:00
Linus Rath 6d2255ad0d fix: keep plugin http.post away from credentialed and admin routes
`http.post` attaches the user's Authorization header to any same-origin
path the plugin's own manifest lists in `apiPostPaths`. A plugin with
only `http:post` could list the JMAP passthrough and get full JMAP
access, or reach `/api/admin/*`, `/api/settings` and the other
credentialed routes. The host now refuses those paths whatever the
manifest says, checked on the decoded path the router sees. Plugins
keep reaching their own sidecar routes.
2026-09-26 15:01:42 +02:00
Linus Rath f98779b0d0 fix: require manifest permissions for plugin hooks and slots
Only the host API methods checked a plugin's permissions. Every hook
and slot the sandbox reported was wired as-is, so a plugin declaring no
permissions could rewrite recipients and body of every outgoing mail
through `onTransformOutgoingEmail`, replace displayed bodies through
`onRenderEmailBody`, or read all fetched mail through
`onEmailsFetched`. Admin-installed plugins are managed, so users never
saw a consent prompt for any of it.

Each hook and slot now maps to a permission in
`lib/plugin-sandbox/permissions.ts`, checked the same way as API calls
(declared, and managed or granted). Hooks that change what is sent need
`email:send`, display takeovers need `email:render-takeover`, and a hook
or slot missing from the table is refused. Logout and account switches
stay implicit so plugins can always drop per-account state.
2026-09-26 14:26:33 +02:00
Linus Rath 5a553a315b fix: report refused read changes and re-register push at its limit
Follow-ups to the review of the push, keyword and identity work:

- A send that names no sender, like the List-Unsubscribe mailto: of the
  unified view, fell back to the first identity in server order, which
  on Stalwart 1.0 is a group's. It now takes the login's own identity,
  with addresses compared regardless of case.
- Re-register creates the replacement subscription before it destroys
  the old one, so at Stalwart's limit (15 per account by default) it
  failed with `overQuota`. It now frees the old record's place and
  tries once more.
- On 1.0 the push settings note goes by the emailpush capability alone
  and ignores refusals remembered under 0.16. On 0.16 a group the user
  belongs to is recognised by its identity, so a member without
  calendars, contacts or files keeps the group's junk filter and is no
  longer told the group sends no notifications.
- Plugin imports can no longer set Stalwart 1.0's attachment keywords,
  and `keywords.discover` leaves them out.
- Batch mark-as-read showed messages the server refused as read. It now
  updates only what was written and reports the rest, and thread
  expansion and category-tab moves keep what a partly refused batch
  did write.
- Refused read and star changes from shortcuts, the list, the viewer
  and Pro tabs show "Failed to update email" instead of an unhandled
  rejection. The automatic mark-as-read on opening stays quiet.
- A tag whose id predates the 128-byte limit can have its colour
  changed again.
- The state-regression guard notes that a multi-node 1.0 cluster can
  trip it harmlessly, at the cost of one extra full fetch.
2026-09-25 21:57:11 +02:00
Linus Rath d7004b3851 fix: report refused tags and keep Stalwart 1.0's attachment keywords intact
Stalwart 1.0 puts `$hasattachment` or `$hasnoattachment` on every
message and derives `hasAttachment` from them alone. It also refuses
keywords over 128 bytes (0.16 cuts them to 128 characters) and more than
100 keywords on one message. The keyword writers ignored Email/set
`notUpdated`, so a refused tag showed until the next reload and then
vanished.

- `markAsRead`, `batchMarkAsRead`, `toggleStar`, `updateEmailKeywords`,
  `setKeyword`, `removeKeyword` and `batchUpdateKeywords` check the
  Email/set outcome, and the errors carry the JMAP type. Tagging from
  the list or the viewer shows "Tagging failed"; a refused category-tab
  move rejects the plugin's promise and skips the categorize hooks.
- Tag ids are capped at 121 characters (128 bytes with `$label:`).
- The attachment keywords are hidden in view source, the plugin
  projection and the `getKeywords` fallback labels, reserved for
  category tabs, and refused by the plugin keyword API. Email/copy and
  the cross-server import still carry them over.
- A tag comes off by leaving its key out of the full keyword map instead
  of being sent as false, which RFC 8621 does not allow;
  `updateEmailKeywords` also drops the false entries local updates leave
  behind (`$seen: false` after marking unread).
- The automatic mark-as-read no longer leaves an unhandled rejection
  when the server refuses it.
2026-09-25 19:24:30 +02:00
bartfaizoli76 2adc281a9d feat: add ui.openDialog + 'plugin-dialog' slot for large, clickable custom UI
Every existing plugin slot renders wherever it's placed in the page (a
toolbar row, an attachment's hover-action bar, etc.) and is constrained
by that spot's own CSS - fine for small controls, but there's no way
for a plugin to show a large custom UI (a graphical file browser, a
multi-step wizard) since even a position:fixed overlay rendered from
inside such a slot is clipped to the surrounding row's own layout, not
the real page (confirmed live building the webdav-storage plugin: a
folder browser rendered but was squeezed into an unusable sliver).

Adds:
- host-dialog.ts: new 'custom' DialogKind + awaitCustomDialog(), reusing
  the existing confirm/alert/prompt queue plumbing.
- host-api.ts: 'ui.openDialog' API method.
- runtime.tsx: sandbox-side api.ui.openDialog(opts) proxy.
- plugin-types.ts: new 'plugin-dialog' SlotName - a plugin implements
  this slot with its actual custom UI.
- plugin-dialog-host.tsx: renders the requested slot via the existing
  PluginIframeSlot INSIDE the dialog's real app-root fixed overlay
  (position: fixed; inset: 0, above everything) instead of the
  confirm/prompt form, for kind: 'custom'. Resolves via a generic
  onResult callback in extraProps (marshalled the same way
  composer-attachment-source's onAttach already is - no new plumbing
  needed there).

A plugin calls api.ui.openDialog({ slot: 'plugin-dialog', extraProps,
title, width }) and awaits the Promise, which resolves to whatever its
own slot component passes to props.onResult(), or null if the user
closes the dialog without doing so.
2026-09-05 18:57:49 +02:00
Michael Richter 84617d6186 feat: OAuth handler for plugins; extended contacts and addressbook API 2026-08-31 09:55:06 +02:00
Roman Varlakov 7928f72ee2 feat: real byte progress for attachment uploads, stock and plugin-offloaded
The composer chip only knew a boolean: `uploading` flipped on, an
indeterminate bar animated at 40% width, and nothing anywhere carried a
percentage, so a large attachment on a slow link looked frozen for the
whole transfer. The JMAP client has been able to report real progress
since #333 gave uploadBlob an XHR path with onProgress; the composer
just never passed it.

Stock path: pass onProgress and the existing AbortController's signal to
client.uploadBlob. The chip's bar becomes determinate as soon as bytes
are reported (falling back to the indeterminate animation otherwise),
and cancel now aborts the transfer itself instead of being checked only
once the upload has finished.

Plugin offload path (#751): a plugin that offloads an attachment moves
the bytes itself via api.http.post, which the composer cannot observe.
It also cannot be handed a callback: the sandbox bridge marshals
functions host-to-plugin only, and the File a plugin holds is a
structured clone, so object identity cannot correlate its request back
to an attachment. The staged file id can: the composer generated it and
handed it to onBeforeBlobUpload. A plugin echoes it back as the new
optional `progressFileId` on the post options; the host sends the Blob
via XMLHttpRequest (fetch exposes no upload progress events) and reports
bytes into a host-side registry keyed by that id, which the composer
subscribes to before running the hook. Progress never crosses into the
sandbox, and reports for an id the composer is not tracking are dropped,
so the plugin API grows exactly one optional field. Calls without
progressFileId keep the fetch path untouched.

Closes #754
2026-08-23 19:44:56 +06:00
Paulhenry Saux d82a8e5ca5 feat(plugins): new plugin API method: getPublicKeyFromWKD 2026-08-22 20:55:45 +02:00
Paulhenry Saux e2f24e017f fix(plugins) : divide PRF workflow (getOrCreate) into two for better compatibility 2026-08-17 21:43:58 +02:00
Paulhenry Saux a5a12e11bc feat: allow privileged plugin to fetch only a range of blob 2026-08-15 18:28:05 +02:00
Paulhenry Saux 1e62029b6c feat(plugin): add isActive property to AccountResponse in plugin API 2026-08-14 13:26:08 +02:00
Ctaoist b09d5c857d fix: make label reordering atomic 2026-08-13 21:17:05 +08:00
Ctaoist 6e792f8834 feat: expose label reordering to extensions 2026-08-13 10:59:45 +08:00
Ctaoist cf89eaad27 feat: expose gateway keywords to extensions 2026-08-12 21:40:02 +08:00
Ctaoist ddfd9e43e6 feat: expose JMAP keyword helpers to extensions 2026-08-12 20:43:55 +08:00