Commit Graph
917 Commits
Author SHA1 Message Date
Linus Rath dfa82f7cca Merge pull request #1019 from lucamzanon/fix/compose-error-recovery
fix: keep the draft when a send is not confirmed
2026-10-02 10:32:27 +02:00
Linus Rath 39efd8af3c fix: preview the named message when the inbox lookups beside it fail #1041
Any failed method in the preview request answered 502, so when the
server named the delivered message and it was read fine, a failing
Inbox query still turned the notification into a generic "New mail".
The named message is now a complete preview on its own.

A response without the optional `total` was also treated as a failure;
it now counts the returned ids.
2026-10-02 10:32:22 +02:00
Linus Rath 86afe94204 Merge pull request #1041 from lucamzanon/fix/web-push-recovery
fix(push): keep notification previews truthful when a lookup fails
2026-10-02 10:31:37 +02:00
Linus Rath 7eced91f82 fix: keep retrying a lost push subscription, at most every 15 minutes #1147
Recreating the subscription removed the account's stored id before the
replacement was created and verified, and the resync now requires that
id. One interrupted or timed-out attempt (up to 75 seconds of
verification) therefore ended recovery for that account for good. The
id now stays until a verified replacement overwrites it.

A failed resync also forgot its timestamp, so every return to the tab
reran the whole enable. It now waits 15 minutes before the next try.
2026-10-02 10:31:33 +02:00
Linus Rath 5d6bd51331 Merge pull request #1147 from lucamzanon/fix/push-resubscribe
fix(push): recreate a browser subscription that was lost
2026-10-02 10:30:43 +02:00
Linus Rath dfea1d0f96 Merge pull request #1157 from dealerweb/fix/select-optgroup-colors
fix: keep the group labels of native option lists readable in dark themes
2026-10-02 10:30:42 +02:00
Linus Rath 15839afec2 Merge pull request #1154 from dealerweb/feat/filter-all-messages
feat: a filter condition that matches every message
2026-10-02 10:30:42 +02:00
dealerweb f4b5027733 fix: keep the group labels of native option lists readable in dark themes
The From dropdown groups the identities by account. #999 gave the
options the popover colours, but the group labels kept the control's
text colour on the popup's base, which a transparent control leaves
light: in dark themes, light text on a white bar.
2026-10-01 17:31:59 +02:00
dealerweb 6da2bf9a43 fix: count the forwards one message can collect against the redirect limit
The rule editor added up the forwards of all enabled rules against the
server's redirect limit, which applies per message. Rules run in order,
and a rule that stops ends the script for its messages, so a rule that
forwards and stops locked forwarding in every other rule, although no
message can reach both.

Count the most forwards one message can collect instead, and warn above
the rule list when the order, a switch or a drag lets one message
collect more than the limit.

Builds on the stop fix: without it, "Stop processing" after "Delete
silently" or "Reject" writes no stop.
2026-10-01 16:33:51 +02:00
dealerweb c55b9f4929 feat: a filter condition that matches every message
The rule editor had no way to say "every message that arrives"; the
workaround was a condition like "From contains @", which only works if
you know it. "All messages" is now a condition of its own, at the end of
the field list. It takes no comparator and no value, writes Sieve's
`true` test, and combines with other conditions as any condition does.
The rule list shows it on its own. A rule with it is never applied to
the messages already in a folder: it would act on all of them.
2026-10-01 14:58:00 +02:00
dealerweb cbdc4deadb fix: stop after a silent delete or reject, and keep spaced rule names single
"Stop processing further rules" wrote no stop when the rule's last action
was "Delete silently" or "Reject". Both only cancel the implicit keep
(RFC 5228 4.4, RFC 5429): the script goes on, so a later rule still
filed the deleted or rejected message into a folder. Such a rule now
always writes a stop, unless its block has one already; rules below it
no longer run on its messages, which is what the option says.

Since 1.11.1 the "# Rule:" line carries the rule name with its runs of
whitespace collapsed, but the parser compared it with the name as
stored. A name like "Foo  Bar", one with a tab or a trailing space, or
an empty one came back as someone else's block on every read, and every
save added one more copy. Both sides are now compared collapsed, and the
name is read from its own line only. That also drops the copies earlier
saves left, as long as the rule still exists under that name.
2026-10-01 14:57:33 +02:00
lucamzanon 4bc5d48888 fix(push): keep notification previews truthful when a lookup fails
A JMAP method error comes back with HTTP 200. The preview route read a
failed Mailbox/query or Email/query as an empty Inbox and answered
"nothing unread", so the service worker dropped the push in silence.
It now answers 502, and the worker shows its generic notification.

When the server names the message that arrived and it cannot be read -
deleted since, or filed where this login cannot see it - the route no
longer substitutes the newest unread message in the Inbox, which was
some other mail announced as if it had just arrived. It names no
message; the unread total still comes back for the generic toast.
2026-10-01 12:44:14 +02:00
lucamzanon 3cfab390a0 fix(push): recreate a browser subscription that was lost
The background re-sync skipped any account whose browser subscription
was gone (isWebPushEnabled needs a live one), which is exactly the
state that needs repairing: the browser or the user cleared the
endpoint while the opt-in and the server registration survived, and
push stopped for good. It now runs whenever the user opted in and
permission is granted, so the normal enable flow recreates the
subscription. It still never prompts. A failed attempt no longer waits a
day for the next one.
2026-10-01 12:42:34 +02:00
lucamzanon 15740fa286 fix: keep the draft when a send is not confirmed
When the response carries no EmailSubmission - a method error, or no
answer for it at all - nothing confirms the message left, yet the send
went on as if it had: the old draft was replaced and the composer
closed. It now fails with "Send confirmation was not received" and
keeps the draft, so the user can check Sent before sending again.

