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
The toolbar's run() had only try/finally, so a failed archive or tag
write rejected silently. Delete, move and archive now go through
runBatchEmailAction with the same messages as the default list, and
tag, read and spam actions show their success and error toasts.
The top bar also starts update polling. It replaces the rail, which
used to start it, so the update dot on the admin icon never showed.
- The refresh button was labelled "Loading" and the overflow menu
"Mark folder as read"; they now say Refresh and More actions.
- The Starred row stayed highlighted while a tag was open, and its icon
ignored the colourful-icons setting.
- Reply, Reply all and Forward use the standard small button size.
- With a single login the account popover offers one Sign out pill
instead of a pill-less footer row.
- The compose backdrop matches the other dialogs.
- Layout settings hide the mail layout and toolbar label options, which
do nothing under Mountain View; their values still apply to the
default 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/).
With several accounts connected, mail could only be moved between
them (drag to another account's folder). There was no way to file a
copy in another account and keep the original where it is.
crossAccountMoveEmails and copyEmailAcrossAccounts take an optional
`{ keepOriginal: true }`. With it the source message is not destroyed,
and the store leaves the list, the open message and the selection as
they are; only the destination's folders are refreshed. Without it
nothing changes.
The message menu gets "Copy to account..." when more than one account
is connected. It lists the writable folders of the other accounts
(their folder lists are fetched when the menu opens) and copies the
selection, or the message the menu was opened on, through the new
copyEmailsToAccount action. That groups the messages by the login and
JMAP account they live in and refuses a copy into the account a
message is already in.
Mentions change what Enter and Tab do right after `@word` and open a
list for everyone, so they get a setting next to the empty subject
warning (`recipientMentionsEnabled`, on by default, translated in
every locale). Turned off, the composer offers no candidates and the
editor keeps its keys.
While the list is open, the editor now carries `aria-autocomplete`,
`aria-controls` and `aria-activedescendant`, as the To/Cc
autocomplete does, so screen readers hear the list and the highlighted
name. Each row now leads with the `@name` that will be inserted,
followed by the full name and the address: for a surname-first name
such as "Nagy János" the inserted word is not what the name alone
suggests.
Three gaps in copying forwarded parts to the sending account:
- A composer stashed before its first autosave (pro tab move, unmount)
lost the parts' owning account, because the snapshot and the restore
dropped `sourceAccountId`. The restored forward then failed every
save and send with blobNotFound.
- A failed copy was swallowed and the send went ahead, failing with the
server's raw blobNotFound. It now stops with the existing "an
attachment failed to upload" message and leaves the composer as is.
- Copies were recorded only after the whole loop, so a failure on a
later part re-copied the earlier ones on every retry. Each part is
now recorded as soon as it is copied.
Quoted inline images are only read at send time, so they are no longer
copied on every draft save, and at send only those the body still
shows are copied.
A forward carries the original message's attachments, and the quoted
body's inline images, as part blobIds of the account that holds it
(replyTo.accountId, #1104). When the forward goes out through another
account - a From picked from another account's identities, or the
holding account has none of its own - the draft was saved and sent
through that other account, which cannot resolve those blobs: Email/set
failed with blobNotFound and the forward could not be sent.
Parts of the original are now tagged with the holding account. Before a
draft save or a send, any part owned by another account is downloaded
through that account's client and uploaded through the composing one,
once; later saves reuse the copy. Blob/copy cannot do it, as every login
is its own JMAP session. Quoted inline images are loaded through the
holding account's client as well. Forwards that go out from the holding
account keep referencing the original blobs exactly as before.
The list's scroll container outlives a switch of folder, so the next
folder opened wherever the last one was scrolled to - often halfway down,
or past its end. Opening another folder, tag, account, unified view or
plugin tab, or running another search, now starts the list at the top.
Refreshes, new mail and loading more keep the view and the position.
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.
Resolve against the explicit batch targets upstream added for filter rules:
a targeted call names its messages and their account itself, the user's
selection stays keyed by owning account. Also key the context menu's
multi-select check and the rule targets, which compared a selection key
with a bare email id.
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.
The composer took its client, identities and active account live from
the stores. Switching accounts with a draft open moved it: autosave
created the draft in the other account, and replacing the previous
save sent that account a delete for the old draft's id, which there
names an unrelated message. The composer now stays on the account it
was opened in; the other account's identities and client are ignored
until it is closed.
The quoted original in a reply or forward renders in the app document
(a shadow root), where the viewer's iframe CSP does not apply. Opening
a reply therefore loaded every remote image and background of the
original, firing the trackers the viewer had just held back.
The quote's node view now parks remote references in
`data-bulwark-remote-*` attributes for display and restores them when
edits are synced back, so the sent reply still quotes the original as
written. Remote content shows when the policy is `allow` or the sender
is trusted.
The conversation view used on phones built its own sanitizer hook that
only caught `<img src>` starting with a lowercase scheme and inline
style `url()`, and its iframe CSP always allowed http(s) images. With
remote content blocked (the default `ask` policy) uppercase schemes,
srcset, `<source>`, video posters and background attributes still
loaded, so trackers fired when a phone user opened a thread.
The view now uses the desktop viewer's `blockExternalResourcesOnNode`,
and the strict iframe CSP is shared as `emailIframeCsp()` and applied
whenever remote content is blocked, together with `no-referrer`.
A newsletter's List-Unsubscribe `mailto:` was sent from the user's own
account as written: every `to=` field became a recipient, and subject
and body were the sender's choice. The confirmation said only
"Unsubscribe from this sender?", so one click could mail
"I resign" to someone's boss and HR.
The unsubscribe now goes to the single address in the URL (a list of
addresses is refused and `to=` is ignored), the subject is kept on one
line and both subject and body are capped. Both confirmations show the
recipient, subject and body before anything is sent.
The composer uploaded any file and learned about `maxSizeUpload` (per
blob) or `maxSizeAttachmentsPerEmail` (per message) only from a failed
upload or a failed send. Files over the per-file limit, and files that
would push the message past the per-message limit, are now skipped
with a message naming the limit. The limits come from the account the
message is composed in.
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.