Commit Graph
891 Commits
Author SHA1 Message Date
Linus Rath 2cbf73ed08 test: expect session URLs hosted on another origin to be kept 2026-10-01 00:36:00 +02:00
Linus Rath 29c74c3b77 feat: sign in with an access token 2026-10-01 00:24:47 +02:00
Linus Rath 2d2b0753be Merge pull request #1132 from lucamzanon/perf/multi-account-startup
perf: stop the startup request storm with many logins
2026-09-30 20:33:32 +02:00
Linus Rath cd14807133 Merge pull request #1112 from theLucius7/fix/config-load-recovery-20260927
fix: recover from transient application configuration failures
2026-09-30 20:32:44 +02:00
Linus Rath 692be6aa9d Merge pull request #1095 from lucletoffe/fix/calendar-location-links
fix(calendar): surface the meeting link and open the location in maps
2026-09-30 20:32:44 +02:00
Linus Rath 7b61e55529 chore: write nul separators as \0 so git diffs these files as text
Three files carried a literal NUL byte inside a string: the admin
check's cache key in `app/api/admin/auth/route.ts`, the global search
hit key in `lib/global-search/types.ts` and two TNEF fixtures in
`lib/__tests__/tnef.test.ts`. Git treats any file containing a NUL as
binary, so it showed no diffs for them, never normalised their line
endings, and reported every pull request that touched the admin route
as a conflict even when a text merge was clean.

The escape produces the same string at runtime. The admin route was
stored with CRLF while git saw it as binary; it is now stored with LF
like the rest of the tree, so its diff covers the whole file (the only
content change is on line 70).
2026-09-30 19:34:37 +02:00
Linus Rath 4db88255e8 fix: stop a sixth sso account from overwriting the first account's sign-in
`/api/auth/sso/complete` only accepted cookie slots 0-4 and quietly
used slot 0 for anything higher. Since the account limit rose to
`MAX_ACCOUNT_SLOTS` (50) on HTTP/2, adding a sixth account through
server-side SSO wrote its refresh token over the first account's cookie,
while the new account was registered on slot 5 with no cookie behind it.

The route and the OAuth callback's pending-slot check now use the same
bound as every other auth route.
2026-09-30 19:31:03 +02:00
lucamzanon 2d5756bff7 perf(push): reconcile push bindings instead of rebuilding them
The push effect re-runs every time a login connects, which while a browser
with many logins restores means once per account. Each run closed every
client's push and set it up again: the live streams were torn down and
reopened and every slow poll re-primed its baseline with a full state read
(Mailbox, Email, Calendar, CalendarEvent, SieveScript) - quadratic in the
number of accounts. With ten logins that was 21 stream openings and about
180 state-poll methods in the first seconds.

Reconcile instead: bind the clients that are new, unbind the ones that are
gone, and on an account switch re-bind only the two logins whose role
changed, handing the live stream to the newly active one when it has
none. Handlers are read through a ref at event time, so a binding never has
to be re-created because a callback changed identity.

The reconciliation is a pure function (lib/push-bindings.ts) with its own
tests; the effect only supplies the callbacks.
2026-09-30 19:03:34 +02:00
Linus Rath 1d90fe17f4 fix: preview audio, video and icon attachments sent with a catch-all type 2026-09-30 15:08:31 +02:00
Linus Rath d0a1d0183b Merge pull request #1127 from dealerweb/fix/preview-declared-type
fix: preview attachments whose declared type the browser cannot show
2026-09-30 14:57:33 +02:00
Linus Rath 98cada387c Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-09-30 14:50:52 +02:00
Linus Rath 3051f58a1b fix: keep the rules menu working for group members and list mail on stalwart 2026-09-30 14:48:26 +02:00
Linus Rath b663f0e774 fix: treat an empty PLUGIN_SIGNING_KEY_FILE as unset #996 2026-09-30 14:24:29 +02:00
Linus Rath 70dee66966 Merge pull request #996 from mulatta/feat/external-plugin-signing-key
feat: support external plugin signing keys
2026-09-30 14:23:45 +02:00
Linus Rath f51ce0e448 fix: read impersonation secrets from *_FILE when the variable is empty #897 2026-09-30 14:23:09 +02:00
Linus Rath 9c1dcd309c Merge pull request #897 from Cameo007/main
add file-based env variables for LibreTranslate and Stalwart Impersonation & handle file-based vars from JSON configs in ConfigManager
2026-09-30 14:22:01 +02:00
Linus Rath ff0d923bd1 fix: let COOKIE_SECURE decide the admin cookie's Secure flag #1063 2026-09-30 14:21:46 +02:00
Linus Rath 9985c86c97 Merge pull request #1063 from lucamzanon/fix/admin-cookie-secure-by-request
fix: derive the admin cookie's Secure flag from the request
2026-09-30 14:21:26 +02:00
Linus Rath fa789a6d9f Merge pull request #748 from lucletoffe/fix/calendar-blank-cn-and-description
fix(calendar): stop writing blank participant names and empty descriptions
2026-09-30 14:21:20 +02:00
dealerweb 1512898cdb fix: preview attachments whose declared type the browser cannot show
The preview hands the attachment's blob to the browser's own viewer,
which picks the viewer from the blob's type and downloads anything it
cannot show - under the blob's UUID. It only corrected the exact
application/octet-stream declaration from the file name. Other
declarations kept the preview from working: IBM i (AS/400) systems send
every attachment as application/x-as400attachment, some senders use
application/x-pdf or force-download, and the octet-stream check was
case-sensitive.

