Commit Graph
100 Commits
Author SHA1 Message Date
jcoffey-dev 5f27923ce6 New mail in the other signed-in accounts: counts, a dot, and a notification
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 2m24s
ci / announce (pull_request) Skipped
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.
2026-10-05 20:10:07 -07:00
jcoffey-dev 9e77fb3f47 Ten translations: count Turkish where the repo states the number
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 2m30s
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
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.
2026-10-05 19:30:40 -07:00
jcoffey-dev 34baca4365 Account switcher: more than one account signed in at once
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 2m26s
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
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.
2026-10-05 18:44:45 -07:00
jcoffey-dev c66a8aadd7 Show an assigned shared mailbox as shared, not as a locked account
ci / version (pull_request) Skipped
ci / github (pull_request) Skipped
ci / node (pull_request) Successful in 1m29s
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
ci / docker-build (pull_request) Successful in 36s
github/ci (branch) GitHub Actions
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.
2026-10-05 15:15:57 -07:00
jcoffey-dev 722a68432c Open a group's mailbox, or folders someone shared, from the account menu
ci / version (pull_request) Skipped
ci / github (pull_request) Skipped
ci / node (pull_request) Successful in 1m29s
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
ci / docker-build (pull_request) Successful in 41s
github/ci (branch) GitHub Actions
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.
2026-10-05 14:49:28 -07:00
jcoffey-dev 6190d2b4f5 Merge remote-tracking branch 'origin/main' into feat/app-rail-layout
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 2m44s
ci / announce (pull_request) Skipped
2026-10-05 12:12:17 -07:00
jcoffey-dev 29795f3988 Merge remote-tracking branch 'origin/main' into feat/collect-recipients
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 6m46s
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
# Conflicts:
#	web/src/store/compose.ts
2026-10-05 12:04:39 -07:00
jcoffey-dev 5fe929c6a4 App rail, pill counts, tinted folder icons and raised controls
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 3m4s
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
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).
2026-10-05 12:01:27 -07:00
jcoffey-dev c2502d36bd Save the people you write to as contacts
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 3m5s
ci / announce (pull_request) Skipped
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).
2026-10-05 11:55:51 -07:00
jcoffey-dev b11f3997d4 Composer: smarter suggestions, links over selections, big images attached
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 3m5s
ci / announce (pull_request) Skipped
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.
2026-10-05 11:52:54 -07:00
jcoffey-dev f6320e6614 Turkish: the 88 strings that fell back to English
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 2m24s
ci / announce (pull_request) Skipped
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.
2026-10-05 11:47:28 -07:00
jcoffey-dev 152311b035 Never send a message twice after a lost reply
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 1m44s
ci / announce (pull_request) Skipped
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).
2026-10-05 11:43:55 -07:00
jcoffey-dev 64b5db01f3 Keep a narrow list's labels inside the row, on the sender's line (ihasmail #36)
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 2m25s
ci / announce (pull_request) Skipped
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)
2026-10-05 10:35:26 -07:00
jcoffey-dev af9b3a7f70 Indent the mark-read effect's first two lines again
Lost while resolving the conflict in the #30 pick.
2026-10-05 10:33:48 -07:00
jcoffey-dev c87a6d1cd6 ci: let the announce workflow run by hand for a given tag
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 2m7s
ci / announce (pull_request) Skipped
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.
2026-10-04 20:53:46 -07:00
jcoffey-dev 0c5ad7a3b8 ci: pin discourse-release at 9282c6f, which maps inbuxa-webmail
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 2m26s
ci / announce (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
ci / node (pull_request) Skipped
The release map knew this repo only by its old name, so announcing
inbuxa-v2026.10.5-g17a8093 failed. The action itself is unchanged.
2026-10-04 20:46:27 -07:00
jcoffey-dev 5664896f53 Rename the repository to inbuxa-webmail
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 1m30s
ci / announce (pull_request) Skipped
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.
2026-10-04 20:29:09 -07:00
jcoffey-dev bdee5de814 ci: copy each release image to GHCR as a replica
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 2m23s
ci / announce (pull_request) Skipped
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.
2026-09-30 09:27:48 -07:00
jcoffey-dev a7c7d88408 ci: copy each release to GitHub after the tag build
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 2m29s
ci / announce (pull_request) Skipped
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.
2026-09-30 06:52:03 -07:00
jcoffey-dev ae83d27e4b ci: name the tag in GitHub's tag status, and make digests re-runnable
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 2m29s
ci / announce (pull_request) Skipped
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.
2026-09-30 06:44:52 -07:00
jcoffey-dev 24dccdb47d ci: run the github wait job on its own runner label
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 2m38s
ci / announce (pull_request) Skipped
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.
2026-09-30 00:36:13 -07:00
jcoffey-dev ddb6ce4611 ci: a cancelled GitHub run no longer reports failure to Gitea
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 2m30s
ci / announce (pull_request) Skipped
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.
2026-09-30 00:26:40 -07:00
jcoffey-dev 3251478d61 docs: point issues and discussions at Gitea and the forum
ci / node (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / version (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 1m49s
ci / announce (pull_request) Skipped
This repository is now push-mirrored to GitHub, where issues and pull
requests would never reach the maintainers. A note under the title says
where development happens, and sends issues to git.coffeylabs.org and
discussions to community.coffeylabs.org.
2026-09-30 00:16:10 -07:00
jcoffey-dev 216ebd4dfe Build on GitHub Actions when BUILD_ON=github
ci / docker-build (pull_request) Successful in 1m20s
ci / github (pull_request) Skipped
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 2m23s
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
Gitea stays the source of truth and push-mirrors this repository to
GitHub. The org variable BUILD_ON, set on both forges, picks where the
heavy work runs:

- unset: nothing changes. Gitea's jobs run as before and every job in
  the GitHub workflow is skipped.
- github: Gitea skips its test, build and publish jobs. GitHub Actions
  runs them on hosted runners, arm64 natively rather than under QEMU,
  publishes to the same Gitea registry, and posts a commit status back
  to Gitea. A new `github` job in Gitea's ci.yml waits for that status
  and passes or fails with it, so the Gitea run still decides a PR.

Announcing and releasing stay on Gitea whatever BUILD_ON says.

The GitHub-era workflows go: cleanup.yml pruned GHCR, release.yml was a
second weekly scheduler, and publish.yml pushed to GHCR. Their work is
in the new .github/workflows/ci.yml or stays on Gitea. dependabot.yml
goes too: its pull request branches would exist only on GitHub, and
every mirror sync would delete them.
2026-09-29 23:06:27 -07:00
jcoffey-dev 1acf2f29e7 Confirm a typed password without replaying it over JMAP
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 1m5s
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
ci / docker-build (pull_request) Successful in 35s
Creating an app password asks for the account password. A session
holding a token has no password to compare with, so it sent the typed
one to the mail server as HTTP Basic on the JMAP session. INBUXA's
server now takes no password outside DAV (contract C-23), so that check
would always fail.

It now asks the server's sign-in endpoint, the one its own sign-in page
posts to, as this client, to its registered redirect URI, with a PKCE
challenge whose verifier is thrown away so the code can never be
exchanged. "Two-factor code needed" counts as confirmed: the server
says so only after the password matched, so accounts with two-factor
sign-in now pass where the Basic check failed them.

The mock answers /api/auth like the server and can refuse Basic on
JMAP; the app-password test turns that on, and fails on the old check.
2026-09-29 06:50:32 -07:00
jcoffey-dev 07cfe1310e DLP on send: warnings, blocks and held mail in the composer
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 2m13s
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
ci / docker-build (pull_request) Successful in 1m13s
The webmail half of inbuxa's DLP (dlp-and-mail-flow-rules spec, §2.5,
§4):

- A send the server's DLP rules refuse (inbuxa:dlpWarning or
  inbuxa:dlpBlocked) comes back to the composer with the rules' notices
  instead of a generic "Send failed". A warning offers "Send anyway…",
  which asks for a reason and sends again with inbuxa:dlpOverride; the
  server records the reason. A block can only be answered by changing
  the message.
- A message DLP held for review says so on sending ("Held for review:
  it's sent once a reviewer releases it"), from the submission's
  inbuxa:held.
- Tests: the override travels with the submission only when there's a
  reason; refusals are told apart from other errors.

Nine new English strings (the notice labels, the prompt, the toasts);
the other catalogs fall back to English until translated.
2026-09-28 18:51:45 -07:00
jcoffey-dev 44f8e30c45 Say which legacy protocols are off when only some are
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 1m10s
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
ci / docker-build (pull_request) Successful in 46s
inbuxa can now switch IMAP, POP3 and ManageSieve off one at a time,
server-wide and per organization, and the session lists what is still
allowed for the account (legacyAllowed). Where the webmail said
"legacy protocols are off", it now also covers the case where only
some are:

- Security & sessions, above app passwords: "Your organization has
  turned off POP3 for mail apps. Mail apps that use it can't connect
  to this account; others still can."
- The Administration dashboard: "Some legacy mail protocols are off for
  your organization: POP3."
- An organization's sheet in Administration: its switch stays the
  all-or-nothing one; when only some are off it names them and points
  to the console, where they're switched one at a time, and "Turn
  legacy protocols back on" turns them all back on.

With every protocol off, the existing wording shows, as before. From a
server that doesn't send legacyAllowed nothing new appears.

3 new strings in all nine catalogs, unreviewed.

Tested: unit tests for reading the session and a tenant's switches;
the existing tests updated for the new field; typecheck; the whole
suite (1475 tests); and in headless Chrome against a local server with
POP3 off, where Security & sessions showed the new note.
2026-09-27 23:21:21 -07:00
jcoffey-dev 5c08fb9fe9 Delete people in the console, not the webmail's Administration
ci / version (pull_request) Skipped
ci / publish (pull_request) Skipped
ci / docker-build (pull_request) Successful in 34s
ci / node (pull_request) Successful in 1m8s
ci / announce (pull_request) Skipped
Deleting a person's account is the console's now, beside locking it and
legal holds: the console asks why, for the audit log, and says when a
hold keeps the data. Where Delete was, the account's page says so and
links to the account in the console when the server names one. Groups,
lists, domains and tenants keep their delete here.

2 new strings in all nine catalogs, unreviewed.
2026-09-27 19:32:33 -07:00
jcoffey-dev 173680cc41 Open a locked account whole: calendar, contacts and files too
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 1m7s
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
ci / docker-build (pull_request) Successful in 35s
Switching to a locked account handed to the reader moved only the mail.
Now calendar, contacts and files follow it as well, through a new
viewAccountFor that the three stores use for what they show. Settings,
signatures and push keep ownAccountFor, so nothing of the reader's is
ever written into the locked account (inbuxa AL-7).
2026-09-27 18:04:38 -07:00
jcoffey-dev c64a23f9d9 List folders in sidebar order in the move-to picker
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 2m29s
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
ci / docker-build (pull_request) Successful in 1m23s
The picker sorted folders A-Z by path, with Inbox first, so a folder
dragged into place in the sidebar turned up somewhere else when moving
mail. It now walks the tree in compareFolders order, the sidebar's
order with every folder expanded: Inbox, then the saved order, then
the special folders, then A-Z, with subfolders under their parent.

treeOrder lives beside compareFolders. A folder the walk from the top
cannot reach is appended rather than dropped, so it stays pickable as
it was before.

Closes #1

(cherry picked from commit ea03406646062359f74e16ad8a8aed074b4dc409)
2026-09-27 16:27:24 -07:00
jcoffey-dev ee675004f2 Mark no interface language Beta
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 2m26s
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
ci / docker-build (pull_request) Successful in 32s
Every shipped language is offered without the Beta mark. The flag and
its settings note stay, so public ihasmail's changes to them still apply.
2026-09-27 16:01:58 -07:00
jcoffey-dev 2c4d6cdfae Link Michael's profile from the Dutch credits
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 2m40s
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
ci / docker-build (pull_request) Successful in 30s
(cherry picked from commit 9840a537834082bfd9a028a170ce2d6ac60ddf35)
2026-09-27 15:57:28 -07:00
jcoffey-dev f95072ab92 Credit Michael (mbjboon82) for the Dutch review
Name the reviewer in both Dutch catalogs, FEATURES and ROADMAP, under both of his handles, and record that his wording stands.

(cherry picked from commit aaa86e96d66e2431a8c98467712a977d4035bf76)
2026-09-27 15:57:28 -07:00
jcoffey-dev ab759143ab Take Dutch out of Beta with the native speaker's final review
Michael (mbjboon-netizen) sent the final corrections for nl.ts and the Dutch
permission labels and signed the language off, so Nederlands no longer
carries the Beta flag in the picker.

The main catalog changes 52 values, mostly "regels" -> "filterregels" and
"post" -> "e-mail(s)". The permission headings move from compound nouns
("Accountbeheer") to verb phrases ("Accounts beheren"), and the reviewer's
note on that is kept in the file. No keys were added or removed, and every
placeholder is intact.

One entry is kept as it was: "It {damage}, ..." stays "Het {damage}, ...".
{damage} is filled with a verb phrase ("stops in the middle of a line"), so
the added "is" would have doubled the verb.

README, FEATURES, ROADMAP and KNOWN-ISSUES now say Dutch has been reviewed
and the other eight have not.

(cherry picked from commit e30fd73d7dbb1463859efbc4048f680d21d64c56)
2026-09-27 15:57:23 -07:00
jcoffey-dev 4fcc8dd1b9 Show every folder of a locked account in view
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 2m25s
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
ci / docker-build (pull_request) Successful in 30s
Folder subscriptions are the reader's own and they have none in an
account handed to them, so only Inbox showed. Every folder shows while a
locked account is in view, and Hide from list is gone there.
2026-09-27 15:50:51 -07:00
jcoffey-dev 8674b70f62 Open locked accounts handed to you, beside your own mail
When the server hands a locked account to the reader (urn:inbuxa:jmap
delegation), the account popover offers it. Only mail follows the switch;
the reader's own settings, push and notifications stay theirs. A red bar,
a red wordmark with a padlock and the tab title say which account is in
view. Read delegates can't change anything, organize delegates can't
delete, and writing needs send-as. A delegation taken away drops back to
the reader's own mail.

14 new strings in all nine catalogs, unreviewed (inbuxa AL-7, AL-8).
2026-09-27 14:32:10 -07:00
jcoffey-dev 0adc402629 Version the brand images' URLs, so a new mark reaches returning visitors
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 2m25s
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
ci / docker-build (pull_request) Successful in 37s
The logo, favicons and app icons are served from public/img under fixed
names with a browser cache of hours, and the service worker fetches them
through that cache. After the mark changed on 2026-09-27, returning
visitors kept the old cat until their copies expired, and the favicon
and an installed app's icon hold on longer still.

Every URL that names one now carries ?v=BRAND_V (src/lib/brand.ts,
brandImage()): the header, sign-in, About, the mail empty state, the
notification icons, index.html's favicon links, the manifest's icons
and the service worker's shell and notification icons. Date-stamped,
never a counter, for the sites' ASSET_V reason; the three static files
carry the value written out, and the comment says to keep them in step.
2026-09-26 22:50:22 -07:00
jcoffey-dev ee41846c2d Brand: inbuxa's own kitten replaces ihasmail's cat
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 2m43s
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
ci / docker-build (pull_request) Successful in 1m7s
The webmail showed ihasmail's cat-and-envelope as inbuxa's mark. The new
mark keeps the family's face, paws and colors, over a server with a bay
for each piece of the suite: the letter (webmail), a prompt (console),
status lights (server).

- img/inbuxa-mark.png (header, sign-in, About) and img/logo.png (the
  mail empty state and the custom-name fallback).
- favicon.ico, favicon-64, apple-touch-icon (opaque white, as before),
  icon-192/512, and icon-maskable, now on an opaque ground with the
  mark inside the safe circle.
- Login.tsx: 120x126, the new mark's proportions; 120x143 would have
  stretched it. Every other use sizes by one dimension.
- The service worker fetches images network-first, so installed copies
  pick the new ones up without a cache version bump.
2026-09-26 22:03:19 -07:00
jcoffey-dev b407969849 Announce releases on the community forum
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 2m38s
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
ci / docker-build (pull_request) Successful in 2m22s
announce.yml runs coffey-labs/actions discourse-release on every published
release, posting it to this project's Announcements category on
community.coffeylabs.org. The release workflow also announces
from its own job, since a release made with the job token fires no
'on: release' workflow in Gitea.
2026-09-26 19:28:12 -07:00
jcoffey-dev dfbfc38258 About: present inbuxa as the suite, not only the webmail
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 2m25s
ci / publish (pull_request) Skipped
ci / docker-build (pull_request) Successful in 29s
The lead now says what inbuxa is: a mail server, its administration
console and this webmail, installed together under the AGPL, with the
name set apart in the brand teal. A new "The suite" table lists the
mail server, the console (linked, for sessions that may administer),
this webmail's version and inbuxa.org.

4 new strings in all 9 catalogues; the old webmail-only lead is dropped
from them, since nothing looks it up any more.
2026-09-25 21:18:13 -07:00
jcoffey-dev 66673bc9d1 About: show the mail node's hostname, not its address
ci / docker-build (pull_request) Successful in 31s
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 2m28s
ci / publish (pull_request) Skipped
The address is only shown when the node has no PTR record.
2026-09-25 21:14:54 -07:00
jcoffey-dev 41fb7875d0 About: find the mail node's name with resolvePtr
ci / version (pull_request) Skipped
ci / node (pull_request) Canceled after 36s
ci / docker-build (pull_request) Canceled after 0s
ci / publish (pull_request) Canceled after 0s
dns.reverse came back empty inside the image while the resolver answered
the PTR, so About showed only the address. Ask for the PTR record of the
in-addr.arpa / ip6.arpa name directly.
2026-09-25 21:14:13 -07:00
jcoffey-dev 6b44705bfd About: name the webmail node and the mail server node
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 2m26s
ci / publish (pull_request) Skipped
ci / docker-build (pull_request) Successful in 53s
Settings > About shows which webmail node answered (NODE_NAME, else the
container hostname) and which inbuxa node it talks to: the address the
server's name resolves to from the webmail, named by its PTR record. The
server only tells administrators its node name, so the webmail works it
out itself. Fetched from /api/about/nodes on every visit, cached for a
minute server-side, for troubleshooting a cluster.

Fork-only: upstream ihasmail runs one webmail against one server.
4 new strings, translated in all 9 catalogues.
2026-09-25 20:46:04 -07:00
jcoffey-dev e4926cfa7d Show the language model's opinion on a message
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 2m23s
ci / publish (pull_request) Skipped
ci / docker-build (pull_request) Successful in 1m24s
When inbuxa-server's AI spam classification is on, it records the model's
answer in an X-Spam-LLM header: a tag (LLM_<category>[_<confidence>]) and,
in parentheses, the model's explanation. The full message now asks for it,
and where it's there:

- the message details show "Language model's opinion" beside the spam
  filter's own working, with category, confidence and explanation;
- a message in Junk carries a banner saying the same.

Both say it's one of several signals the spam filter weighed, never the
reason on its own, as the server's spec requires. The explanation is model
output and is only ever rendered as text. Nothing shows without the header,
so a server without the feature, or with it off, looks as before.

Translations: two new strings, "Language model's opinion" and "One of
several signals the spam filter weighed", in all eight catalogues (16
entries). Category and confidence come from the server and aren't
translated.
2026-09-22 22:03:29 -07:00
jcoffey-dev f5dd4e5537 Use the server's renamed registry capability, urn:inbuxa:jmap:registry
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 2m47s
ci / publish (pull_request) Skipped
ci / docker-build (pull_request) Successful in 3m55s
inbuxa-server renames the identifiers that carried the upstream name (its
SPEC.md §2.4). Upstream's capability for the registry (x:) objects is now
urn:inbuxa:jmap:registry, beside the fork's own urn:inbuxa:jmap, which is
unchanged. There's no alias, so this lands with the server change and
deploys with it. The mock advertises the new name too. No user-visible
strings change.
2026-09-22 19:00:19 -07:00
jcoffey-dev 1752276229 Lowercase the name in the page title
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 1m37s
ci / publish (pull_request) Skipped
ci / docker-build (pull_request) Successful in 40s
The browser tab, and anything that takes its name from the document title,
read INBUXA. The manifest, the server's app name and the sign-in card all
have it lowercase; the title was the one place left in caps.

Prod's APP_NAME override was set to inbuxa at the same time; the code
default already was.
2026-09-22 17:13:39 -07:00
jcoffey-dev 413ece3bca Take the upstream name out of the mock's sample data
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 1m16s
ci / publish (pull_request) Skipped
ci / docker-build (pull_request) Successful in 33s
The mock inbox showed a sender called "Stalwart Labs" at [email protected],
a "Welcome to Stalwart!" subject, a link to stalw.art in the sample HTML
and a start-up banner tagged [mock-stalwart]. None of that belongs in this
fork, and it turns up in any screenshot taken from the mock.

Sample senders and subjects now name inbuxa, the sample link points at
inbuxa.org, and the banner says [mock-server].
2026-09-22 16:03:31 -07:00
jcoffey-dev d0832013fe Lowercase the tab title's fallback name
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 1m46s
ci / publish (pull_request) Skipped
ci / docker-build (pull_request) Successful in 37s
The title the tab falls back to before the session names the app was the one
user-visible string the brand pass missed; setBaseTitle overwrites it as soon
as the session arrives, so it shows only for that first moment.
2026-09-22 15:21:35 -07:00
jcoffey-dev 8d717e0037 Write the name in lowercase where people see it
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 2m21s
ci / publish (pull_request) Skipped
ci / docker-build (pull_request) Successful in 1m10s
The brand is lowercase inbuxa. This changes what the app calls itself by
default, the wordmark's accessible name, the sign-in card's version line, the
installed app's name in the manifest, and the four translated strings that
name the console or the mail server.

Those four are source strings, so their catalog keys changed with them in all
nine languages; the translations keep their text with the name corrected. No
key was left behind, and no language falls back on more strings than before:
1643/1662 translated, 19 falling back, in each of the nine, unchanged.

Code identifiers, capability URNs, env var names and comments are untouched.
2026-09-22 15:13:53 -07:00
jcoffey-dev afb39fac20 ci: publish tagged releases as INBUXA's own image
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 1m15s
ci / publish (pull_request) Skipped
ci / docker-build (pull_request) Successful in 28s
A tag named inbuxa-v<version> (the tagged commit's own version from
scripts/version.mjs, '+' as '-') now builds a linux/amd64 + linux/arm64
image at <REGISTRY>/inbuxa/ihasmail-inbuxa, tagged with the version and
latest, links the package to the repository and creates the release.

The inbuxa- prefix keeps upstream ihasmail's v* tags, which this
repository carries on shared commits, from ever publishing under the
INBUXA name. The tag must name its commit's version and the commit must
be on main. No schedule yet: releases are cut by hand.
2026-09-22 09:56:09 -07:00
jcoffey-dev 2d7d8952ab Point links at the new git host
ci / docker-build (pull_request) Successful in 1m13s
ci / node (pull_request) Successful in 4m42s
GitHub went dark with the account suspension on 2026-09-20 and GitLab was
retired on 2026-09-22, so links to either no longer resolve. Repository,
file, release and download links now point at git.coffeylabs.org, images
at registry.coffeylabs.org, and old GitHub issue and pull request links at
coffey-labs/ihasmail-github-archive, whose numbers match GitHub's.
2026-09-22 09:03:00 -07:00
jcoffey-dev dd955a939e ci: run socket-free jobs on the light label
ci / docker-build (pull_request) Successful in 1m31s
ci / node (pull_request) Successful in 2m28s
Both runners carry `light` (host1, and host2 over the wg-hosts link), so
these jobs run on whichever host is free. Jobs that mount the docker socket
keep `runs-on: docker`, which only host1 has.
2026-09-22 06:45:41 -07:00
jcoffey-dev 26cf45f502 Merge branch 'ci/gitea-actions' into 'main'
ci / node (push) Successful in 5m11s
ci / docker-build (push) Successful in 1m8s
ci: add Gitea Actions workflow

See merge request inbuxa/ihasmail-inbuxa!6
2026-09-22 00:03:04 -07:00
jcoffey-dev 7469598178 ci: add Gitea Actions workflows ported from .gitlab-ci.yml
ci / node (pull_request) Successful in 2m25s
ci / docker-build (pull_request) Successful in 3m28s
2026-09-21 22:49:35 -07:00
jcoffey-dev f0a92deb08 Merge branch 'feat/tenant-legacy-switch' into 'main'
A tenant's administrator can turn legacy mail apps off for the organization

See merge request inbuxa/ihasmail-inbuxa!5
2026-09-21 14:40:47 -07:00
jcoffey-dev 8abf3a96aa A tenant's administrator can turn legacy mail apps off for the organization
INBUXA's tenant switch (legacy-protocols LP-9 to LP-18) in the
administration. Each tenant's sheet gains "Legacy mail apps": whether IMAP,
POP3, ManageSieve and sending from mail apps are on or off on the tenant's
domains, and the switch.

Nobody turns it off by accident. "Turn off legacy protocols…" first shows
who would notice -- every account in the tenant that signed in with a
legacy mail app in the last 30 days, with the protocols and when (LP-15) --
and the statement of what it means, "for everyone in {tenant}" (LP-16),
then asks for the phrase "turn off legacy mail", matched exactly (LP-17).
Turning it back on is one click; the server refuses while it has legacy
protocols off for everyone, and its words are shown. A tenant's switch
closes no port, so the statement names none.

It needs the domain permissions the server checks for the switch; with
read-only access the state shows and the buttons don't. On a server that
isn't INBUXA, or is older, the section isn't there.

The dashboard says so while legacy mail is off for the signed-in
administrator's organization (LP-18), from the same session flag as
Settings › Security's line.

Every new string in all nine languages, the register each catalog uses, and
the count in each language's plural forms.
2026-09-21 13:54:24 -07:00
jcoffey-dev acb93a90a6 Merge branch 'feat/legacy-protocols-notice' into 'main'
Security says why a mail app won't connect, when legacy protocols are off

See merge request inbuxa/ihasmail-inbuxa!4
2026-09-21 13:50:32 -07:00
jcoffey-dev 4cac9088dd Security says why a mail app won't connect, when legacy protocols are off
When the mail server has turned off legacy mail protocols -- IMAP, POP3,
ManageSieve and sending from mail apps -- for this account, whether for
the whole server or for the account's organization, Settings › Security
says so at the top of App passwords, the section people come to when a
phone won't connect:

  Your organization allows only {app} and JMAP apps, so phone and desktop
  mail apps can't connect to this account.

This is INBUXA's legacy-protocols LP-19. The server reports it per
account as legacyProtocols on the urn:inbuxa:jmap account capability
(contract C-1); anything short of a plain "disabled" -- an older server,
another server, no session yet -- reads as on, so the line never appears
where it isn't true.

The app's name comes from {app}, as everywhere else. Translated into all
nine languages, in the register each catalog already uses.
2026-09-21 13:44:50 -07:00
jcoffey-dev 1cde6f3032 Merge branch 'ci/no-publish' into 'main'
Drop the publish job

See merge request inbuxa/ihasmail-inbuxa!3
2026-09-20 23:01:24 -07:00
jcoffey-dev e5f590978e Drop the publish job
Every tag in this repository is one of ihasmail's own upstream tags, the
same commits, and at those tags publish.yml pushed to ihasmail's image. A
tag-driven publish here therefore ships plain ihasmail under the INBUXA
name as soon as upstream tags reach this project. That happened once and
the image was deleted.

The version and publish jobs go, with the publish stage and the IMAGE
variable only they used. Tests and the Docker build check are unchanged.
The header says why, so the job is not ported back from publish.yml.
2026-09-20 22:58:38 -07:00
jcoffey-dev cf93a1697f Merge branch 'ci/image-version' into 'main'
Build published images with the version they report

See merge request inbuxa/ihasmail-inbuxa!2
2026-09-20 22:27:17 -07:00
jcoffey-dev 17f0552453 Build published images with the version they report
publish.yml passed the computed version into the image build, and the
first port of it to GitLab CI did not. A tag pushed with that port would
have shipped an image reporting itself unversioned (or, for ihasvpn, with a
stray leading "v" no earlier build had), and tagged it with the git tag
rather than the version string.

The version is now computed the way publish.yml computed it and passed as
the build arg, and the image is tagged with it, '+' turned into '-' where a
Docker tag needs that.
2026-09-20 22:19:32 -07:00
jcoffey-dev 461129c5d6 Merge branch 'ci/gitlab-pipeline' into 'main'
Run CI on the self-hosted GitLab

See merge request inbuxa/ihasmail-inbuxa!1
2026-09-20 20:36:11 -07:00
jcoffey-dev 95f7f8008e Run CI on the self-hosted GitLab
Ports ci.yml and publish.yml after the GitHub account was suspended. The
workflow here is identical to the one upstream in ihasmail, so this is the
same pipeline: tests as the image's unprivileged node user, git installed
for the version check, and a tag-driven multi-arch publish under QEMU.

The job environment differences are explained inline -- they are all cases
where a container is not a workstation, not changes to what is tested. No
test was modified.

The Actions workflows stay in the tree as the reference.
2026-09-20 20:15:24 -07:00
jcoffey-dev 25763832f3 Publish to INBUXA's own image, and don't release on a schedule yet
Two things inherited from public ihasmail that became live the moment this
repository went public.

publish.yml pushed ghcr.io/coffey-labs/ihasmail -- the image every public
ihasmail install pulls. A release here would have published INBUXA's webmail
over it, which is the exact confusion SPEC 5 exists to prevent. It now
publishes ghcr.io/inbuxa/ihasmail-inbuxa.

release.yml cuts a release every Monday at 09:17 UTC, which public ihasmail
wants because it has users expecting one. This fork has none yet, and the
release triggers the publish, so on Monday it would have shipped an image
and mailed everyone watching about a product that has not shipped. Scheduled
runs now stop at the first job; a release can still be cut by hand, and the
weekly one comes back by removing one line.
2026-09-20 00:01:02 -07:00
jcoffey-dev fdcf27f3ea Merge public ihasmail: the app's name comes from APP_NAME everywhere
Upstream's {app} placeholder (#406) replaces most of the fork's own
renamed strings: the user menu, the About heading and its version line
now say INBUXA because APP_NAME does, not because the fork wrote it in.

Kept from the fork: inbuxa.org rather than ihasmail.org, no Documentation
entry until INBUXA has its own, the INBUXA mark and wordmark, the "Built
on ihasmail" credit, and the About note that says nothing about the
server software. DEFAULT_APP_NAME stays INBUXA.

The credit's placeholder is {project} now, so the name of the project is
not spelled inside a key that upstream's new test reads as a hard-coded
app name.
2026-09-19 14:29:43 -07:00
jcoffey-dev f8119fafbf Merge public ihasmail: folder reordering, full-screen composer, Dutch update
- Reorder folders by dragging, with special folders first (#402, #405).
- Open the composer full screen, as a setting (#401, #404).
- Dutch translation update (#403).

FEATURES.md stays deleted here, as in bb25355.
2026-09-19 14:11:50 -07:00
jcoffey-dev cfa661de20 INBUXA's name and site where the webmail still said ihasmail
- The sign-in footer names the build "INBUXA webmail" and links to
  inbuxa.org, next to its AGPL source link.
- About gives the INBUXA webmail version, and credits ihasmail on a line
  of its own.
- The user menu's "About ihasmail" is "About INBUXA", to inbuxa.org. The
  Documentation entry pointed at ihasmail's docs; it's gone until INBUXA
  has documentation of its own.
- The startup log says "mail server:" rather than naming the server
  software.

"About INBUXA" and "Built on {ihasmail}" are translated in all nine
catalogues.
2026-09-19 10:09:28 -07:00
jcoffey-dev cee4f74257 Source link opens in a new tab 2026-09-19 00:49:24 -07:00
jcoffey-dev bb25355c23 AGPL source offer and name cleanup
Every build writes the exact source it was built from, uncommitted work and
new files included, as source.tar.gz next to the app, named after that tree.
Docker builds, which have no git, pack the build context and name it by a
hash of its files. The sign-in page and Settings > About link to it instead of
a repository that can drift.

What users, operators and packagers see no longer names the upstream server:
- interface text, in all nine catalogues, with a token-session line for
  Security;
- server messages;
- the settings, now MAIL_SERVER_URL, MAIL_SERVERS_FILE, ADMIN_URL and
  MAIL_SERVER_FOLLOW_ADVERTISED_URLS, and mail-servers.example.json;
- the Tenants notice, which is gone;
- the README, CONTRIBUTING and SECURITY.

ihasmail's own FEATURES, KNOWN-ISSUES and ROADMAP stay with public ihasmail,
and INBUXA.md is folded into the README.
2026-09-19 00:13:12 -07:00
jcoffey-dev 9d2de725c9 Merge public ihasmail: drag calendar events to another day in the week grid (#400) 2026-09-18 21:30:38 -07:00
jcoffey-dev 2e668edb51 Brand the webmail INBUXA: name, mark and wordmark
INBUXA is a product suite and ihasmail an independent product, so INBUXA's
webmail says INBUXA: the sign-in page, header, page title, installed-app name
and About page. The wordmark is drawn in the current text color. ihasmail's
version and AGPL source line stay as its credit. Two new strings, in all nine
catalogues; three Stalwart-only ones are no longer used.
2026-09-18 15:58:56 -07:00
jcoffey-dev cdd8fabff9 No address step before signing in when there's only one mail server
The server's own page asks for the username, so with a single server the
sign-in page keeps only the own-device choice. With several servers the
address still comes first, since its domain picks the server. One new string,
in all nine catalogues.
2026-09-18 15:51:54 -07:00
jcoffey-dev 8857bdac30 Sign in on the mail server's own page (OAuth with PKCE), sessions hold tokens; tenants on every edition
Contract C-8 and C-10: with OAUTH_CLIENT_SECRET set, sign-in goes through the
server's page and the session keeps sealed tokens, renewed before they expire,
instead of a password. Push keeps a credential that renews itself. A password
change signs the session out, since the server revokes its tokens. The mock
answers OAuth for tests and development. Eleven new strings, in all nine
catalogues.
2026-09-18 15:30:37 -07:00
jcoffey-dev f79915aa89 Drop a wrong issue reference from a comment 2026-09-16 12:23:47 -07:00
jcoffey-dev 191c4e7e68 Ask before opening a shared item in a message
The share address takes a plain form POST, which any website can make,
and the app opened whatever arrived straight into a composer. It now
shows what was shared -- the title, the start of the text and link, and
the file names -- and opens a message only when the reader chooses to.
Discarding drops it.

Confirm dialogs now put a message that is not plain text in a div, since
the summary has blocks of its own.

Three new strings, translated in all nine catalogs.
2026-09-16 12:23:27 -07:00
jcoffey-dev 8a08c3d6db Advertise byte ranges on downloads, and record the live checks
Stalwart honors a single byte range on its download endpoint but sends
no Accept-Ranges, and Chrome's PDF viewer only reads a file in pieces
when the first response says it can. The proxy now says so itself.

Checked live on 0.16.22: ContactCard/changes reports creates, updates
and destroys exactly, which the contacts store's sync relies on, and a
range the server cannot serve gets the whole file with 200, never 416.
The mock now answers ranges the same way and sends no Accept-Ranges.
2026-09-16 12:10:20 -07:00
jcoffey-dev 37eb145652 Merge main into fix/push-subscriptions
# Conflicts:
#	KNOWN-ISSUES.md
2026-09-16 11:39:35 -07:00
jcoffey-dev 4054f82c37 Stop duplicate push notifications and piling up subscriptions
Browsers subscribed to Email changes, so every read or move on any
client arrived as a push the worker could only show as "New mail". They
now subscribe to EmailDelivery, which changes only on delivery; Stalwart
sends a delivery to a subscription with an emailPush filter as an
EmailPush alone. The payload now names id and threadId, which Stalwart
sends only when asked, so notifications carry their actions and open the
message. The worker stays quiet while a focused window is open, and the
page leaves notifications to the worker where push is on.

Every renewal registered a new subscription, on the belief that a
repeated deviceClientId replaces the old one. Stalwart keeps both and
allows fifteen per account, which filled up. A browser now extends its
subscription, clears its own duplicates, replaces them only when its
endpoint changed, and on overQuota makes room among other browsers'
subscriptions. The server names its subscriptions by installation and
removes what its previous process registered, and extends rather than
re-creates.

Checked live on 0.16.22; the mock now keeps duplicates, enforces the
limit and accepts an expiry update.

Fixes #375.
2026-09-16 11:36:07 -07:00
jcoffey-dev d38dee7eb9 Save contact photos inline, and load cards so avatars show
Stalwart refuses a blobId in a card's media ("blobIds in media is not
supported"), so adding or changing a photo always failed. The editor now
saves the photo as a data: URI, which Stalwart accepts and returns
unchanged, and leaves the card's other media as it was. Checked live on
0.16.22; the mock now refuses a blobId the same way.

Avatars in the mail list come from the address book's cards, and nothing
loaded those at sign-in, so a photo showed only after Contacts had been
opened. The cards now load in the background at start, the avatar uses
whatever cards are held, and a shared card's photo is fetched from the
account it belongs to.

Fixes #376.
2026-09-16 11:27:47 -07:00
jcoffey-dev f123467897 Let go of old message bodies and of exported files
Every message opened kept its full copy for as long as the tab was open.
The store now holds bodies for the 40 messages most recently wanted; older
ones go back to the list properties and are fetched in full again if
opened. The open conversation is never released.

Contact, settings and calendar exports go through downloadFile, which
releases the object URL once the download has started; three of them
never released it.
2026-09-16 10:59:57 -07:00
jcoffey-dev 71d211a13f Precompress the bundle, validate the shell, and pass byte ranges on
The web build now writes a Brotli and a gzip copy of each compressible
file, and the static handler serves the best one the browser accepts.
The bundle was gzipped again for every request and Brotli was never
offered; the main chunk is 122 KB with Brotli against 144 KB gzipped.

index.html and every static file carry an ETag, and a matching
If-None-Match gets a 304. The shell and the worker are revalidated on
every load and were downloaded whole each time.

Attachment downloads pass a plain byte Range to Stalwart and relay a 206,
so a PDF viewer or a video element can read in pieces where the server
allows it. On the reader's own device a blob is cached as immutable,
since its id names its content.

The upstream session and account-info caches drop entries past their age
on a timer; they lost an entry only on sign-out or refusal, not when a
session expired. The mock answers byte ranges.
2026-09-16 10:54:14 -07:00
jcoffey-dev 360420402d Sync contacts by what changed, and hold fewer calendar windows
A pushed contact change, and every edit or import made here, reloaded the
whole address book. The store now keeps the state its cards were read at
and asks ContactCard/changes what changed since, fetching only those cards,
split to maxObjectsInGet. A server that cannot say falls back to the full
load.

The calendar held every week or month the reader had visited, queried each
of them again on any event change, and walked them all on every render. It
now holds the four most recently shown; a change reloads those in place,
without emptying the view first, and a window dropped is loaded again when
it is next shown. Shared calendars' events are fetched from every account
at once, and instancesIn builds the added-shares set once.

The mock keeps a ContactCard change log, answers ContactCard/changes, and
announces a ContactCard/set, as Stalwart does.
2026-09-16 10:41:21 -07:00
jcoffey-dev 4c7b2ec370 Ask shared accounts together, and about files only when Files opens
At sign-in the files, contacts and calendar stores each asked every shared
account a question, one account after another: a request apiece before the
reader had opened any of those views.

Files now only works out at sign-in whether it is available. Which shared
accounts hold files is asked when the Files view or the file picker opens,
which the Files view already did on every visit. Shared address books and
calendars are asked for in one request, and the calendar store loads its
calendars, identities and shared calendars side by side.
2026-09-16 10:04:24 -07:00
jcoffey-dev 6139031689 Load the composer, previews, dialogs and other sidebars on demand
The main chunk carried everything the mail view might open: the file
preview and its Markdown renderer, the composer and its editor, the contact
editor, the filter and share dialogs, and the calendar, contacts and files
sidebars. Each is now loaded when first shown. The composer is also
fetched when the browser is idle after startup, so the first Compose does
not wait on the network.

Import the notification helpers statically where they already were: the
dynamic imports beside those static ones split nothing.
2026-09-16 09:48:04 -07:00
jcoffey-dev c6dbcaef63 Render only the message rows that changed
The rows were memoized, but nothing they were given kept its identity: the
list built each row's thread messages afresh, passed inline handlers and the
whole selection, and the click and context-menu handlers changed with the
selection and the menu. Every visible row rendered on every store write.

Rows now select their own message and conversation from the store, take a
plain selected flag, and get handlers whose identity never changes. The
conversation summary is memoized.

Refreshes also keep the object for a message whose fetched properties did
not change, so a refresh that changed one message renders one row.
2026-09-16 09:10:41 -07:00
jcoffey-dev a607450aaa Keep the service worker's cache to the current build
Cache a build asset only when it arrived: a 404 for a chunk asked for while
a deploy was changing over used to be kept as that chunk in that browser.

Refresh the offline copy of the app page after every successful page load,
and when it changes, drop the assets it no longer names along with any
failed response. The same tidy runs when this worker activates, which
clears what earlier workers left. Every deploy's chunks used to stay in
the browser for good.

The cache keeps its name: it also holds what the worker leaves for a tab
to collect.
2026-09-16 08:59:01 -07:00
jcoffey-dev dfe885a921 Close the smaller gaps from the security review
Ask for the account password before minting an app password, and keep
sessions the proxy checks from writing the account's own registry objects,
so a session left open on someone else's machine cannot take a credential
away from it. The password is compared with what the session holds; Stalwart
is asked only when 2FA moved the session onto an app password.

Serve attachments and proxied images with no-store on a device that is not
the person's own. Give files from a winmail.dat only the types the server
would show inline. Strip direction controls from sender and attachment
names and from saved filenames.

On signing out, send what is inside its undo window, then close every
composer, so the next person to sign in does not find the last one's draft.

Group sessions by the account Stalwart names and its server, so "sign out
other sessions" also reaches a session opened as a bare or differently
cased username.
2026-09-16 08:47:23 -07:00
jcoffey-dev e2b4cc18db Keep list refreshes within the server's limits and stop repeating them
Refresh the list in pages of at most maxObjectsInGet. A list scrolled past
500 rows used to send all of its ids to one Email/get, which the server
refuses whole, and the refresh failed without a word.

Fetch the other messages of listed threads in their own capped requests,
and only those not already held. They used to be back-referenced from
Thread/get with no bound.

Have loadThread fetch bodies only for messages not held in full. Every push
refetched the whole open thread's bodies, and the new attachment objects
made the reading pane redo work it had already done.

Build the list query from the folder names, roles and tree rather than the
mailbox map, which every reload replaces. Each reload used to build a new
query, which query() answered with another full refresh.
2026-09-16 08:25:57 -07:00
jcoffey-dev 98e105efd6 Bound what a request can make the server hold
Cap JSON bodies at 64 KB on every API route except JMAP and uploads, which
bound themselves. Sign-in used to read a body of any size before its rate
limits ran; the flood ceiling now also runs before the body is read.

For sessions whose JMAP requests are checked, lower the read cap from 16 MB
to 4 MB, allow four such reads per session at once, and turn requests away
with a 503 once 32 MB is held across everyone.

Count sign-in limits per /64 for IPv6, since one host holds a whole /64.

Bind the compose example to loopback, and run it read-only with no
capabilities and no-new-privileges. Keep .env.* out of git and the image
build context.
2026-09-16 07:54:20 -07:00
jcoffey-dev 55fcbf72f5 Harden the email sanitizer's CSS handling
Rewrite mail CSS in place instead of cutting pieces out, so a strip can no
longer join text into a closing </style>, and escape < last. Decode escaped
letters before checking, parse url() properly and drop CSS that cannot be
parsed, and disable @import and image-set() in every spelling. The body
element's style goes through the same path.

Give <area> links the same target, rel and click handling as <a>, strip
<style> blocks from HTML quoted into the composer, and contain the editor's
layout as .message-body already is.
2026-09-16 07:07:26 -07:00
jcoffey-dev 5b85c254e7 Split the mock server along the section markers it already had
server/src/mock/index.ts was 1,545 lines, the largest file in the repo.
It had carried `/* ---------- data ---------- */` style markers for a
while, so the seams were already drawn; this turns six of them into
files.

  mock/config.ts     51  env-derived constants, `account`, `state`
  mock/data.ts      410  fixtures and the builders that make them
  mock/engine.ts    442  the generic JMAP machinery -- get/set, filters,
                         patches, refs, limits, recurrence plumbing
  mock/handlers.ts  455  the `Method/name` dispatch table
  mock/events.ts     26  SSE fan-out and the Email/changes ring buffer
  mock/auth.ts       11  checkOtp
  mock/index.ts     195  HTTP routing, the session document, listen

TWO THINGS THAT COULD NOT JUST MOVE:

`counter` and `vacation` were module-level `let`s written from both the
fixture builders and the handlers. An ES module can export a `let` and
importers see it update, but they cannot assign to it, so both became
containers: `seq.counter` and `vacationBox.current`. Seven call sites.

`recordEmailChange`, `broadcast`, `sseClients` and `checkOtp` lived in
the HTTP section, but the handlers call them -- and index.ts imports the
handlers. Leaving them there is a cycle, so they became events.ts and
auth.ts rather than being dragged into data.ts, which is fixtures.

`account` is still exported from index.ts, because account.test.ts and
login-guard.test.ts reach for `mock.account` and `mock.server`.

Verified by running it, not only by compiling it: `npm run mock` boots
and listens, `/.well-known/jmap` returns a session, and a POST to
`/jmap/` answers Mailbox/get with the nine seeded folders and
Email/query with the seeded messages.
2026-09-15 23:22:34 -07:00
jcoffey-dev bd6a605d61 Group six more clusters out of web/src/lib
Takes the flat module count from 66 to 42, continuing what admin/ and
calendar/ started.

  lib/mailbox/  archiveDate, emptyFolder, folderMove, labelTree,
                mailboxName, mailboxRoute
  lib/sieve/    sieve, sieveApply, sieveFolders
  lib/input/    keyboard, swipe, touch, listSelection, dropUpload
  lib/notify/   notify, webpush, webpushEnable
  lib/sw/       swCache, swFacts, staleBuild
  lib/text/     html, markdown, text, emlName

FOUR THINGS THE FILENAMES GET WRONG, each checked by reading the file
rather than trusting what it is called:

  - appFolder is not a mailbox. It is the `ihasmail` folder in JMAP
    *Files*, where the client keeps signature images and synced settings.
    It stays flat.
  - format holds no formatting of text. It re-exports the date and clock
    formatters, so it belongs with dates/datetime, not with text/.
  - preview is the file viewer deciding what it can show without
    downloading, and source is where to point someone asking for this
    instance's AGPL source. Neither is about text.
  - notify is not Web Push. It is the tab title, the favicon badge and
    the new-mail sound -- in-app notification, which is why it sits with
    webpush rather than under sw/ with the service worker's own concerns.

threadScroll stays flat too: it decides where a conversation opens, which
is view state rather than a gesture, and input/ is honest only if
everything in it interprets something the reader did.

No behavior change. Almost every reference was on the @/ alias; eight
relative imports in files that did not move, or that moved away from a
sibling, needed rewriting by hand.
2026-09-15 23:17:50 -07:00
jcoffey-dev 4517d154a2 Split the extractable parts out of the mail store
store/mail.ts was 1,463 lines. It is now a directory, so `@/store/mail`
resolves to index.ts and none of the 36 modules importing `useMail`
changes a line:

  mail/props.ts      72   MAILBOX_PROPS, LIST_PROPS, FULL_PROPS, BODY_PROPS
  mail/types.ts     125   ListQuery, ListState, MailState, DEFAULT_SORT
  mail/mailboxes.ts  28   mailboxIcon, ROLE_ORDER
  mail/index.ts   1,266   the store, and everything bound to it

Everything exported before is still exported from index.ts, so this is
file layout and nothing else. No behavior change, no call-site change.

WHAT THIS DOES NOT DO, and why. index.ts is still 1,266 lines because
947 of them are one `create<MailState>((set, get) => ({ ... }))`. Cutting
that up means Zustand slices -- splitting the state object itself and
recombining it -- which is a change to how the store is built rather than
to where its text lives, in the part of the app that every screen leans
on. That deserves its own PR and its own argument, not a quiet ride along
with a file move.

Three things had to stay behind and are worth knowing about, because the
obvious boundary is wrong in each case:

  - `listKey` sits among the type declarations but is a function the
    store calls, not a type.
  - `ensureFolderPath`, `folderRefs` and `followFolders` read like folder
    helpers and look like they belong beside mailboxIcon, but they close
    over `useMail`. Moving them makes mailboxes.ts import index.ts, which
    imports mailboxes.ts.
  - the sieve import inside index.ts is `await import(...)`, not a static
    one, so rewriting import paths by their `from` clause misses it.
2026-09-15 22:49:14 -07:00
jcoffey-dev f7712b1c1e Group the admin and calendar modules, and stop calling screenshots docs
web/src/lib had grown to 85 flat modules -- 42% of the web source, about
12,800 lines -- with one subdirectory (smime/) to its name. The tell was
that a naming prefix had taken over a directory's job: eight adminX.ts
files sat adjacent because alphabetical order put them there, not because
anything said they belonged together.

  lib/admin/     adminAccess, adminDashboard, adminDirectory, adminDomains,
                 adminGroups, adminLists, adminRoles, adminTenants
  lib/calendar/  appointment, availabilityWindow, eventDrag, ics, recurrence

Tests move with their modules into lib/admin/__tests__ and
lib/calendar/__tests__, which is what views/ already does. describeRules
stays in lib/__tests__: it checks that sieve's describeRule and
recurrence's agree, so it belongs to neither.

recurrence.ts joins the calendar group and archiveDate.ts does not, which
is the opposite of the first guess from the filenames. archiveDate picks
the Archive/2026/09 mailbox for a message -- mail, not calendar --
while recurrence reads JSCalendarRecurrenceRule. schedule.ts is scheduled
*send*, so it stays put too. birthdays.ts is left alone deliberately: it
is read off the contact cards and only rendered by the calendar, so it
belongs to whichever of the two you ask.

docs/ held no documentation. It held ten JPEGs and the two scripts that
capture them, while the actual documentation is a separate site in the
ihasmail.org repository -- so anyone opening docs/ expecting prose found
a headless-Chrome driver. The images are now screenshots/, and the two
capture scripts join the other .mjs tooling in scripts/, which is where a
generator belongs. Renaming docs/ to screenshots/ wholesale would have
produced screenshots/screenshots/inbox-dark.jpg.

No behavior changes: every import was already on the @/ alias, so this is
path rewrites and nothing else.
2026-09-15 22:44:53 -07:00
jcoffey-dev 441fb07cc9 Pin every action to a commit SHA
A tag is a mutable pointer. `actions/checkout@v7` is whatever the
publisher last moved v7 to, so using one is not trusting the version that
was reviewed -- it is trusting every future version, including whatever
is pushed by whoever compromises the publisher's account. That is the
shape of the tj-actions/changed-files compromise: no repository changed a
line, the tags moved underneath them, and the action began dumping runner
memory to the logs.

Each `uses:` now carries the full 40-character SHA with its release in a
trailing comment. Read the comment for the version; the SHA is what runs.
Dependabot already covers github-actions weekly and updates both halves
together, so keeping current costs nothing.

The dataaxiom cleanup action was already pinned -- it is handed
`packages: write` and deletes things, so it was worth doing early -- and
only picks up the trailing-version convention here. Its comment loses the
"rather than a moving major tag" framing, which is no longer what makes
it different from its neighbors now that they are all pinned too.

The two `uses: ./.github/workflows/...` entries are local paths, not
actions: they always resolve within the commit already running and there
is no SHA to pin.
2026-09-15 22:12:25 -07:00
jcoffey-dev f3ee4ff65d Document the fork CI approval gate in CONTRIBUTING.md
Belongs with the commit before it and was left out of it by mistake.

The repository's fork-pr-contributor-approval policy is now
all_external_contributors rather than GitHub's first_time_contributors
default, so every run on an outside contributor's branch waits to be
started by hand instead of only their first one. A contributor who does
not know that reads a build check that never appears as an orphaned run
-- which this repository has had, during the 2026-08-26 Actions outage --
and pushes again to shake it loose. Neither that nor reopening the PR
starts it, so say so where the other main protection notes are.
2026-09-15 22:11:47 -07:00
jcoffey-dev 1ec9579db2 Add the pull request template CONTRIBUTING.md already refers to
Step 7 of "Submitting Pull Requests" tells contributors to open the PR
"filling out the PR template", and there has never been one. The four
things it names -- summary, related issues, screenshots for UI changes,
manual testing -- are the four sections here, plus translations, which
step 8 asks for separately and which is the easiest of the five to
forget: a missing catalog key renders its English source rather than
failing, so nothing in CI or on screen says it was skipped.

Also documents the CI approval gate on fork PRs, now that every outside
contributor's run waits to be started by hand rather than only a
first-time contributor's. Without a note, a contributor whose build
check never appears reads it as an orphaned run and pushes again to
shake it loose, which does nothing.
2026-09-15 22:06:52 -07:00
jcoffey-dev d1731efdb9 Use American English spelling throughout 2026-09-15 11:45:58 -07:00
jcoffey-dev 6ba89696ee Spell license the US way 2026-09-15 11:35:33 -07:00