Commit Graph
932 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 52188f54d6 Merge branch 'feat/enterprise-pack' into feat/enterprise-pack-f 2026-11-01 13:50:22 +01:00
Linus Rath e9b235bab2 Merge branch 'main' into feat/enterprise-pack
# Conflicts:
#	components/admin/admin-shell.tsx
#	lib/admin/outgoing-hosts-registry.ts
#	lib/admin/reachable-hosts.ts
#	scripts/lite/build.mjs
#	stores/policy-store.ts
2026-11-01 13:46:59 +01:00
Linus Rath eb26219b3c feat: send telemetry error counts within a minute when an admin turns on instant mode 2026-11-01 12:55:58 +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 91be465cbc feat: version the service worker by build and offer a reload after a deploy 2026-11-01 11:13:31 +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 6e931a731c fix: keep the server-side device policies on when a login's session or the policy can't be read 2026-11-01 09:44:03 +01:00
Linus Rath 61733d839f fix: show an announcement that arrives after its start, once in the Pro shell, and keep it closed when only its times move
- The banner judged its start and end by the time it was mounted, so an
  announcement fetched later than its start never showed in that tab.
- The Pro shell shows it once above its panes; email and folder tabs had
  none and split panes showed it twice.
- Screen readers get it from a live region that fills a moment after it
  is in the page; text present when a live region appears isn't read out
  reliably. The tests that checked the strip's role now check that region.
- Moving the start or end no longer shows a closed announcement again;
  the lib test that expected a new end time to reset it now expects the
  opposite.
2026-11-01 09:39:36 +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 140db2cf8f feat: turn printing and export off in the office editor under the device policies 2026-11-01 08:40:03 +01:00
Linus Rath adbec718ef feat: let admins keep sender and subject out of push notifications, enforced by the server
The push preview route now reads the device policy of the login it
serves (its scopes worked out from the login's own JMAP session, every
non-personal account counted as a group) and under
`pushWithoutContent` asks the mail server for the message's ids only,
so sender, subject and text never reach the notification. The service
worker shows "New mail" for such an answer. A policy that can't be read
counts as on.

The three existing push preview route tests now mock the config manager
with the default policy: without it the route can't read a policy in the
test environment and holds the text back, which those tests didn't mean
to cover. No assertion changed.
2026-11-01 08:40:02 +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 2a952f4b72 Merge branch 'feat/enterprise-pack-c'
# Conflicts:
#	lib/sign-out-cleanup.ts
2026-11-01 07:39:30 +01:00
Linus Rath ebb33619a0 Merge branch 'feat/enterprise-pack-a' 2026-11-01 07:36:59 +01:00
Linus Rath 870881c57e fix: fetch a company calendar once for everyone at a time and pause after a failure 2026-11-01 06:51:17 +01:00
Linus Rath 0fcdc2ece4 fix: reach a company calendar on a private address only when its host names the intranet 2026-11-01 06:35:56 +01:00
Linus Rath f007d01c29 fix: keep company calendar addresses back without a session secret 2026-11-01 06:30:47 +01:00
Linus Rath 3bd8e83d60 feat: show company calendars set by the admin 2026-11-01 05:41:34 +01:00
Linus Rath d714957f98 feat: import mbox, EML and ZIP mail with progress, pause and resume 2026-11-01 04:58:55 +01:00
Linus Rath 50f95f4463 feat: report a message as phishing and move it to Junk 2026-11-01 04:58:01 +01:00
Linus Rath ab2fc6454d feat: let admins share every user's free/busy with a group 2026-11-01 04:51:30 +01:00
Linus Rath 123018f322 feat: add the policy sections and admin pages for the enterprise features 2026-11-01 04:20:02 +01:00
Linus Rath 83a19f3329 docs: point code comments at the monitoring guide where the website has it 2026-11-01 03:49:54 +01:00
Linus Rath d49839435b feat: send OpenTelemetry traces when an OTLP endpoint is configured
With `OTEL_EXPORTER_OTLP_ENDPOINT` or
`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` set (and neither
`OTEL_SDK_DISABLED=true` nor `OTEL_TRACES_EXPORTER=none`), Bulwark
loads the OpenTelemetry SDK at startup and exports Next's own spans
plus a client span for every call to Stalwart, WOPI, translation, the
plugin proxy and OAuth over OTLP/HTTP. Calls to Stalwart carry a
`traceparent` header, so Stalwart's spans join the trace. All other
settings come from the standard `OTEL_*` variables.

Every span is scrubbed before export: URLs keep their origin only,
request paths become their route group, query strings, user agents and
client addresses are dropped, and addresses or credentials in other
attributes are removed. Without an endpoint the SDK is never loaded.
The endpoint is listed under the hosts that stay reachable with no
third-party traffic, and System > Monitoring shows whether tracing runs.
2026-11-01 03:25:28 +01:00
Linus Rath e580ed24da feat: send anonymous error counts with the usage stats when the admin also agrees to it
A second switch under System > Usage stats, "Also send anonymous error
counts", or `BULWARK_TELEMETRY_ERRORS=on|off`. Off by default, off
whenever the heartbeat is off or no third-party traffic is on, and
switching the heartbeat off switches it off too. With it on the
heartbeat becomes schema 2: the v1 fields unchanged plus `errors`, up to
50 fingerprints of the last complete UTC day with the error name, the
top frames in Bulwark's bundles (path, line, column, debug ID), the
version, a bucketed count, on how many of the last 7 days it occurred,
and a message only when it is a fixed error code. Without the consent
the heartbeat stays exactly schema 1. The admin preview shows the v2
payload as soon as the switch is flipped.
2026-11-01 03:10:41 +01:00
Linus Rath 481c4b6030 feat: serve Prometheus metrics and split health into live and ready
- `/api/metrics` in Prometheus text format, off until `METRICS_TOKEN`
  or `METRICS_TOKEN_FILE` is set (404 before, 401 without the bearer
  token): requests per route group with status class and latency,
  sign-in outcomes, calls to Stalwart, WOPI, translation, the plugin
  proxy and OAuth, browser errors, errors today, process memory,
  event-loop lag, uptime and build info. Labels come from fixed lists.
  Each replica serves its own.
