Second of three for background push across accounts (multi-account
spec, MA-8 part 2).
Turning on "Notify me even when inbuxa is closed" now registers this
browser in every signed-in account, each through its own session (the
route from the previous change), and renewal keeps them all current.
GET /api/auth/accounts says which mail account each session is, so the
subscription can name it. The remembered endpoint is kept per account,
and survives the clean-up that follows switching accounts.
The service worker is told who the other accounts are. A push for one
of them is titled with that account's address, shown even while a tab
is focused (the tab only shows the front account's mail), and its
Mark read and Archive act through that account's session. Clicking it
brings that account to the front and opens the message. While push is
on, the tab's own polling of other accounts stops notifying, so nothing
arrives twice.
Signing out of one account removes this device's subscription there
only; signing out of all, or turning push off, removes every one.
Also fixes where every background notification opened: the worker
linked to /mail/inbox/<thread>, and the route takes a mailbox id there,
so a click landed on the inbox list with "That folder no longer
exists". The worker now gets each account's inbox id and links to the
message.
Checked end to end in Chrome against a local server with two accounts:
both registered and verified, a message to the account not in front
showed a notification under its address, and clicking it switched
accounts and opened the message. No new strings. typecheck, tests
(web 1547, server 279) and build pass.
First of three for notifications with the app closed, for every
signed-in account (multi-account spec, MA-8 part 2). No behavior
changes yet.
A JMAP push subscription belongs to whoever signs the request, so an
account that isn't in front can only have one registered, verified and
renewed through its own session. POST /api/auth/accounts/<sessionId>/jmap
forwards to the mail server as that session, when it is one of this
browser's other accounts, and only for PushSubscription/get and /set,
Mailbox/get, and Email/set limited to keywords and mailboxIds updates
(what a notification's Archive and Mark read need). Anything else is
403; the account in front, or a session this browser doesn't hold, is
404. Its OAuth token is renewed first if due. The browser already holds
the session, so nothing new becomes reachable.
On the web app side the push helpers (list, create, extend, destroy,
verify) take a JMAP caller, defaulting to the account in front exactly
as before, and lib/notify/otherAccount gives the caller for another
account through the route.
Tests: the allowlist, the route end to end with two accounts (allowed,
refused, front and unknown sessions), and the caller. The accounts test
file now raises LOGIN_RATE_LIMIT, since it signs in more often from one
address than the default allows. typecheck, tests (web 1543, server
278) and build pass.
With more than one account signed in (#49), mail arriving in one that
isn't in front went unseen until someone switched to it (multi-account
spec, MA-8).
- GET /api/auth/accounts/unread answers the Inbox unread count of each
account not in front, asked through that account's own session (its
OAuth token renewed first if due), kept a minute per account.
- The web app asks every two minutes while another account is signed
in. The account menu shows each one's count beside its name, and the
avatar carries a dot when any of them has unread mail.
- When a count rises while the app is open and desktop notifications
are on, a notification names the account ("New mail for
[email protected]"); clicking it switches to that account. An
account seen for the first time doesn't notify: its mail was already
there.
Not in this change: notifications with the app closed, which need each
added account's own Web Push subscription.
New strings (3, English only in the other ten catalogs): "New mail for
{name}", "Unread in the Inbox: {count}", "Account: new mail in another
account". Tests: the server answers the other account's count and
nothing when alone; the client keeps the counts, notifies only on a
rise and only with notifications on. Checked in Chrome against the
mock. typecheck, tests (web 1541, server 277) and build pass.
Turkish shipped (ihasmail #32/#37), so ten languages ship alongside English, not nine. CONTRIBUTING.md, the PR template and the emlName.ts comment said nine; ROADMAP.md and the i18n-literals.mjs comment describe what shipped on 2026-08-31 and stay as they are. Translations: no user-visible strings added or changed, so no catalog work.
Someone who looks after several mailboxes of their own can now keep
them all signed in in one browser and move between them from the
account menu, without signing out (multi-account spec, MA-B; forum
topic 75).
How it holds them. The session cookie is unchanged: it is the account
in front, and every request is answered with it, so nothing else in the
server changes. The others ride in a second cookie, <name>_more, as a
list of their own session cookies. Each session stays its own -- sealed
credential, expiry and "this is my device" -- and nothing about one is
read through another. At most 5 in all, all on this mail server.
- Add account (account menu): with sign-in on the mail server's page
it goes there with prompt=login, so the server asks again rather than
reuse the first sign-in; with the password form, a small dialog asks.
Refused, and the front stays, when either account's organization has
addAccounts off (inbuxa:SharingPolicy), the account is on another
server, or 5 are open. The same account again just comes to the
front.
- Switching (POST /api/auth/accounts/<id>/front) swaps it into front;
the web app clears what it cached for the previous account and
reloads. A message being written blocks the switch.
- Sign out ends only the account in front, and the next one comes
forward; Sign out of all accounts ends every one.
- GET /api/auth/accounts lists them, the front first, and says whether
one more may be added.
Not in this change: unread counts and notifications for the accounts
not in front (MA-8), which the spec puts last.
The mock can sign in a second user (MOCK_SECOND_USER/PASS) and answer
addAccounts false (MOCK_NO_ADD_ACCOUNTS), for the new server tests:
two accounts joining, switching, the same account twice, a switch to a
session it doesn't hold, signing out of one and of all, and an
organization that forbids it; and the OAuth start asking prompt=login.
The client tests cover listing, switching (cache cleared, reload) and
both sign-outs. Checked in Chrome against the mock: add, switch, sign
out of one.
New strings (9, English only in the other ten catalogs): "Add
account", "Sign out of all accounts", "Add an account", "Both accounts
stay signed in here; switch between them from this menu.", "Working…",
"That account couldn't be added.", "You can't add more accounts here.",
and the other-server and organization refusals. typecheck, tests (web
1538, server 276) and build pass.
The server now marks a shared mailbox (support@, legal@) as a
delegation of kind "sharedMailbox" (multi-account spec, MA-S). Without
this, the webmail would show one exactly as it shows a locked account:
a red bar, a padlock in the tab and on the brand, and its calendars and
files in place of the reader's own.
Now such a mailbox is listed with the other shared mailboxes under
"Mail to show", opens with the shared bar, which also gives the
person's access level and whether they can send as it, and changes
mail only. Its access level still applies exactly as a lock's does:
read changes nothing, organize never deletes, no sending without
send-as. Found without asking Mailbox/get, since the server's mark says
it is mail.
A server that sends no kind is treated as before: every delegation is a
lock. No new strings. typecheck and vitest (174 files, 1534 tests)
pass.
A group's members, and anyone a folder was shared with, could reach that
mail over IMAP but not here: Mail read only the reader's own account.
Calendar, Contacts and Files already list other people's shares; Mail
now does too (multi-account spec, MA-A).
Such an account is listed under "Mail to show" in the account menu,
after any locked account handed to the reader, and opens in place of
the reader's own mail through the same switch AL-7 uses. Only accounts
whose Mailbox/get answers with a mailbox are offered: the server
advertises every capability on any shared account, so a colleague who
shared one calendar would otherwise appear with mail.
While one is in view:
- a bar in the palette's accent (not the locked account's red) names
it, with "Back to my mail"; the tab title names it without a padlock;
- calendars, contacts and files stay the reader's own (viewAccountFor
follows only a delegation now);
- writing a message uses the viewed account's identities, so a reply in
support@ goes out as support@ and is saved in its Drafts and Sent.
Losing the account (removed from the group, share withdrawn) takes the
reader back to their own mail with the existing "You no longer have
access" notice.
New string: "Shared mailbox:" (1, English only in the other ten
catalogs). Checked in Chrome against a scratch server with a support@
group and two members. typecheck and vitest (174 files, 1533 tests)
pass.
On a wide screen the switcher moves from the foot of the folder pane to a
rail down the left edge: Mail (with the inbox's unread badge), Calendar,
Contacts and Files, then Settings and the folder-list toggle at the
bottom. The top bar loses the menu button and the Settings gear there,
both now on the rail. Phones are unchanged: no rail, the tab bar, the
menu button for the drawer and the gear.
- Folder counts are pills: filled with the accent when there is unread
mail, neutral for totals (Drafts, Scheduled).
- Role folders get their own icon tint (Inbox, Drafts, Sent, Archive,
Junk, Trash, Scheduled); a color the reader picked still wins.
- Buttons, Compose and the current selection have a top-lit, raised look
with a pressed state; Compose and the selected rail item are filled
with the accent.
- Collapsing the sidebar keeps Mail's icon strip; Calendar, Contacts,
Files, Settings and Admin have no icon-only form, so their sidebar
hides instead of being crushed.
- The collapsed sidebar no longer draws a cut-off "FOLDERS": the rule
hiding section headings lost to a later one of the same weight (this
is on main today too).
Contrast: the accent behind small text is darkened in light themes and
lightened a touch in dark ones. Measured in a browser across all 12
palettes x 6 accents x both modes: text on pills, the rail badge and
Compose at least 4.83:1, tinted icons at least 3.14:1 on the sidebar.
New strings: 2 ("Show folder list", "Hide folder list"), in all ten
catalogs (1699 -> 1701).
The addresses written to were remembered only in the browser that sent
the message, as a list of recent recipients, so a new device or a cleared
browser suggested nobody. After each confirmed send, the recipients who
are not contacts yet are now saved on the server, in an address book of
their own called Collected, so they are suggested everywhere.
- Only addresses on no card in any address book, own or shared, are
added, de-duplicated, and never the sender's own identities.
- The book is created on first use and remembered by id in the synced
settings, so its name can be anything; a deleted one is replaced.
- Names are split as the contact editor does ("Smith, Jane").
- Settings > Calendar & contacts > Contacts has a switch, on by default,
as mail clients do; the book can be emptied or deleted like any other.
New strings: 3, in all ten catalogs (1699 -> 1702).
Recipient suggestions:
- the words typed match in any order, each one the start of a word in the
name, a nickname, the organization or the address: "jane smi" finds
"Smith, Jane", and "globex" finds the people at Globex;
- someone written to lately ranks a little above an equal match;
- an address already in To, Cc or Bcc is no longer offered in the other
two fields.
Pasting:
- a single web or mailto address pasted over selected words makes those
words the link, instead of replacing them with the address;
- a pasted or dropped image over 10 MB goes in as an attachment rather
than inline, where it would swell every reply.
No new strings.
Turkish came from public ihasmail (#41), which has no strings for what
only this webmail has: delegated and locked accounts, sign-in through
the mail server, the suite's About page, legacy protocol switches, the
data loss prevention notices, and held-for-review sends. Those 88 showed
in English.
They are translated now in the terms the Turkish catalog already uses
(Hesap, Yönetim, Kiracı, posta sunucusu, posta uygulaması), and the 22
Turkish entries no key looks up any more (ihasmail's Stalwart wording,
renamed here) are removed. Turkish: 1611/1699 -> 1699/1699, no stale
keys. These 88 are not a native speaker's; the rest of the catalog is.
When a send's request went out and no answer came back, the composer said
"Send failed" and offered the draft back. If the server had in fact
accepted it and only the reply was lost, sending the draft again
delivered the message twice. Nothing identified the first attempt, so
nothing could check.
Each send now carries its own Message-ID (on the sending identity's
domain, RFC 5322 §3.6.4), kept on the draft if it comes back. A failure
that may have happened after the server acted (no answer, a timeout, a
5xx from the proxy) is followed by asking the server what it did:
- a message with that Message-ID was submitted (EmailSubmission/query by
emailIds, RFC 8621 §7.3): it was sent, and the composer says so;
- one exists with no submission: it was created and never sent; it is
removed from Sent and the failure stands, so resending is safe;
- none exists: the failure stands;
- the server can't be asked either: the composer says it couldn't confirm
and to check Sent, instead of a plain "Send failed".
A refusal (4xx) means the server did not run the request, so it is not
checked. Sending a draft again that came back from a failure asks first,
and sends nothing if a message with its Message-ID already exists;
submissions are expunged after the server's hold period, so later on any
surviving copy counts as sent (every failure path removes the copy it
made).
New strings: 3, in all ten catalogs (1699 -> 1702, no new fallbacks).
On a phone or a narrow list, rows are two lines, and labels went on a third
line of their own. A row is a fixed height at each density, and that third
line fit it only at Comfortable: at Cozy, the default, the labels were cut
off at the bottom, and at Compact they sat outside the row.
They now sit on the sender's line, after the name, which has room to spare,
so the row's height is unchanged at every density. When the line runs short
the name gives way first, then each label truncates; the date stays.
Reported in coffey-labs/ihasmail#35.
(cherry picked from commit 22490d8fe0190d85d8039a977435245b0d7da3cc)
A release event runs the workflow as it was at the tag, so re-running a
failed announcement repeats the failure even after main is fixed. A manual
run takes the tag and uses main's workflow; the action already accepts it.
The repository moved from inbuxa/ihasmail-inbuxa to inbuxa/inbuxa-webmail,
matching inbuxa-server and inbuxa-admin. Point the source links, the image
name and the package link at the new name. The OAuth client id stays
ihasmail-inbuxa, since that is what the server registers.
The Gitea registry stays authoritative; GHCR becomes a copy of it, the way
the GitHub repository is a copy of the Gitea one. After the tag build has
pushed the release image to the registry, a new ghcr job copies it to
ghcr.io under the same version tag and :latest with `imagetools create` --
a copy, not a rebuild, so the digest on GHCR is the digest on the registry.
Anything still pulling the old ghcr.io name, including the TrueNAS app
submission, keeps receiving releases. The job uses the run's own token and is
left out of the status reported to Gitea, so a GHCR problem cannot fail a
release.
The mirror carries tags to GitHub but not releases, so the replica's
Releases page -- and anyone watching the repository there -- stopped at the
last release made on GitHub. After the tag build has published, a new
github-release job copies the tag's Gitea release to a GitHub release: the
same notes, with PR and issue numbers rewritten to Gitea links, the same
files, and a line pointing back to the Gitea release.
It uses the run's own token and is left out of the status reported to
Gitea, so it cannot fail a release. With no Gitea release for the tag it
does nothing.
Commit statuses belong to the commit, not the tag. Upstream v* tags and
inbuxa-v* tags can sit on the same commit, so Gitea's github job, waiting
for "github/ci (tag)", could read the other tag's older result and move on
before this tag's build finished. A tag's status is now "github/ci (tag
<name>)" on both sides.
The per-architecture digest artifacts are also overwritable and kept for a
week, so re-running a build, or publish on its own, still works.
The github job only polls Gitea for GitHub's commit status, but it holds a
runner slot for as long as the GitHub build takes -- the better part of an
hour for a cold build. On the shared build runners a handful of those
could take every slot and stall real work, so it now runs on the `wait`
label: a runner of its own, with many slots, no docker socket and a small
CPU and memory cap.
The mirror can push one commit twice in quick succession. GitHub then
starts two runs and cancels the older, and that run's report job posted
"failure" for the commit. Gitea's github job, seeing the newest status,
failed the check while the surviving run was still building and later
passed.
A cancelled run now posts nothing and leaves the result to the run that
superseded it. A real failure still reports failure.