Commit Graph
96 Commits
Author SHA1 Message Date
Linus Rath f87be32388 fix: keep an empty draft subject empty so send still warns #1189 2026-10-07 20:22:51 +02:00
Linus Rath a60768eb17 test: wait for the lazy viewer in the blob routing tests #1188 2026-10-07 19:49:37 +02:00
Linus Rath 7bf5e0d802 fix: keep a shared-folder message's thread key in line with its row #1188 2026-10-07 19:34:20 +02:00
Linus Rath 1b9aeff4e5 Merge pull request #1188 from VasilisNtovolos/fix/shared-account-blob-downloads
fix: route shared-account blob downloads by message owner
2026-10-07 19:33:24 +02:00
VasilisNtovolos bb3ef6d9fc fix: route shared-account blob downloads by message owner 2026-10-07 15:57:45 +03:00
Oğuz YılmazandClaude Opus 5.5 54ec8c6584 feat: let operators keep the message subject out of the tab title
Opening a message puts its subject in the browser-tab title. The title does
not stay in the tab strip: browsers keep it in history and in the
session-restore list, and screen shares show it. For mailboxes that hold
other people's correspondence that is a leak nobody audits.

A new feature gate, tabTitleSubjectEnabled ("Subject in Tab Title" under
Admin -> Policy), is on by default, so nothing changes for existing
installs. With it off, the title names the folder instead, so the tab still
says where you are. It is an admin gate rather than a user setting: whose
mail the boxes hold is a deployment decision.

The gate counts as off until the policy has loaded, so a subject never
slips into history while the setting is still unknown.

Refs #1159

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:26:51 +03:00
Oğuz YılmazandClaude Opus 5.5 8ca7dbbe9b feat: name the active account in the browser-tab title
The tab title was "<context> - <app>", so two mailboxes open side by side
read the same in the tab strip and in the browser's session-restore list.

It is now "<context> - <account> - <app>", the order Gmail and Outlook Web
use: browsers cut a long title from the end, so the part that changes comes
first and the app name last. There is no setting for it; the account is what
tells the tabs apart.

The formatting, and the mail view's choice of context (composer, then
message, then mailbox), live in lib/tab-title.ts with tests. The mail view
sets its title through useDocumentTitle(), which other surfaces can use too.
Next renders the root layout's metadata <title> again on every client-side
navigation, after the surface has mounted; the hook puts its title back when
that happens. Only the surface that set the title last does so, so two
titles never fight.

