Commit Graph
176 Commits
Author SHA1 Message Date
Linus Rath 96797597c5 Merge branch 'main' into feat/org-features 2026-10-28 15:14:34 +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
Linus Rath 45cc6988e0 feat: sign out after an admin-set idle time or session length, with a warning first 2026-10-28 02:18:07 +01:00
Linus Rath ab878ebf4f feat: tell plugins when Bulwark's own external mail warnings are on and skip the retired plugin's 2026-10-28 02:10:10 +01:00
Linus Rath 2f8b4863ff Merge pull request #1338 from HOLOEYE-JZ/feat/plugin-search-results 2026-10-26 19:32:56 +01:00
Linus Rath 7c00a02319 Merge pull request #1345 from HOLOEYE-JZ/feat/plugin-action-badge 2026-10-26 19:32:08 +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 fb793220b7 fix: keep plugin switches from shrinking next to long labels, like the app's settings switches 2026-10-26 14:52:04 +01:00
Linus Rath c4a399f5b8 fix: stop plugin moves from filing another account's messages while its folder is open 2026-10-26 11:29:37 +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 d02988f431 feat: show the results plugins provide through onProvideSearchResults
The hook was called on every search, but nothing showed its entries,
the list waited for it, and an entry's onClick could not cross from the
plugin's sandbox. Its entries are now drawn as a block above the
search's message list, a group per plugin and source.

Each plugin is asked on its own (HookBus.collect) beside the search, so
the list no longer waits, and the answer to a search that was replaced
is dropped. The answer is checked: at most 20 entries, a key and a
title, plain text cut to its limits. A click goes to the plugin that
gave the entry as onAction with surface "search-results", the entry's
key and the query; ExternalSearchResult loses onClick.

Closes #1336

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:22:33 +01:00
Jacob Zeitz 4a130bc1f9 feat: let plugins show a count on their buttons
An action declared with ui.setActions may carry a badge, a whole number
from 0. On buttons it is drawn as the search's filter button draws its
count: the button tinted, the number in a bubble at its corner, "99+"
past two digits. In menus it follows the label, as the mobile header's
notification count does. 0 shows none.
2026-10-25 14:36:01 +01:00
Linus Rath 171fd1ea4c perf: load React DOM and the UI kit only in plugin frames that draw 2026-10-25 02:56:14 +01:00
Linus Rath 5edbbf0816 perf: start plugin slot frames without waiting for the plugin's own frame 2026-10-25 02:24:08 +01:00
Linus Rath e38e326efb perf: load plugin frames as a plain page instead of a Next app
Every plugin iframe loaded /plugin-sandbox, a force-dynamic Next page.
Each frame then fetched, parsed and hydrated Next's client router on top
of React and the runtime, and the server rendered the page once per
frame, one after another when a dozen plugins started together.
Sandboxed frames have opaque origins, so the browser's code cache never
helps, and they run on the host page's main thread: with 12 plugins
the frames took 2.5-3 s to come up after the app had loaded.

The sandbox routes now return a small HTML page with the UI kit's
stylesheet and the runtime as an ES module, both built ahead of time:

- `scripts/build-plugin-sandbox.mjs` bundles the runtime with esbuild
  and compiles `plugin-kit.css` with the app's Tailwind setup into
  `public/plugin-sandbox-assets/`, with a manifest. Named Tabler imports
  are pointed at the icons' own files, so the full icon set stays in a
  chunk that only loads for an icon name the kit doesn't ship.
- `npm run build`, `npm run dev`, the Dockerfile and the perf-guard
  workflow run it before `next build`.
- `lib/plugin-sandbox/sandbox-document.ts` writes the page; proxy.ts
  still sets the sandbox CSP and the script carries its nonce.
- The assets are served from public/ with CORS and immutable caching.

