Commit Graph
372 Commits
Author SHA1 Message Date
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 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 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 bcb333d40c feat: share installed plugins and themes between instances #1066 2026-11-01 00:04:15 +01:00
Linus Rath 740a892590 fix: answer 503 for admin and setup saves while the shared store is down #1066
Config, policy, branding, OAuth setup, password and setup wizard writes
used to answer 500 when the shared store couldn't be reached, and the
wizard's token check failed outside any handler. They answer 503 with
Retry-After now, through the helper that already turned a read-only
refusal into 409. The step-session-secret test's wizard mock gains
wizardSessionRefusal, which the setup routes now call.
2026-11-01 00:02:57 +01:00
Linus Rath 48ac0d9196 fix: keep the stored heartbeat schedule when the setup wizard sets telemetry consent #1066 2026-11-01 00:01:14 +01:00
Linus Rath 46f47b6b8f fix: answer 409 instead of 500 for every admin save a read-only config refuses #1249 2026-10-31 23:59:00 +01:00
Linus Rath 37696f9cf7 fix: answer 503 on the update status routes while the shared store is down #1066 2026-10-31 23:58:21 +01:00
Linus Rath 2acf79db43 fix: keep other instances' domain branding and server secrets when saving one entry #1066 2026-10-31 23:57:28 +01:00
Linus Rath 68d64f5018 fix: pair the policy with its own version and keep config reloads and saves from undoing each other #1066
Fixes from the review of the admin part:
- GET /api/admin/policy reads the policy and its version together; an
  identity check waiting on the store let another replica's change pair
  the old policy with the new ETag, and a save with it then won.
- A policy save answers with the stored policy and its ETag, and the
  policy, organisation, plugins and themes pages continue from that
  instead of reading it back from an instance that may not have it yet.
- A stored policy that doesn't parse keeps its version, so a save can
  replace it instead of answering 409 forever.
- A reload of another replica's change throws when this replica's writes
  kept landing while it read, so the change poller tries again; a write
  whose reload raced it reads the document again instead of applying its
  older result.
- Dropping a deleted plugin from the policy rereads policy.json first in
  local mode, as every other write does.
- New updateAdminConfig(key, change) builds a value on the stored one.

The policy route's test mocks gain getPolicyWithVersion.
2026-10-31 23:55:17 +01:00
Linus Rath d6e98e207a Merge branch 'feat/multi-instance' into feat/multi-instance-admin 2026-10-31 23:45:00 +01:00
Linus Rath 8b9df1b633 Merge branch 'feat/multi-instance' into feat/multi-instance-data 2026-10-31 23:43:48 +01:00
Linus Rath 2cdd43d94c Merge branch 'feat/multi-instance' into feat/multi-instance-ext 2026-10-31 23:32:31 +01:00
Linus Rath 291b6364c9 feat: count admin login attempts across every instance and refuse sign-in while the shared store is down #1066
The three admin login limiters (5 per address and 50 overall in one
reservation, 5 per known device) keep their numbers and still don't count
refused attempts, but live in the store: memory in this process as before,
or the shared store with several replicas. checkRateLimit and
checkDeviceRateLimit are async now; the login route awaits them and
answers 503 "Sign-in is temporarily unavailable" when the store can't be
reached. The two limiter tests gain await and reset the store's counters,
which vi.resetModules no longer clears.

The admin code tells a store outage by the error's name, since the store
may come from another bundle's copy of the module.
2026-10-31 23:18:40 +01:00
Linus Rath ee5a107649 fix: answer 409 for a stale policy save also when another bundle made the config manager #1066 2026-10-31 23:17:36 +01:00
Linus Rath eb0005f3ef feat: share telemetry and update-check state across instances, with one instance id and login count #1066 2026-10-31 23:10:30 +01:00
Linus Rath 7148dd2ded feat: keep account archives in the shared store #1066 2026-10-31 23:04:02 +01:00
Linus Rath 7a482a4d0e feat: share admin config and policy between instances and spread changes within seconds #1066 2026-10-31 23:03:57 +01:00
Linus Rath 9651098634 fix: keep both templates when two settings saves arrive at once #1066 2026-10-31 22:58:14 +01:00
Linus Rath 25408b2ac2 fix: refuse a policy save from a page opened before someone else saved #1066
The admin pages save the whole policy from the draft they loaded. A
save from a page opened before another tab's or another admin's save,
or before a deleted plugin was dropped from the policy, used to put the
older policy back. GET /api/admin/policy now sends the policy version as
an ETag; the policy, organisation, plugins and themes pages send it back
as If-Match, and a save based on an older version answers 409 and keeps
the draft on the page. A PUT without If-Match replaces the policy as
before. Deleting a plugin or theme changes only its id in the stored
policy, and a policy import compares against what it replaces.

