Commit Graph
108 Commits
Author SHA1 Message Date
Linus Rath 1614cab78b fix: return to the message list after sending a new message on a phone 2026-10-11 19:04:02 +02:00
Linus Rath 1421e29a6d fix: toggle the focused conversation by key under Mountain View #1061
The x shortcut passed the bare email id, which the key-based selection
no longer accepts. It now passes the email, so the toggle hits the
right account's row in unified views.
2026-10-10 22:05:41 +02:00
Linus Rath 7cbd33dbc8 fix: act on the ticked account's mail in batch actions #1061
Batch delete grouped the selection by bare email id, so with the same
id in two accounts, permanent-deleting B's row destroyed A's message
and left B's alone. Copy had the same lookup and went out through the
wrong account.

- resolve the selection to email objects by key, including ticked
  members of an expanded thread that are not list rows
- delete, move, mark read, tag and copy group by those objects
- rows, thread checkboxes, drag and drop compare by key, so ticked
  rows show as checked in unified and tag views
- the Mountain View toolbar uses the same resolver
2026-10-10 21:51:19 +02:00
Linus Rath e567df3ab3 Merge pull request #1061 from lucamzanon/fix/cross-account-selection-key
fix: scope batch selection to the owning account
2026-10-10 19:29:02 +02:00
lucamzanon 19e06d02ad fix(mountain-view): keep the same top bar on every app
Calendar, contacts, files and settings still drew the left navigation
rail under Mountain View, so moving away from mail switched to the
default frame. They now show the Mountain View top bar too, without the
mail search and the menu button, which only make sense in mail.
2026-10-09 08:27:01 +02:00
lucamzanon 96cdeb1d66 Merge remote-tracking branch 'origin/main' into up/fix/cross-account-selection-key
# Conflicts:
#	components/email/email-list.tsx
#	stores/email-store.ts
2026-10-08 10:11:20 +02:00
lucamzanon ade30622f2 Merge remote-tracking branch 'origin/main' into up/feat/mountain-view-layout
# Conflicts:
#	components/mail/mail-app.tsx
2026-10-08 10:05:29 +02:00
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
lucamzanon d632956231 feat: add Mountain View, an optional alternative layout
Add an "Interface layout" setting at the top of Settings > Layout with two
options, Default and Mountain View. Default is the existing layout and stays
the default, so nothing changes unless a user opts in.

Mountain View rearranges the chrome while keeping the theme's colours, icons
and type:

- desktop: a top bar with a wide search field (filters drop from the field),
  an apps grid and the account avatar instead of the left app rail; flat
  navigation with Starred under Inbox and the rest behind "More"; a Compose
  pill; one list toolbar with select-all, refresh, overflow menu, a 1-N of
  total range and the batch actions; rows led by a checkbox and a star; an
  icon-only message toolbar with Reply / Reply all / Forward under the
  message; compose in a docked window; an account popover with per-account
  unread counts; j/k cursor, x to select and g-sequences
- phone: rounded search header, compact rows with the star on the preview
  line, and Reply / Reply all / Forward at the end of a message instead of
  the bottom action bar

It is one setting (`interfaceLayout`), written to a data attribute on <html>
that keys a block of CSS tokens in globals.css, plus a few components gated
on the flag. The mail layout in force is derived (effectiveMailLayout), so
the user's saved mail layout is never rewritten. The setting is listed in
the admin policy tab, and the new strings are translated in all locales.

Includes unit tests and a Playwright smoke test against a running instance
(scripts/mountain-view-smoke/).
2026-10-05 10:36:20 +02:00
lucamzanon 81d47d74bf Merge remote-tracking branch 'origin/main' into up/fix/cross-account-selection-key
# Conflicts:
#	components/email/email-list.tsx
2026-10-01 11:58:16 +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
lucamzanon 4ee234b7c3 fix: resolve selection keys to ids before the batch spam actions
batchMarkAsSpam and batchUndoSpam take the bare ids of the viewed account,
but were handed the selection keys. Those match only while no row carries
a source account; mail of a shared account was silently left out.
2026-09-30 09:02:58 +02:00
lucamzanon 9da6a26404 Merge remote-tracking branch 'origin/main' into fix/cross-account-selection-key
# Conflicts:
#	components/mail/mail-app.tsx
#	stores/email-store.ts
2026-09-30 09:02:58 +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 fac3bdf31c fix: scope batch selection to the owning account
Email ids are unique only within their account, so in a view that merges
several - the unified mailbox, and since #1038 the tag view - two
unrelated messages can share one. Selection was a set of bare ids
resolved with `emails.filter(e => selected.has(e.id))`, so ticking one
row also ticked its namesake in another account, and batch delete, move,
archive or mark-read then acted on a message nobody selected.

Reported by @alyyousuf7 on #1013, where `eiaaaabc` exists in two
accounts at once.

Selection is now keyed by `emailKeyFor` - the same account scoping
#1013 introduced for threads - and the actions take the email rather
than its id, so a caller cannot reintroduce the collision by passing a
bare id. Unstamped emails keep the bare id as their key, so
single-account views are unchanged.

The batch actions derive the JMAP ids from the selected emails
themselves instead of looking each id up again, which is where the
namesake used to ride along, and every place that pruned the selection
after a delete or a move now prunes by key.
2026-09-19 08:59:21 +02:00