A frame on an idle page is ready in 255 ms instead of 362 ms, eleven at
once in 1.0 s instead of 2.1 s.
2026-10-25 02:01:47 +01:00
Linus Rath 181a0d7d88 perf: send plugin frames only the translations of the current language 2026-10-25 02:26:17 +02:00
Linus Rath 52959e8135 fix: stop plugin panels flashing white in the dark theme 2026-10-25 02:22:59 +02:00
Linus Rath 782d4f8bf5 perf: stop loading the app's stores in every plugin frame 2026-10-25 02:09:37 +02:00
Linus Rath 7370b6bad0 Merge pull request #1335 from tobiasge/feat/plugin-regional-formatting
feat: mirror regional presentation settings to plugin sandboxes
2026-10-24 17:55:06 +02:00
Linus Rath 6d44e9320c Merge pull request #1333 from HOLOEYE-JZ/feat/plugin-keywords-update-remove
feat: let plugins update and remove the tags they added
2026-10-24 17:54:58 +02:00
Tobias Genannt 2b5a9dbfc5 feat: mirror regional presentation settings to plugin sandboxes
Plugins running in iframe sandboxes previously lacked access to the user's regional formatting settings (dateLocale, timeFormat 12h/24h, timeZone, and firstDayOfWeek) unless they requested privileged settings:read permissions.

This mirrors the host's resolved presentation preferences into the plugin sandbox:
- Adds RegionalSnapshot (locale, dateLocale, hour12, timeZone, firstDayOfWeek) to BackgroundInit, SlotInit, and LocaleChangeMsg.
- Implements snapshotRegionalSettings() in host-bridge, reusing host resolveDateLocale() and getEffectiveTimeZone().
- Subscribes stores/plugin-store.ts to useSettingsStore to broadcast updates to running sandboxes when regional preferences change.
- Exposes api.i18n.formatTime(date, options) and api.i18n.formatDate(date, options) along with regional getters on api.i18n.
2026-10-24 11:01:13 +02: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
ghoztyandClaude Opus 5.5 30f51e5ca4 feat: name the app with the configured APP_NAME in user-facing strings
Some user-facing strings still said "Bulwark" on a deployment with its own
APP_NAME: Settings > General > Default apps (protocol handlers), the "Open
Bulwark" notification, the rate-limit banner and toast, the plugin connect
dialog, one plugin permission text and the connector page.

These strings now take an {appName} placeholder, filled by productName():
the configured APP_NAME, or "Bulwark" when it is unset or the default
"Webmail" (the same rule as the TOTP issuer and the About card, #1160).
A stock install reads as before.

The settings search (the settings page and api.app.searchSettings) indexes
these texts with the name filled in, so "bulwark" or the configured name
finds Default apps and no result carries the raw placeholder.

Locales: the 13 keys in every locale that has "Bulwark" in them. Languages
that inflect the name (cs, et, hu, ko, ro, sk, tr) use a neutral form such
as "{appName} uygulamasında" instead of a declined "Bulwark'ta"; a native
check of those is welcome.

Not changed on purpose: the mobile app, push relay, Lite, demo and admin
texts, which speak about Bulwark itself.

Closes #1314

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 04:14:54 +03:00
Linus Rath b586dc467c fix: keep a plugin's passphrase dialog open while the user types #1302 2026-10-20 21:01:02 +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 95003fd28b feat: let a plugin panel start folded until the user or the plugin opens it 2026-10-20 17:21:13 +02:00
Linus Rath 95bfb1681c feat: let a plugin's panel frame fill the panel and scroll inside 2026-10-20 17:21:13 +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 1f11cc7249 fix: stop talking to a plugin frame once it has navigated to another document 2026-10-19 01:32:27 +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 1e992e6c7f fix: check plugin dropdown messages before the host draws them 2026-10-18 14:39:59 +02:00
Linus Rath 7573471544 fix: keep plugins from reading mail of other connected logins #1273 2026-10-18 14:39:58 +02:00
Linus Rath 5aba2a92c5 Merge pull request #1251 from tobiasge/feat/plugin-ui-kit-combobox
feat(plugins): add Combobox component to UI kit with host floating dropdown portal
2026-10-18 14:34:26 +02:00
Linus Rath 24e7e738bd Merge pull request #1274 from alyyousuf7/fix/plugin-email-get-shared-accounts
fix: let api.email.get read a listed message of a shared account
2026-10-18 14:34:18 +02:00
Linus Rath 86d6c78ce7 fix: keep a plugin slot's callbacks working when its props change while it loads #1266 2026-10-18 13:46:47 +02:00
Ali Yousuf 6fe9f8f7f1 fix: let api.email.get read a listed message of a shared account
doEmailGet asked the signed-in account for every id, so a message in a
folder shared from another account came back as null even though it was
in the list. A message in the loaded list is now read from the account
that owns it, through resolveEmailBlobContext like attachment downloads.
getAttachment and getRaw keep reading from the signed-in account only.

Fixes #1273
2026-10-17 18:41:31 -04:00