Test mocks of the config manager gain getPolicyVersion and updatePolicy,
which the route and the extension cleanup now call.
2026-10-31 22:57:57 +01:00
Linus Rath 585e10041e fix: refuse to save over stored settings or plugin credentials that no longer decrypt #1066 2026-10-31 22:54:35 +01:00
Linus Rath fd4a319eae feat: show the storage and how many instances checked in on the admin overview #1066 2026-10-31 22:46:08 +01:00
Linus Rath 1b2d81be27 feat: hold each instance back until it has loaded from the shared store and add a readiness check #1066 2026-10-31 22:40:51 +01:00
Linus Rath d23a2869d1 Merge remote-tracking branch 'origin/main' into fix/found-fix-server
# Conflicts:
#	Dockerfile
#	docker-compose.yml
2026-10-31 18:17:39 +01:00
Linus Rath f884ffe441 Merge branch 'fix/found-fix-ui' into fix/found-fix-server 2026-10-31 18:17:13 +01:00
Linus Rath ddc24e4fc7 fix: let a browser an admin signed in from before past the shared admin login limit 2026-10-31 17:55:29 +01:00
Linus Rath 825d6e76f8 fix: let admins switch off usage stats by clearing the collector address 2026-10-31 17:51:09 +01:00
Linus Rath f11ddf82dd fix: tell banner plugins when an admin opened the mailbox through impersonation 2026-10-31 17:48:29 +01:00
Linus Rath 1e462fc67e Merge pull request #1436 from shukiv/fix/impersonation-expired-app-passwords
fix: let a mailbox be opened through the handoff more than five times
2026-10-31 16:24:52 +01:00
Linus Rath 5931cd1678 feat: send an oauth resource indicator when one is configured #1461 2026-10-31 16:14:35 +01:00
Linus Rath 3bdbdb1364 docs: move the mobile hand-off contract to the website 2026-10-31 14:32:39 +01:00
Linus Rath f74ff49f58 feat: give the mobile app its organisation policy for its access token 2026-10-31 14:10:53 +01:00
Linus Rath fb3ba0275a feat: hand the mobile app a one-time code instead of the sign-in in its link 2026-10-31 14:06:44 +01:00
Linus Rath cabb174108 feat: register a client for bulwark mobile in the oauth auto-setup #1459 2026-10-31 00:56:49 +01:00
Linus Rath 66351e0d31 feat: show admins the changelog of every plugin version an update brings or a rollback undoes 2026-10-30 02:02:40 +01:00
Linus Rath cf0eb03c4a feat: show the sender's BIMI logo as the sender icon when the message passes DMARC
Stalwart does no BIMI, so sender icons were only ever favicons, which
anyone controls for any domain. The webmail server now does the
receiver side itself at /api/bimi, and avatars ask it only for
messages whose own Authentication-Results say dmarc=pass for the exact
From domain.

- `lib/bimi/`: DMARC policy check (quarantine or reject, pct=100, the
  parent domain's sp and p for subdomains), the assertion record with
  fallback to the parent domain, mark certificate validation (chain to
  pinned DigiCert, GlobalSign and SSL.com VMC roots, BIMI key purpose,
  validity, SAN, SCTs present, CRL revocation, logo from the logotype
  extension), SVG Tiny PS checks and rendering to PNG, caching
- logos backed only by the DNS record show by default; the admin
  setting "Require a mark certificate" (`BIMI_REQUIRE_CERTIFICATE`)
  limits them to VMC and CMC backed ones
- a check mark beside the sender's name for certificate-backed logos
- avatars try contact photo, BIMI logo, plugin avatar, favicon; the
  sender warning shield still wins, and logos follow the sender icons
  setting and the no-third-party-traffic mode
- the two-label public suffix list moved to `lib/root-domain.ts`

SCT signatures are not verified and only the default selector is used.
2026-10-30 00:12:24 +01:00
Linus Rath 50b3ddccec feat: let admins keep the refresh token valid on sign-out so Stalwart keeps the consent #1388 2026-10-29 20:47:18 +01:00
Linus Rath 0b4e501142 feat: make the login email placeholder configurable #1423 2026-10-29 20:24:26 +01:00
Linus Rath d875714f35 feat: offer remember me on the sso-only login and apply it to every sso path #1437 2026-10-29 20:06:44 +01:00
Linus Rath 623c9e1792 feat: let operators set accessibility statement, barrier report, help, Easy Read and sign language links 2026-10-29 18:27:35 +01:00
Shuki Vaknin 322274fff8 fix: let a mailbox be opened through the handoff more than five times
Each impersonation handoff makes an app password on the target mailbox
that expires after 8 hours. Stalwart keeps an app password after its
expiresAt and counts it against the mailbox's limit (5 by default), and
nothing deleted the expired ones, so after five handoffs every further
one failed with "You have exceeded your quota of 5 app passwords" and the
mailbox could no longer be opened that way.

Before making a new one, the handoff now lists the mailbox's app
passwords and deletes its own expired ones, recognised by the
"Support session " description. The user's own app passwords and a
session still running are never touched. It is best effort: if the list
or the delete fails, the handoff tries to create the password as before.
2026-10-29 16:37:06 +00:00
Linus Rath f12a13e1fd feat: create documents in the formats the document editor can edit from New file #1393 2026-10-29 00:01:13 +01:00
Linus Rath d10d6b60ce fix: tell admins the OAuth setup needs a webmail sign-in and disable it until there is one 2026-10-28 17:40:26 +01:00
Linus Rath 9e809a31db Merge branch 'feat/org-features' into feat/org-thirdparty 2026-10-28 05:27:58 +01:00
Linus Rath 1a747ac9dd feat: limit the page's CSP to the configured hosts with no third-party traffic #903 2026-10-28 04:30:41 +01:00
Linus Rath 031d94635c refactor: keep the outgoing host descriptions out of the browser bundle and scan every URL form #903 2026-10-28 04:30:23 +01:00
Linus Rath defe4e93f3 fix: bind the sign-in time to the refresh token and count unstamped sessions from a fixed time 2026-10-28 03:42:19 +01:00
Linus Rath cefb20db4e test: give the plugin proxy route test the config reader the mode needs #903 2026-10-28 03:10:14 +01:00