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
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.
- 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.
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.
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.
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.
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.
- `/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
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
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.
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.