Only the mail shell updated document.title, so every other surface sat at the
static value the root layout renders: the tab could not tell you where you
were, and on a multi-account setup it could not tell you whose mailbox you
were in either.
Each surface now sets "<screen> - <account> - <app>" through the shared
useDocumentTitle hook. Labels reuse the sidebar navigation translations, so
the tab and the rail never disagree about what a screen is called - no new
translation keys.
Refs #1159
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On a branded deployment (APP_NAME, APP_LOGO_*) three places after sign-in
still showed Bulwark or Stalwart (#1160):
- the TOTP issuer was 'Stalwart', so the authenticator app listed the
mail server instead of the service; it is now the configured app name,
- the About card used the Bulwark wordmark and title; with APP_NAME set
it shows the configured name and logo (light/dark as configured), and
keeps the Bulwark mark otherwise,
- the Files settings preview thumbnail used the bundled logo; it now uses
the configured one when there is one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A change to the Sieve script from another device or tab reloads the
filters, and the page showed its loading or error state instead of its
content. That took an open rule dialog or the Sieve editor with it,
together with whatever was typed there. While a dialog is open, the
page keeps what it has.
useFocusTrap started over whenever its onEscape changed, and callers pass
a new function on every render. Each start moved the focus back to the
opener and then to the dialog's first element, so a field lost the focus
whenever the page behind rendered again: every 30 seconds through the
connection check, or when new mail updated the folder counts. The trap
now reads the latest onEscape from a ref and starts once per opening.
A view change in a dialog (a list giving way to a form) can remove the
element that had the focus. The restart used to pull it back by
accident; the trap now does that on purpose, and only when the focused
element was removed.
Filter rules can be limited to a period: the rule dialog takes a start
and an end on the user's clock, the rule list shows whether a rule is
planned, active or expired, and the script tests the moment of
delivery with RFC 5260 currentdate in UTC. A rule is never written
without its period.
The vacation card becomes an out of office page: the period on top,
then the automatic reply and forwarding, each with its own switch.
Forwarding runs with or without the auto-reply, within the period,
never for spam, and keeps a copy here or not. Against the server's
redirect limit it counts as a rule ahead of all others, one that stops
the message unless it keeps a copy. The auto-reply can go to
internal or external senders only, told apart by the envelope sender
against the account's own domains. Both run from the filters script
that includes Stalwart's vacation script, which is put back in charge
when the server's own script takes over.
A script that uses any of this says metadata version 2, which older
builds leave alone instead of writing it back without these fields.
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.
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.
The notification settings read the push state once, when they opened.
Push turned off meanwhile - in the browser's site settings, or from
another tab - still showed as on until the page was reloaded. The state
is now read again when the window regains focus or the tab becomes
visible, unless an enable or disable is in progress or has just failed.
A failed check reads as off instead of leaving the panel unchanged.
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.
The values of a multi-value condition were glued together as one
unbreakable run - the space around each "or" was a margin, not a
character the line could break at - so a rule with several addresses ran
out of its card. Each value is now an item of its own: they wrap between
each other under the first value, with "or" leading the next line and
still inside the condition's chip, apart from the "and"/"or" between
conditions. A single value too long for any line, in a condition or an
action, breaks inside itself instead of overflowing.
The Link Mobile App step-up marked the tab before asking the server to
start the provider round trip. When that request failed, or the user
backed out at the provider, the mark stayed, and the next sign-in's
callback in that tab went to the pairing route and failed. The mark is
now set only once the round trip starts, holds that round trip's state,
and the callback drops one that does not match.
The app refuses a JMAP server over plain http (loopback aside), yet the
desktop issued a code, reported "Signed in on your phone" once it was
redeemed, and the phone failed. Linking now stops before the step-up
with an explanation.
"Link Mobile App" only appeared for OAuth sessions, its step-up could
only go through the identity provider (which failed without an OAuth
client and bounced password+TOTP users to the provider's login page),
and it handed the phone the desktop's own refresh token. Revoking that
token on sign-out, or a rotating provider, signed the phone out too; a
confidential client stopped the phone at its first token expiry; and an
identity provider on another host was refused by the app.
The step-up now mints the phone's own grant:
- Password and password+TOTP accounts confirm the current password (and
code) in the dialog. The server uses it once to get an OAuth grant
from Stalwart's structured login for the app's own client, retries
with the webmail's client when the server insists on a registered one,
and falls back to an app password ("Bulwark mobile (paired <date>)")
or, on servers that offer neither, the verified password. Failed
attempts are limited per account so the webmail's address cannot be
banned by guessing through it.
- SSO accounts keep the provider round trip, now with `offline_access`
where offered; its fresh tokens become the phone's grant instead of
being thrown away, and must belong to the account being paired.
- The account and server always come from the slot's verified session
context, never from the request.
Grants live 5 minutes server-side, codes 2 minutes, both single use; the
store is pinned on `globalThis`. Redeem answers `invalid_code`,
`expired_code` or `used_code`, and the desktop polls a separate status
id to replace the QR with "Signed in on your phone". A phone whose
client is confidential, or whose provider is on another host, renews
through `/api/auth/pair/token` with a sealed refresh token, so current
app builds work unchanged. Signing out discards a pending grant but no
longer affects a paired phone.
The dialog shows for every account (not in Lite), with a copyable
sign-in link, a TOTP field when needed, and the app's own "Scan a
sign-in code" wording.
The re-sync effect depended on pushStatus.kind, so its own
setPushStatus({ kind: 'busy' }) re-fired the effect, whose cleanup
cancelled the in-flight enableWebPush - the push row then sat on
"Working…" forever and never updated the filter. Depend only on the
setting flip; read client/relay/status live from the recreated closure.
Flipping the setting only wrote to the store; the server-side JMAP push
filter stayed stale until the next app launch ran resyncWebPush. When push
is already enabled on the device, re-run enableWebPush on change so the
toggle takes effect without a restart.
Stalwart refuses a VacationResponse subject of 512 bytes or more and a
text or HTML body of 2048 bytes or more, answering only "Field could
not be set." The limit counts bytes, so 1030 "ä" already exceed it,
and the HTML from the rich-text editor reaches it quickly. The save
failed with a generic error.
On Stalwart the vacation settings now check the subject and the text
and HTML that would be sent, and name the limit instead of saving.
Bulwark read the live display name (#900) and the account profile with
`x:Account/get`, which Stalwart only allows for admins. Every other
user got `forbidden`, so the settings page showed no name and the
identity kept the stale snapshot. The name now comes from
`x:AccountSettings/get`, the account's own settings object; aliases
and quota still use `x:Account/get` where it is permitted.
With TOTP on, Stalwart requires the current code for a password change
and for switching TOTP off ("Current OTP code is required"). Neither
dialog asked for it. Both now do, and the password change sends it as
the `otpAuth/otpCode` patch, because a whole `otpAuth` object would
reset `otpUrl` and turn TOTP off.
Saving the display name also ignored a `notUpdated` answer and showed
success.
The Sieve generator wrote actions in the order they were picked in the
rule editor. `fileinto` stores the flags set at that moment, so "mark
read", "star" or "add label" placed after a move never reached the
moved message. Flag actions are now emitted first.
Moves and copies also targeted the folder by path only. Renaming the
folder silently sent the rule's mail to Inbox. Rules now store the
folder id and emit `fileinto :mailboxid "<id>" "<path>"` when the
server lists `mailboxid` (declaring `mailbox` too, which Stalwart
requires), with the path as the fallback. Saving a rule refreshes the
path from the id, so existing rules pick up the id on their next save.
Forward actions get a "Keep a copy" option (`redirect :copy`), and the
editor honours the server's `maxNumberRedirects` (1 on Stalwart):
forward is disabled once the limit is used up, with a hint when
existing rules already exceed it, because the server skips extra
redirects without telling anyone.
Stalwart allows one active Sieve script and turns the auto-reply on by
activating its own `vacation` script. Turning the auto-reply on switched
every Bulwark filter off (including junk and folder rules), and saving
any filter switched the auto-reply off again.
When the vacation script takes over from an existing filters script,
the filters script is now re-activated with
`include :personal :optional "vacation"`, so the server-built reply
(with its date window and HTML body) runs from there. Turning the
auto-reply off drops the include again. A filters save while the
vacation script is active keeps it through the same include, and the
vacation settings read the include as "on", because
VacationResponse.isEnabled reports false in that state.
Clients that only look at VacationResponse (Stalwart's web UI, phones)
still show the auto-reply as off while the include is active.
In the unified view each row can be shaded with its account colour
instead of carrying an account dot, behind a layout setting. Tags keep
their own generated hue on their chips.