Refs #1159

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:26:23 +03:00
Linus Rath 506f47e155 fix: show the translated message when no Archive folder can be created #1176 2026-10-06 23:31:20 +02:00
sudandClaude Fable 5.1 45c7e2c360 fix: create the Archive folder on first use when the account has none
Stalwart's default folder set has no archive-role mailbox, so on a fresh
account Archive did nothing but raise the not-found toast (#578). Both
archive paths now go through ensureArchiveMailbox(), which creates
"Archive" with the archive role in the account the action targets (own,
the selected shared folder's owner, or the pinned account in unified
view), reloads the mailbox list and continues with the store's own
object. createMailbox() takes an optional role for this. A refused
creation or a folder that does not come back still surfaces as an error.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 06:55:02 +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 ca7d5e22df fix: keep a late notification link from pulling you out of what you opened #1131
A notification link now waits up to 20 s for its login to reconnect.
If the user opened another folder or message, switched account or left
Mail in the meantime, the late link used to switch account anyway and
take them away from it. It now stays put.

When the account switch was overtaken by another one or failed, a
folder link was resolved against whichever folders were on screen, so
an Inbox link could open the wrong account's Inbox. It now stops there.
2026-09-30 20:33:08 +02:00
Linus Rath efe44e09cc Merge pull request #1131 from lucamzanon/fix/deep-link-late-login
fix: let a notification link wait for a login that is still reconnecting
2026-09-30 20:32:44 +02:00
Linus Rath bfc8e0e52e fix: show only that folder's unread mail when its unread count is clicked
Clicking a folder's unread count turns on the unread filter and runs
the search. Since the search scope stopped following the open folder
(#788) its default is "all folders", and since #1082 that fans out over
every shared account too, so the Inbox badge listed unread mail from
every folder of every account the login reaches.

The badge now scopes the search to its folder. That scope moves with
the open folder, the way the search itself carries across folders
(#553); picking a scope in the dropdown or clearing the filters stops
it. Clicking the badge from a tag view or the Scheduled list now also
leaves that view, as clicking the folder does.
2026-09-30 19:49:13 +02:00
lucamzanon a578150c2d perf(unified): fetch each login's folders once, all logins at once
The unified scope is rebuilt on every connection while a browser restores
its logins, on every background push, and on every unified browse,
load-more and search - and each build asked every login for its folders
again, one login after the other. With ten logins that was ten full
Mailbox/get per build and well over a hundred in the first seconds, and the
slowest build waited on the sum of every login's round trip.

Fetch the logins in parallel, and fetch each login's list once: a build
shares a fetch that is already in flight and reuses a finished one until
that login reports a change - the push handlers invalidate the login they
hear from, so a background push now costs one Mailbox/get instead of one
per login. A 60 s limit backstops a change whose push was lost.

A reused list is never written back into `accountMailboxes`: optimistic
updates land there directly, and publishing an older list over them would
snap a just-changed counter back.
2026-09-30 19:03:34 +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
lucamzanon 1d82c59469 fix: resolve a folder link against the mailbox it switched to
Once a link had waited for its login and switched to it, the folder was
looked up at once, in a store that still held the previous mailbox's
folders or none at all: an inbox link opened on a mailbox that was just
switched to reported "this folder is no longer available". Wait for the
new mailbox's folder list to arrive before resolving the link against it.
2026-09-30 19:01:24 +02:00
lucamzanon 1c5dbbcd0b fix: let a link wait for a mailbox that is still reconnecting
A notification tapped with the app closed reopens it on a link that names
its mailbox (`?slot=`). The browser reconnects its logins one at a time,
so that mailbox is often not connected yet when the link is read: with ten
logins, one on the Gmail bridge came back at 3.3 s and the link was judged
at 1.8 s. It was declared unavailable - "sign in to that account to open
this link" - and the app stayed on whichever mailbox was active.

Wait for the login instead, and give up only when it is not one of this
browser's logins, its restore fails, or 20 s go by.
2026-09-30 19:01:14 +02:00
Linus Rath e6a565ee27 fix: bring the mail list back on tablet when the reading pane empties 2026-09-30 00:08: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 7b25d3a95b fix: keep message list from jumping while attachment chips load 2026-09-26 20:24:23 +02:00
Linus Rath aa0075a8e1 fix: write only the changed keywords when tagging or pinning a message
Tagging and pinning replaced the message's whole keyword set with a
copy built from the list row. When the row was stale - the mail had
been read or answered on another device since - the write cleared
`$seen` or `$answered` again, and removed tags were written as `false`,
which a JMAP keyword map may not hold.

Keyword writes now go out as `keywords/<name>` patches for just the
keywords that change, removals as `null`, through the call that
reports a refused update. The local row gets the same patch.
2026-09-26 16:16:13 +02:00
Linus Rath 4858eb0915 fix: open a push notification's message in the account it came from
Clicking a new-mail notification opened `/mail/message/<id>` in
whichever account was active. With several accounts signed in, that
looked the id up in the wrong account: nothing opened, or a different
message did and was marked read.

The preview API now returns the cookie slot of the login that owns the
notified account, the service worker adds it to the permalink (or links
that account's Inbox for a group notification), and the app switches to
the login on that slot before opening the message.
2026-09-26 16:12:20 +02:00
Linus Rath efdc6a1dc3 fix: keep a reply draft in its thread when it is re-opened or undone
Drafts were saved without In-Reply-To and References, and a draft
re-opens in compose mode, where the composer has no original message to
rebuild them from. A reply that was saved and re-opened, or sent and
brought back with Undo, therefore went out as a new conversation.

`createDraft` now stores the threading headers, the composer saves them
with every draft of a reply, and re-opening a draft carries them into
the composer for later saves and the send.
2026-09-26 16:06:28 +02:00
Linus Rath ff8b192508 fix: keep a folder from switching back to the unified inbox while it loads #1102 2026-09-26 12:41:13 +02:00
Linus Rath daf67850d2 fix: reply from the receiving account's identity in the unified inbox #1104 2026-09-26 12:31:29 +02:00
Linus Rath 7e5b996ee6 feat: open office attachments in mail read-only in the document editor #1047 2026-09-23 19:01:47 +02:00
Linus Rath a69d8a71e9 fix: load attachment chips lazily instead of in the message list #1089 2026-09-23 18:09:35 +02:00
Linus Rath 9ef7068bd8 fix: include shared accounts in "All folders" mail search #1082 2026-09-22 17:25:43 +02:00
Linus Rath 5a6c340f18 Merge pull request #1013 from lucamzanon/fix/cross-account-thread-key
fix: scope thread identity to the owning account in unified views
2026-09-21 22:33:26 +02:00
Linus Rath ec67ab994b feat: add missing mail action toasts and show sonner toasts again 2026-09-21 20:52:02 +02:00
Linus Rath b03a0c1dd9 feat: show "email sent" toast after immediate sends 2026-09-21 19:36:01 +02:00
Linus Rath d7548a4bab refactor: migrate icon set from Lucide to Tabler Icons 2026-09-19 13:32:41 +02:00
lucamzanon 5b63993bfe fix: scope thread identity to the owning account in unified views #1012
JMAP thread ids are only unique within their account - Stalwart hands out
per-account counters such as "b" - so the cross-account views merged
unrelated conversations that happened to share an id, and every
client-side thread map (expansion, cache, loading flag, counts,
virtualizer key) mixed them up the same way.

Give ThreadGroup an account-scoped `threadKey` derived from the email's
source stamps and key grouping and all thread state by it. `threadId`
stays the raw id for Thread/get, which #281 already routes to the owning
account. fetchThreadEmailCounts now asks each source account for its own
threads instead of one client for every id. Single-account views carry no
stamps, so their key is the bare id and nothing changes there.
2026-09-18 18:33:02 +02:00
Linus Rath 5de98bc04b feat: add Bulwark Lite static export build 2026-09-17 23:54:33 +02:00
Linus Rath f6ad8a5942 feat: add per-message HTML/plain-text toggle #1022 2026-09-17 22:34:20 +02:00
Linus Rath d6c923f20e Merge pull request #947 from shukiv/feat/list-attachment-chips
feat(list): show attachments on list rows and open them from there
2026-09-11 17:05:08 +02:00
Linus Rath 26358c7a63 Merge pull request #991 from rotterp/fix/shared-mailbox-send-identity
fix(composer): send from the address a message was delivered to
2026-09-07 22:38:51 +02:00
Linus Rath 80dadfbbd4 fix: open search hits from shared folders in the right account #923 2026-09-07 22:34:47 +02:00
Linus Rath 12416d9c47 fix: wire the x shortcut to thread expansion #683 2026-09-07 22:34:47 +02:00
Linus Rath 0efde2e61d fix: set the answered flag when the reply is sent with a delay #985 2026-09-07 22:34:47 +02:00
Linus Rath 8dc1564b72 fix: archive shared-inbox mail into the owner's archive #889 2026-09-07 22:34:47 +02:00
Linus Rath 2b32a0110b fix: report a missing archive mailbox instead of failing silently #578 2026-09-07 22:34:47 +02:00
Patrick RotterandClaude Opus 5 4fc7b8b50b fix(composer): send from the address a message was delivered to
On a shared, group or aliased mailbox every reply, reply-all and forward
went out as `identities[0]` - the account owner - whichever mailbox was
open. `autoSelectReplyIdentity` fused two behaviours: matching one of the
user's OWN identities, which never rewrites `From:`, and the domain
catch-all, which puts an address they have not configured into `From:`.
Only the first is safe for everyone, so it is now unconditional; the
rewrite stays behind the setting, which is what its description promises.
`findReplyIdentityId` already implemented that match and was referenced
only from tests.

`forward` is admitted to the match - it was excluded, so a forward from a
shared mailbox went out as the owner - but never takes the rewrite: a reply
continues a thread whose participants know the addressing, while a forward
introduces a rewritten From to a recipient the user just typed. Quick reply
matches own identities only, having no From row to show or correct a
rewrite, and now falls back to the primary's signature as the composer
does, so a shared-mailbox quick reply is still signed.

`findReplyIdentityId` now scans recipients in To, Cc, Bcc order rather than
scanning identities, and prefers an untagged identity over a differently
tagged sibling. Scanning identities let their order decide, and
`sortIdentities` puts the login's own address first - so `To: team@,
Cc: you@` replied as you, the case this is meant to fix.

Automatic selection is restricted to the active account: `composerClient`
follows the chosen identity, and a reply or forward still carries blobIds
only its own account's server can resolve. The dropdown still offers every
account's identities to pick by hand.

New messages are deliberately left gated: `composeFromAccountEmail` falls
back to `AccountEntry.email`, written once at first login, so resolving it
unconditionally would override a user-chosen default sender identity (#507).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 20:37:29 +02:00
Linus Rath 1122306ba1 Merge pull request #944 from shukiv/fix/rtl-mobile-drawer-side
fix(mobile): open the folder drawer from the right in RTL
2026-09-07 17:54:30 +02:00