Move the decision into previewBlobType(): a PDF, image, audio or video
preview takes the first type its viewer can show - declared, then
served, then the one the file name implies. Other previews keep the
declared type unless it is generic, and the name never yields a
script-capable type.
2026-09-30 14:20:20 +02:00
Linus Rath 1531dae7eb feat: create filter rules from the message right-click menu 2026-09-30 13:52:41 +02:00
Linus Rath fee0e7c9e7 fix: show default oauth scopes in the admin dashboard #1126 2026-09-30 12:17:25 +02:00
lucamzanon 2b83bce8c3 Merge remote-tracking branch 'origin/main' into fix/admin-cookie-secure-by-request
# Conflicts:
#	app/api/admin/auth/route.ts
2026-09-30 08:54:14 +02:00
Linus Rath a24d6626b5 fix: stop reading ids in delivery reports as verification codes 2026-09-30 00:14:08 +02:00
Linus Rath ac34c9a6ac test: pass inboxOnly in web-push vapid tests 2026-09-30 00:00:38 +02:00
Linus Rath 040cea30c5 fix: keep calendars visible after a failed calendar fetch 2026-09-29 23:59:03 +02:00
Linus Rath 9d140fa762 Merge pull request #1052 from lucamzanon/feat/account-row-colours
feat: colour unified rows by account and give every tag a hue of its own
2026-09-29 23:30:24 +02:00
Linus Rath f499915cf1 Merge pull request #1121 from shukiv/fix/web-push-vapid-key-mismatch
fix(push): resubscribe on VAPID key mismatch, and survive an unreadable subscription
2026-09-29 23:24:55 +02:00
Linus Rath ddc0b03055 fix: reuse cached access token on lite session restore #552 2026-09-29 21:25:02 +02:00
Linus Rath affa999cd0 fix: report recipients the server rejects at send #1123 2026-09-29 20:49:40 +02:00
Linus Rath be0494cb5c fix: move mail to trash when emptying an ordinary folder 2026-09-29 20:35:09 +02:00
Linus Rath 324fb48312 fix: keep an opened mail in place in unread-first lists until another is opened 2026-09-29 20:28:39 +02:00
Linus Rath 3bccca715c feat: show the folder hierarchy in the search folder dropdown 2026-09-29 20:15:14 +02:00
Linus Rath 3904af2cbb feat: search spam and trash themselves while they are open 2026-09-29 20:09:26 +02:00
Linus Rath 763732fdc9 feat: leave spam and trash out of search by default 2026-09-29 19:52:39 +02:00
Linus Rath aed0d27320 fix: stop the mail list flashing a reload right after the page opens 2026-09-27 18:21:24 +02:00
Linus Rath be3c4b0d99 feat: draw calendar events as solid blocks and toasts as plain cards 2026-09-27 12:45:19 +02:00
Linus Rath 3ed484df35 feat: offer the code from sign-in mail as a copy chip in the list and reader 2026-09-27 11:19:48 +02:00
Lucius7nya d923b92034 fix: recover from transient application configuration failures
Retry interrupted or invalid runtime configuration requests with bounded backoff and per-attempt timeouts. Validate the complete response before caching it, and prevent stale HTTP-cached configuration.

Cover request and body timeouts, shared hook consumers, retry bounds, the server response contract, and both Lite configuration paths.
2026-09-27 15:41:21 +08:00
Linus Rath 377c283d86 fix: say up front when the mobile app cannot use an http mail server
The app refuses a JMAP server over plain http (loopback aside), yet the
desktop issued a code, reported "Signed in on your phone" once it was
redeemed, and the phone failed. Linking now stops before the step-up
with an explanation.
2026-09-27 04:53:49 +02:00
Linus Rath 473e5c8bff fix: clean up after pairings a phone never completes
- An app password minted for a phone that never redeemed its code stayed
  valid (no expiry, full permissions, no TOTP) and used up one of the
  account's five app-password slots. It is now deleted, with its own
  credential, when its grant is discarded or expires.
- Replaced codes were kept for twelve minutes each, so a session could
  grow the store, and the cost of every request, without bound; only the
  last three of a grant are kept now.
- A session read that failed after the credential check was taken for a
  non-Stalwart server and handed the phone the account password; it now
  refuses the pairing instead.
