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.
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.
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.
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.
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.
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.
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).
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)
With conversation view off, marking a message unread from the list --
the hover button, the right-click menu -- marked that message. Opening
it and pressing Mark as unread in the toolbar above it marked every
message in its thread, and so did Move to, Report spam and Delete.
The setting already reaches all the way into the reading pane: the list
draws one row per message, and `visibleMessages` narrows the pane to the
one opened. The toolbar was half converted. Its labels were right --
Mark as unread against Mark as read, the star, the labels shown -- all
of those read `messages`, which is the narrowed set. Only `rowIds`, the
one thing actually handed to the action, still read `thread.emailIds`.
So the button said one message and did the whole conversation.
`rowIds` is now the same question `visibleMessages` answers for the
pane, asked of the same ids, with the same fallback: an id that names
nothing in the thread -- a link from somebody with conversation view on,
a stale `m` in the URL -- shows the conversation, so the toolbar takes
the conversation. Conversation view on is unchanged: nothing is singled
out, so the whole thread comes back as before.
No new strings.
(cherry picked from commit f627bfc1237d6e8bbc728147022f624c3834d648)
A website contact form mails the site's own address: From and To are
both info@thesite, and the person who filled the form in is in Reply-To.
Replying addressed the draft to info@thesite -- the site's own desk --
instead of to them.
The reply already knows two shapes. A message somebody sent me is
answered to its Reply-To, which is what that header is for. A message
*I* sent is answered to the people I wrote to, and deliberately not to
my own Reply-To, which is where answers to me belong and would send my
reply to myself. A contact form passes the test for the second: every
address in From is mine.
So it fell down the chain the second shape keeps for a message with
nobody obvious to answer -- To without me, then Cc, then, having run
out, every address on the message, which here was mine alone.
The Reply-To now goes in that chain, one step before the last: when no
recipient but me is left and the message names a Reply-To that is not
mine either, that address is who it is really from. Keeping it after the
Cc is what leaves a message I did send alone -- somebody I actually
wrote to still beats my own Reply-To, which is the case the existing
guard was built for and its test still holds.
No new strings.
(cherry picked from commit 01dc322aebcff0e8075c54f81eb7d171a32fb9e7)
Reading a message fetches its remote images through this server, so the
sender learns nothing about the reader. Quoting the same message into a
reply fetched them directly: same pixel, same reader, but the request
carried their IP and user agent -- exactly what the proxy withholds.
A quote now proxies them the way the message view does. That alone would
be wrong, because a proxied URL belongs to this deployment: sent
unchanged it would reach the recipient as images only this server can
serve, broken for them and a beacon back here. So buildEmailObject turns
them back into the addresses they came from, beside the pass that
restores images blocked under pr411 and the one that turns editor blob
URLs into cid: references.
Deployments with the proxy off are unaffected: the quote fetches
directly, as reading does there.
Three tests from pr411 asserted the address sat in src when images were
allowed, which was the old behaviour; they now ask whether the draft
fetches it at all, proxied or not.
No new strings.
(cherry picked from commit 23557a72a2a72088081792f8ea0cabedea4e7bcb)
Replying sanitized the quoted body with allowRemote: true, so quoting
fetched every remote image in the message whatever the reader had
decided about it. A tracking pixel in the quote then reported the
message read, and the address live, to whoever was counting -- the thing
leaving the images blocked was meant to prevent. Edit as new and opening
a draft that quotes a message did the same.
The decision now lives in one place, remoteImagesAllowed(), asked with
the same inputs the reader's answer used: the image policy, the trusted
senders, whether the sender is a contact, and whether Show images was
pressed on that message. The last of those was component state, so it
moves to the mail store, where the composer can see it.
Blocked images already keep their address in data-ihm-remote, so nothing
is lost by not fetching: it goes back on the way out, and the sent quote
is what its sender wrote. The recipient's client decides for itself, as
it would with any other client's reply.
Before pr408 this needed a rich-text default to reach; the format offer
made it reachable from plain text, which is how it was found.
No new strings.
(cherry picked from commit d329b33912921a851c548bffef5085e5fbf72bed)
Switching a reply between plain text and rich text converted whatever
body the draft was showing. Going from plain text to rich, that meant
the quoted message came back as the "> " text quote run through a
converter -- the sender's formatting, images and links gone, even though
the original markup was sitting on the draft untouched.
Both forms of the quote are prepared when the reply opens, so keep them
on the draft and re-attach the right one when the format changes. Only
what the author typed above the quote is converted. Where the quote
can't be found any more -- edited by hand, or a draft that quotes
nothing -- the whole body is converted as before, which is what every
non-reply draft does.
No new strings.
(cherry picked from commit 88f9e6c50a04f8ffc4702d1d1e3cffa6a93e7690)
A reply opened in the format the settings ask for, whatever the message
being answered was written in, and the per-draft switch was buried in
the composer's ⋮ menu. Replying in plain text to a rich text message
throws away the formatting; replying in rich text to a plain-text one
overrides what the sender chose to write in.
When the two disagree the composer now says so above the editor -- "This
message is rich text", with a Switch button and a dismiss -- and the
draft still opens in the format the settings ask for. Switching converts
that draft only and leaves the setting alone; switching from the ⋮ menu
answers the offer too. Forwards get it as well, where the formatting
being passed on is somebody else's.
What counts as rich text is hasHtmlAlternative(), which reads the body
part's own type: `htmlBody` is derived (RFC 8621 4.1.4), so a plain-text
message has one too and its presence proves nothing.
The mock said otherwise -- it returned an empty `htmlBody` for a
plain-text message, where Stalwart 0.16.21 returns the text/plain part
in both lists. Both builders now answer as the server does, so the path
this feature depends on is exercised in development rather than only
against a real mailbox.
Two new strings, translated in all nine catalogs; the buttons reuse the
menu's existing "Switch to plain text" / "Switch to rich text". The
count falling back to English stays at 16 in every language.
Fixes#407
(cherry picked from commit d992442b8194be5e9c48204332c7243d9587b4ca)
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)
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)
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.