An empty notCreated map is a success some servers spell out; it is no
longer read as a failure.
2026-10-01 12:12:46 +02:00
Linus Rath 90c8195a00 Merge pull request #1134 from dealerweb/feat/recipient-mentions
feat: mention a recipient by first name with @ in the message body
2026-10-01 11:06:00 +02:00
Linus Rath 868805ec9a fix: keep ringing for a second mail in the same account #1141
The quiet window ignored which account rang, so a second, different
message in the same account within 30 seconds arrived without sound, and
every single-login user lost the alert too. The window now records the
account of the last audible alert and silences only other accounts,
which is the burst across logins the change is meant for.
2026-10-01 11:02:39 +02:00
Linus Rath fa45ff997f Merge pull request #1141 from lucamzanon/feat/push-quiet-window
feat(push): ring once for a burst of mail across accounts
2026-10-01 11:02:33 +02:00
Linus Rath 6bce3323a9 fix: keep previews that only start like a style sheet #1136
The leading-CSS strip emptied or cut ordinary previews:
"@media team, the brief is attached", "@Page can you check?" and
"@import the CSV ..." became empty, and "Reminder: your appointment
{date: 2026-10-01} is confirmed" became "is confirmed".

At-rule names now match in lower case only, only `@import` and
`@charset` end at a semicolon, an at-rule word with no block after it
is prose, and a selector colon must not be followed by a space.

The conversation view's collapsed cards, contact activity, the pro
folder tab and the list's row-height estimate now use `cleanPreview`
too, so they agree with the list rows. The test's invisible characters
are written as escapes.
2026-10-01 11:02:26 +02:00
Linus Rath 1a186f8962 Merge pull request #1136 from lucamzanon/fix/preview-leading-css
fix: skip a style sheet at the start of a message preview
2026-10-01 11:01:28 +02:00
Linus Rath 5597eadc28 fix: give a server its own sign-in button only when it has a button label #1135
A server entry's OAuth client ID already has a meaning while OAuth is
off globally: it is the registered client that password and TOTP logins
use with Stalwart's require_client_registration. The PR made every such
entry offer OAuth on its own, so existing installs would suddenly run
discovery and show SSO buttons on the login page, and multi-IdP setups
would get one extra button per server.

A server now gets its own button only with both a client ID and a button
label, which no existing config has. The rule lives in `offersOwnOAuth`
with a test. Also:
- the OAuth-only login form uses the selected server's label
- the public config validator checks `buttonLabel` is a string
- the admin hint and `.env.example` say the label enables the button
2026-10-01 11:01:17 +02:00
Linus Rath d2855695a6 Merge pull request #1135 from lucamzanon/feat/per-server-oauth-sign-in
feat(login): let a server in the list sign in with its own OAuth client
2026-10-01 11:00:26 +02:00
lucamzanon cd39805566 feat(push): ring once for a burst of mail across accounts
Each account has its own notification, so one message reaching several
of the user's accounts rang once per account. Only the first alert in a
30 s window makes a sound now; the others still appear or update,
silently. The window runs from the last audible alert, so a steady
trickle still rings every so often.
2026-10-01 04:40:08 +02:00
lucamzanon 717f18fc46 feat(unified): say how many accounts the unified mailbox holds while logins reconnect
After a load the logins other than the one on screen reconnect in the
background, and until they are all back the unified counts cover only
some of them. With many logins that lasts seconds, and a partial total
looks exactly like the whole one.

While that restore runs, the "All accounts" header shows how many logins
are already in the scope, e.g. "4/10", with a translated label for screen
readers. It goes away when the scope is complete or the restore is over -
a login that failed to reconnect does not keep it up.

`restoringAccounts` in the auth store marks the background restore; the
count itself is a pure function with its own tests.
2026-10-01 04:38:04 +02:00
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
dealerweb 2b5110e0e4 feat: mention a recipient by first name with @ in the message body
Typing @ at the start of a word in the rich-text body offers the To and
Cc recipients (contact groups expanded) and inserts the chosen one's
first name as plain text. The name comes from the display name; with a
firstname.lastname@ address, the address tells which word of the name is
the first name, and without a name the address supplies it. A bare
initial keeps the mailbox name (@m.mustermann), and recipients who share
a first name get their full name. Bcc recipients are never offered -
naming one in the text would disclose them to everyone else.

The list only opens while a recipient matches, so Enter and Escape
otherwise keep their usual meaning. While it is open, Escape closes the
list rather than the composer, which handles Escape in the capture phase,
and the list closes when the editor loses focus.

Built on @tiptap/suggestion, at the version of the other TipTap packages.
2026-09-30 22:43:59 +02:00
lucamzanon 2d521d19b2 fix: skip a style sheet at the start of a message preview
Some servers build a message's preview from its HTML body without skipping
the <style> element, so a marketing mail's preview - in the list and in its
push notification - can read "@media screen and (min-width:600px){.hide{
display:none!important;…" and never reach the text.

Drop whole leading rules from the preview before showing it; a preview that
ends inside the style sheet was nothing but CSS and becomes empty, so the
usual "no preview" text shows instead. A rule only counts as CSS when its
block holds declarations or further rules, so prose with a brace in it
("the meeting is at {time} tomorrow") is left alone.
2026-09-30 21:35:20 +02:00
lucamzanon 1fac44832a feat(auth): let one server in the list sign in with OAuth on its own
A server entry that names its own OAuth client now offers the OAuth
button even while OAuth is off globally, so a Gmail bridge can sign in
with Google next to a server that uses passwords. The entry can also
name the button ("Sign in with Google"); the generic SSO label stays
the default.
2026-09-30 21:28:06 +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