Monitoring, SCIM, scale-out storage and per-domain directories each say
"Built 2026-09-19" in their own implementation-status sections, and the
code is where those sections say it is. The §4 table still listed them as
specs awaiting a build, which made the whole feature set look half-finished
to anyone reading the table alone.
Each row now names where the feature landed, as rows 1 to 5 already did.
Monitoring and per-domain directories sit outside crates/features, so their
rows name the paths rather than the crate.
§2.2b already recorded that all nine were rebuilt by 2026-09-19 and every
pending-rebuild gate came off. Only the table lagged.
INBUXA's cutover is the first run of something other operators will want:
an existing Stalwart server becoming an INBUXA one with nothing re-entered
and nothing re-issued. Accounts, passwords, app passwords, OAuth sessions,
aliases, tenants, DNS records and provider settings, certificates and ACME
state, Sieve scripts, the queue and the mail all live in the store, so a
migration that copies the store carries them.
Offered beside the fresh-install workflow, which has different questions to
ask, so §6.1 now names both.
It rolls back, which is where it parts company with stalwart-migrator:
that one upgrades in place and says outright it cannot undo a migration.
This one never writes to what it migrates from, so going back is stopping
one service and starting another. Rollback is automatic when verification
fails, available on demand while the old install stands, honest about the
mail that stays behind, and never points the old server at the store the
fork has written.
Every phase is timed, and the number to advertise is the downtime, phases
2 to 7, not the total that preflight and the copy dominate. The report
writes both as JSON so a release note quotes something measured.
Steps 1 to 3 of SPEC §7 are met, so the remaining one is running the fork
as the mail server. The draft covers the sequence on the host, what to
check before letting mail flow, and what to watch in the first week.
Two things it refuses to gloss: rolling back stops being a snapshot restore
the moment the fork accepts a message, because nobody has tested whether
the Enterprise build reads a store the fork has written; and certificate
renewal is the failure that arrives 90 days late and quietly, on the one
path the suites couldn't settle.
Nothing in it has been rehearsed. The rehearsal on a copy is step 2 of
"Before the day", and it is what turns "the data opens" into "the server
runs on it".
Run from a copy of the stopped server's RocksDB store. Eight green lines,
which are worth reading carefully: six carry weight, and masked_email and
undelete carry none, because there are no masked addresses and retention is
off, so they iterate an empty list. tenant_compat checked the tenant, its
quotas and its members, but not what a tenant administrator can see, which
needs a --tenant-admin recording.
ai_compat is the one that might have looked vacuous and isn't: the twelve
LLM_ tags are there with the scores that were observed.
Three things had to be fixed first, each failing all eight identically and
none about the data: listener names, privileged ports, pending tasks. The
next import's copy will bring the same three, so they are written down.
SPEC §7: cutover steps 1 to 3 are met. Step 4 remains.
record-compat.py ran against the live server as a server-level
administrator: 8 accounts, which matches the dashboard, so it reached all
of them. One tenant with its quotas and members; no masked addresses, no
archived items.
The empty files are right rather than short. INBUXA has no masked
addresses and retention is off, so masked_email_compat and undelete_compat
iterate an empty list: they pass without comparing anything, which is worth
saying plainly, because a green run from either would otherwise read as
evidence of compatibility. That puts them where scim_compat and
per_domain_directory_compat already sit.
So one recording carries weight, expected.json, and it is made. SPEC §7's
cutover steps 2 and 3 come down to the tenant, the domains and the
accounts until either feature is switched on.
Every one of the 14 was `#[cfg(not(feature = "enterprise"))]` on the arm the
fork always compiles: the Enterprise arms went with the import, and nothing
turns the feature on. Removing the attribute leaves the same code, now
unconditional, in 11 files.
Two of them looked like behavior worth checking before touching: the
`validate_tenant_quota` stub that always passes, and the refusal to cancel a
pending DestroyAccount task. The stub is vestigial — the rebuilt
multi-tenancy enforces quotas in `crates/features/src/tenancy/quota.rs` for
those objects and more — and the refusal is undelete's open question, which
this change leaves exactly as it was.
The binary builds with no new warnings, and `system_tests` and `jmap_tests`,
which cover the touched registry, task-manager and auth paths, both pass.
The feature definitions stay in the manifests, inert: taking them out would
widen every sync's diff for nothing.
The feature held the shared tests of features the fork hadn't rebuilt yet.
All nine are built and the last gate came off with per-domain directories,
so the feature was defined and documented but gated nothing.
The crate builds and holds the same tests without it: 113 by default, 118
with postgres, mysql and redis. SPEC §2.2b keeps its account of the first
import and now says when the gates came off, so a spec that still describes
a suite as gated reads as the record of its own date, which is what
monitoring.md's gate table already calls itself.
A few of upstream's dual-licensed files carry code from other projects
under MIT or BSD terms. The fork redistributes it, so their licenses
require the notices to travel with it. THIRD-PARTY.md reproduces them.
strip.py now reads the stripped tree's comments for another copyright
holder, another license, or a note that code came from somewhere else, and
names any file THIRD-PARTY.md doesn't cover. It reports, never fails: the
notice goes in with the merge that brings the release in.
On v0.16.22 it finds 14 files, all of them covered. The rest of the report
is byte-for-byte what the committed one says, so the scan disturbs nothing
it already did.
The classifier sends only the subject and text, between unforgeable markers
after the operator's prompt, to an OpenAI-compatible endpoint the operator
configured; nothing is preset. Its answer maps to an LLM_ tag whose score is
clamped (+5.0, -1.0 by default) and can never discard or reject on its own;
X-Spam-LLM is sanitized, encoded and folded, and a planted one is removed.
Failures, timeouts past the ceiling, a full slot or a paused model leave
mail flowing untagged. llm_prompt answers trusted scripts, and accounts
holding interactAi within an hourly limit. Redirects aren't followed and no
content or secret is logged. The limits live in inbuxa:AiLimits.
Acceptance tests 1 and 3 to 21; test 2 as the re-enabled shared llm case,
whose setup no longer waits on a rules file from a developer's own path;
test 22 written as the ignored ai_compat.
Logos resolve domain, then tenant, then server-wide, then the built-in, with
subdomains finding their domain. GET /logo serves a data-URL image, redirects
to a URL logo without fetching it, sandboxes SVG, and answers 404 when no
custom logo applies. Emails embed the first PNG, JPEG or GIF logo. Logo and
template writes are checked; stored templates are read at send time, always
escaped, and fall back to the built-in with a build warning when they don't
parse. The RSVP page is served byte for byte with a CSP and no-referrer. The
sign-in and RSVP pages load the logo through an image element. MT-22's
session logo follows the chain to the server-wide logo.
Acceptance tests 1 to 17; test 18 written as the ignored branding_compat.
With archiveDeletedAccountsFor set, a destroyed account's record is kept in
the fork subspace with its id, its DestroyAccount task is due at the end of
the period, and its shares are suspended both ways. Its addresses can't be
taken by new accounts, aliases, lists or masks. inbuxa:DeletedAccount/get
lists kept accounts to server and tenant administrators; /set restores one
with a new password (same id, task cancelled, shares reinstated) or destroys
it now. The destroy task also clears undelete's own records.
Acceptance test 14; test 16 written as the ignored undelete_compat.
Discovery and a contract version in the session; front ends configured once
(x:FrontEnds); OAuth with required registration, first-party clients,
server-hosted sign-in and consent for everything else; per-grant revocation;
cross-origin limited to the front ends; an admin lane by scope; push
unchanged. Records what upstream does today, including that it accepts any
client and redirect URI by default, and the phishing that allows.
inbuxa --version, the banner, startup events, OpenTelemetry and the JMAP
implementation string read "2026.9.18 (Stalwart 0.16.22)". The base comes
from Cargo, which keeps following upstream so version bumps merge cleanly.
Received headers and IMAP ID carry the INBUXA version.
Administration is no longer moving wholesale into ihasmail. INBUXA Admin, a
fork of the schema-driven webui (no Enterprise-only code, ordinary fork),
covers the whole server, setup and recovery from its own deployment.
ihasmail keeps its account and tenant administration. Contract additions:
the inbuxa-admin OAuth client, cross-origin access limited to the two
front ends (the server currently allows every origin), and relative OAuth
endpoints on servers with no public URL.
- crates/main: package and [[bin]] renamed to inbuxa; homepage inbuxa.org;
license AGPL-3.0-only (upstream is dual; the fork takes the AGPL).
- types::branding::env_var reads INBUXA_<name>, falling back to
STALWART_<name> with a warning, for all nine server settings.
STALWART_APP_ and STALWART_SPAM_* storage keys are unchanged.
- New-install default paths /var/lib/inbuxa and /var/log/inbuxa.
- Dockerfiles, systemd unit, launchd plist and AppArmor profile renamed.
- Upstream's .github moved to .github-upstream so none of it runs.
- install.sh stubbed: upstream's would install Stalwart.
- Two missed brand strings: the SMTP Received header and the utils user agent.