- `/api/health/live` answers while the process runs;
  `/api/health/ready` checks the config, the shared store, writable
  data dirs and Stalwart's session resource. `/api/health` and
  `/api/ready` are unchanged.
- the Docker image gets a HEALTHCHECK on the live endpoint
- adds prom-client, and the OpenTelemetry packages the tracing step
  loads on demand
2026-11-01 02:59:40 +01:00
Linus Rath cba1109444 feat: report browser and server errors to an error tracker the admin sets
Admins had logs and nothing else: client errors only reached the
browser console. An admin can now set a Sentry-protocol address (DSN)
for Bugsink, GlitchTip or Sentry under System > Monitoring, or with
`ERROR_REPORTING_DSN`, `ERROR_REPORTING_DSN_FILE`,
`ERROR_REPORTING_SAMPLE_RATE` and `ERROR_REPORTING_ENVIRONMENT`.
Bulwark Lite reads the same from `errorReporting` in policy.json.
Everything stays off while no address is set.

- browsers report errors from the error boundaries, window.onerror,
  unhandled rejections and failed JMAP requests (method and error type
  only), with route and JMAP method breadcrumbs and the chunks' debug IDs
- the server reports what Next hands to onRequestError and what routes
  log with logger.error
- every report is scrubbed first: quoted text, addresses, URLs, file
  names, tokens and the rest after the first line are removed, and the
  server scrubs what browsers post to /api/errors again
- reports are deduplicated and rate-limited per session and process
- errors are counted per day for "errors today" on the admin page
- startup messages and audit entries go through the logger, so
  `LOG_FORMAT=json` covers them
2026-11-01 02:50:19 +01:00
Linus Rath d781e8762e Merge branch 'feat/multi-instance' 2026-11-01 01:43:15 +01:00
Linus Rath 2e5397c228 test: mock the wizard session refusal in the setup telemetry consent test 2026-11-01 01:35:20 +01:00
Linus Rath 38bc06f2d3 fix: answer 503 for plugin lists and marketplace installs while the shared store is down #1066
directory-keys-shared.test.ts: the store-down case now expects the store error instead of an 'invalid' signature, since a store outage no longer counts as a bad signature.
2026-11-01 01:03:13 +01:00
Linus Rath d78ce02f78 fix: refuse a plugin install that can't hold the registry lock and a bundle that doesn't match its entry #1066 2026-11-01 01:01:19 +01:00
Linus Rath 256855b294 fix: refuse to sign plugins on an instance whose signing key differs from the others #1066 2026-11-01 00:54:57 +01:00
Linus Rath 905d841be4 chore: drop the outdated Stalwart/RocksDB warning from files 2026-11-01 00:41:41 +01:00
Linus Rath ecdd2b54fe chore: name the app Bulwark Webmail in exported files, outgoing requests and the manifest 2026-11-01 00:41:21 +01:00
Linus Rath 355e57ca6c Merge branch 'feat/multi-instance' into feat/multi-instance-ext 2026-11-01 00:28:15 +01:00
Linus Rath 3c34eda260 fix: cap the cache of Stalwart admin checks so it can't grow without bound 2026-11-01 00:25:35 +01:00
Linus Rath 5efccb65e0 fix: show a re-uploaded logo or icon at once instead of after a restart 2026-11-01 00:24:07 +01:00
Linus Rath fc7d5617c2 feat: serve uploaded branding from the shared store on every instance #1066 2026-11-01 00:20:24 +01:00
Linus Rath 51e7999e3c feat: sign plugin bundles with one key on every instance #1066 2026-11-01 00:16:42 +01:00
Linus Rath d73494593a refactor: let the config write error answer the 503 in the plugin and theme routes 2026-11-01 00:15:14 +01:00
Linus Rath 2078b0a13f Merge branch 'feat/multi-instance-auth' into feat/multi-instance 2026-11-01 00:13:17 +01:00
Linus Rath 5e4b6465d1 Merge branch 'feat/multi-instance' into feat/multi-instance-ext 2026-11-01 00:13:14 +01:00
Linus Rath e3936287fd feat: share plugin approvals and plugin settings between instances #1066 2026-11-01 00:13:09 +01:00
Linus Rath 89f337c0ae Merge branch 'feat/multi-instance-admin' into feat/multi-instance 2026-11-01 00:12:24 +01:00
Linus Rath 47281e5156 fix: answer 503 when an impersonation link's use can't be recorded and keep used links as expiring keys #1066
The marker-write test from d28ee361c now expects the 503 path (a rejected promise) instead of false: refusing with "Token already used" sent admins looking for a replay that never happened, and the jti is now remembered in memory only after it was recorded, so a retry works once the store or state dir is back.
2026-11-01 00:07:41 +01:00
Linus Rath ff08fe50fe fix: say when an admin sign-out or password change couldn't be stored instead of answering ok or wrong password #1066 2026-11-01 00:05:15 +01:00
Linus Rath df1f5cd6a4 fix: give a pair step-up attempt back only to the windows it was counted in #1066 2026-11-01 00:05:09 +01:00
Linus Rath bcb333d40c feat: share installed plugins and themes between instances #1066 2026-11-01 00:04:15 +01:00