A draft that carried attachments and no message re-opened with the
contents of one of the attachments as its text. Attach a .csv to an
empty draft, save it, re-open it: the editor shows the rows of the csv
as what you had written, and the next save or send makes that the real
message.
Both edit-draft paths built the text body by joining every value in
bodyValues:
Object.values(draft.bodyValues).map(v => v.value).join('\n')
bodyValues is keyed by part id, and getEmail asks for
fetchAllBodyValues, which per RFC 8621 4.1.4 returns a value for every
text/* part of the message - attachments included. So any attached
.csv, .txt, .log or .md was joined in as body text, and with no html
body to prefer it became the whole message.
Take the body from the parts the server lists as the body instead, and
from nothing else. A draft whose textBody is empty now re-opens empty,
which is what an attachments-only draft is.
The selection lives in lib/email-body-selection.ts beside
getRenderableHtmlBody, which exists because the viewer and the mobile
thread view had already drifted apart once (#489). mail-app and the pro
email tab held character-for-character copies of this join, so the same
drift was already in place.
Dark mode used to invert the whole message body with a CSS filter and
re-invert images and coloured cells. Brand colours came out shifted
(red turned pink), white cells set through bgcolor stayed white, and
the result changed after loading remote content because the blocker
strips the background attributes the selectors relied on.
The rendered body is now read back through computed styles and its
colours are rewritten in place: light backgrounds are darkened in their
own hue, vivid and dark fills stay as sent, text and borders only change
when they lack contrast, and anything painted on a background image is
left alone. Transparent dark images get their original light background
behind them when their pixels show they would vanish. The thread
conversation view uses the same pass.
Also fixed along the way:
- blocking remote content dropped every leading `<style>` block, so
mails lost their CSS until images were loaded
- a mail's `prefers-color-scheme` rules followed the OS instead of the
app theme
Plugins rendering in calendar-event-actions could never see the event's
uid or participants, so a meeting plugin minted a fresh room on every
click and could never propagate attendee or time changes; and of the
declared calendar hooks, none ever fired — the form-save bus existed but
had no emit site anywhere.
- calendar-event-actions eventData gains uid (undefined for new drafts)
and attendees (email list), so plugins can key meetings off the event
and fan out to participants.
- onCalendarEventFormSave now fires in handleSave as a transform:
handlers receive the accumulated patch plus the form view and may
return a patch (currently { virtualLocation }) merged into the saved
event — a meeting plugin refreshes its link in the same save that
sends scheduling messages, so attendees always get the current URL.
- Both buses are typed; contract tests cover the patch flow.
Testing: vitest plugin-hooks suite (41 passing), tsc clean, eslint clean
on touched files.
Users with several accounts can save the list of accounts in their browser
(address, server, label, colour) on the webmail server and bring it back on
another device. The list is encrypted in the browser with a password the user
chooses (PBKDF2-SHA-256, 600,000 iterations, AES-256-GCM, owner bound as
additional data); the server stores only the envelope, one file per owner
under SETTINGS_DATA_DIR/account-archives. No mailbox passwords are included.
Off by default behind the admin feature gate accountArchiveEnabled, pinned
off in Lite. Every method of /api/account-archive needs a session cookie of
the owner, matched without regard to case, whose credential is checked
against the mail server on each call; listing included.
Restored accounts this browser lacks are listed as awaiting sign-in: they
survive a reload, and opening one leads to the sign-in form with the address
filled in. Accounts already present get their archived label and colour.
Switching folders kept the previous folder's mail under a spinner until
the new page arrived, and a failed load emptied the folder.
The store now remembers the first page of each plain folder it showed
(in memory, 30 folders, keyed by login, shared owner, folder, category
filter and sort). Opening a remembered folder paints those rows straight
away and fetches in the background; the fresh page replaces them, and a
failure leaves them up with the error. What happens to the list on screen
- a message read, archived, deleted - is remembered with it.
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>
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>
Tailwind v4's preflight no longer gives buttons a pointer cursor, so most
buttons, menu items, tabs, switches and selects showed the arrow. A base-layer
rule in globals.css gives them the pointer again and gives disabled ones
not-allowed. Cursor utilities on components still take precedence.
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>
The agenda sidecar queried only the session's primary calendar account,
so events in a shared or group account (a family or team calendar) never
reached the Calendar Agenda plugin, although the calendar app shows them.
Read every account the calendar app reads: the primary account plus
accounts that advertise the calendar capability or are not personal,
mirroring the client's getCalendarCapableAccountIds(). Event and
calendar ids repeat across accounts, so the DTO id is scoped as
<accountId>:<id> and carries accountId; calendar colours are looked up
per account. A shared account whose calendars cannot be read is skipped
instead of failing the whole agenda; a failure of the primary account is
still reported.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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/).