2026-09-27 04:51:30 +02:00
Linus Rath a5583aa8e8 fix: re-check a paired phone's token endpoint against the current config
The token proxy trusted the endpoint sealed into a phone's refresh token
for good: after a server was removed or its provider moved, it kept
posting there with the plain fetch and whichever secret the server id
now maps to. Trust is now re-derived on every renewal; an endpoint the
webmail is no longer configured for gets neither the secret nor the
unguarded fetch.
2026-09-27 04:50:38 +02:00
Linus Rath 5c1da4efa7 fix: refuse a pairing re-auth the identity provider did not make fresh
The step-up for SSO accounts asks the provider for a new login with
`prompt=login`, but a provider that ignores it answers from its existing
session, so anyone at an unlocked desktop could link a phone. When the
ID token's `auth_time` is older than the step-up, the re-auth is now
refused.
2026-09-27 04:49:54 +02:00
Linus Rath 75bffd35e6 fix: limit pairing password attempts per mail account and per day
The step-up's failure count was keyed on the cookie slot, which the
client chooses, so a session could spread guesses over 50 slots; and
even one key allowed 480 failures a day, well above Stalwart's ban of
100 a day per address, which would ban the webmail's own IP for every
user. Failures now count per server and login name, 5 per 15 minutes
and 20 a day.
2026-09-27 04:49:03 +02:00
Linus Rath 3494d2f138 fix: keep the app's "Sign in via webmail" working past its first token expiry
The mobile handoff returned the provider's token endpoint and the
webmail's client id as they were. With a confidential client the phone
could not renew (it has no secret), and a provider on another host was
refused by the app outright. The handoff now returns the same bundle a
paired phone gets: such grants renew through `/api/auth/pair/token` with
a sealed refresh token.
2026-09-27 04:34:19 +02:00
Linus Rath 1446fd39dc fix: link the mobile app from every account with a sign-in of its own
"Link Mobile App" only appeared for OAuth sessions, its step-up could
only go through the identity provider (which failed without an OAuth
client and bounced password+TOTP users to the provider's login page),
and it handed the phone the desktop's own refresh token. Revoking that
token on sign-out, or a rotating provider, signed the phone out too; a
confidential client stopped the phone at its first token expiry; and an
identity provider on another host was refused by the app.

The step-up now mints the phone's own grant:

- Password and password+TOTP accounts confirm the current password (and
  code) in the dialog. The server uses it once to get an OAuth grant
  from Stalwart's structured login for the app's own client, retries
  with the webmail's client when the server insists on a registered one,
  and falls back to an app password ("Bulwark mobile (paired <date>)")
  or, on servers that offer neither, the verified password. Failed
  attempts are limited per account so the webmail's address cannot be
  banned by guessing through it.
- SSO accounts keep the provider round trip, now with `offline_access`
  where offered; its fresh tokens become the phone's grant instead of
  being thrown away, and must belong to the account being paired.
- The account and server always come from the slot's verified session
  context, never from the request.

Grants live 5 minutes server-side, codes 2 minutes, both single use; the
store is pinned on `globalThis`. Redeem answers `invalid_code`,
`expired_code` or `used_code`, and the desktop polls a separate status
id to replace the QR with "Signed in on your phone". A phone whose
client is confidential, or whose provider is on another host, renews
through `/api/auth/pair/token` with a sealed refresh token, so current
app builds work unchanged. Signing out discards a pending grant but no
longer affects a paired phone.

The dialog shows for every account (not in Lite), with a copyable
sign-in link, a TOTP field when needed, and the app's own "Scan a
sign-in code" wording.
2026-09-27 04:16:18 +02:00
Linus Rath bb80d01ddf fix: keep a pairing re-auth from signing the browser in as another account
The step-up for linking a device sends the user to the identity
provider with `prompt=login`. The login route accepted that pending
state too, so a code from the re-auth could be spent there and sign this
browser in as whichever account answered the prompt. It now refuses a
re-auth state; only the pairing route completes one.

The callback page also handles its code once. React's development
double effect ran it twice: the second pass no longer saw the re-auth
flag and raced the pairing route for the single-use code.
2026-09-27 04:14:15 +02:00
Linus Rath 32b2afee3b fix: stop sending the webmail's OAuth client secret to user-chosen mail servers
With custom JMAP endpoints allowed, the password+TOTP login posted the
admin's client secret to `/auth/token` on whatever server the user
named. The secret now only goes to admin-configured servers.
2026-09-27 04:12:47 +02:00
Linus Rath ce5f177d53 refactor: move Stalwart's password and TOTP login into a shared helper
The password+TOTP login drove Stalwart's structured login (`/api/auth`,
then `/auth/token` with PKCE) inline. Linking the mobile app needs the
same exchange to mint a phone's own grant, so it now lives in
`lib/auth/stalwart-password-login.ts` and the route only maps its
result to the responses it always gave.
2026-09-27 04:11:30 +02:00
Linus Rath 5ce8c89c51 fix: open and save files shared with you in the office editor #1094 2026-09-26 22:28:34 +02:00