Files
inbuxa-server/docs/spec/SPEC.md
T
jcoffey-dev 7d468ad22e Specs: a migration tool, with rollback as a path rather than an appendix
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.
2026-09-19 21:34:11 -07:00

573 lines
31 KiB
Markdown

# inbuxa-server: specification (draft)
inbuxa-server is the project. Whether the product it ships carries that name
or another inside the INBUXA brand is still open (§8). Not published: no
remote, and nothing public before the cutover in §7.
## 1. What this is
An AGPL-3.0-only fork of the Stalwart mail server that ships every feature
under the AGPL, with nothing held back for a paid edition. Two web front ends
come with it, both separate from the mail host: ihasmail for webmail, account
self-service and day-to-day account and tenant administration, and INBUXA
Admin, a fork of Stalwart's own web interface, for full server administration,
setup and recovery.
**Goals**
- Everything Stalwart's Enterprise Edition adds is available to everybody,
under the AGPL, rebuilt clean-room.
- One product: server, webmail and administration installed and versioned
together.
- No web interface on the mail host. Both front ends talk to the server over
JMAP and OAuth, and can run beside it or on another machine.
- Stay close enough to upstream that its releases can be taken quickly.
**Non-goals**
- Carrying, adapting or reading Stalwart Enterprise code (see §3).
- Using Stalwart's name or logo anywhere in the product (see §2.4).
- Changing Stalwart's storage formats beyond what upstream changes. Existing
Stalwart 0.16 data, including data written by Enterprise features, must
open unchanged (see §7).
## 2. The fork itself
### 2.1 What is taken
Every upstream file licensed `AGPL-3.0-only OR LicenseRef-SEL` is taken under
the AGPL. Nothing licensed only `LicenseRef-SEL` is taken.
Upstream marks Enterprise-only code two ways, and both must go:
- **Whole files** whose SPDX header is `LicenseRef-SEL` alone. At upstream
`d9dee0a` (2026-09-15) that is 63 files, about 21,000 lines, 5,500 of them
tests.
- **Snippets** inside dual-licensed files, between `SPDX-SnippetBegin` and
`SPDX-SnippetEnd`, whose identifier is `LicenseRef-SEL` alone. At the same
commit that is 117 snippets in 50 files, about 1,700 lines.
### 2.2 The strip step
Upstream publishes its own remover, `resources/scripts/ossify.py`, licensed
AGPL-3.0-only. It deletes Enterprise-only files and snippets by their markers,
which is this step's core. The fork runs it on every sync, wrapped by a
script of its own that:
1. Runs `ossify.py` over `crates/` and `tests/`.
2. Checks independently that no `LicenseRef-SEL`-only marker survives
anywhere in the tree, not just in `.rs` files.
3. Fails the sync if a marker is malformed, e.g. a snippet with no end, a
file whose header can't be read, or a new license identifier.
4. Writes a report listing what was removed and where, committed with the
sync.
It reads only the SPDX markers, never the code between them. That keeps the
people running it outside the clean room's restrictions. The removed code is
never committed to any branch of the fork, including history.
The wrapper is `tools/fork/strip.py`. Beyond `ossify.py` it:
- exports the release with `git archive`, never a checkout, so no upstream
history comes with it;
- checks every snippet's begin and end markers pair up before anything is
stripped (`ossify.py` would swallow an unterminated snippet to the end of the
file without saying so). Markers are matched anywhere on a line, because
upstream often ends a snippet with a trailing `} // SPDX-SnippetEnd`;
- turns the `enterprise` Cargo feature off where it's switched on. At
`v0.16.22` that's `crates/main`'s default features and 13 dependencies in
`tests/Cargo.toml`: 14 edits. `ossify.py` doesn't touch manifests, so without
this the stripped tree can't build;
- removes `mod` declarations left pointing at deleted Enterprise files. They
sit just outside the snippets `ossify.py` removes, so they survive it. At
`v0.16.22` there are 5: one in `common`, behind the test features, and four
in the integration tests. The normal build never meets them, but the tests
don't compile until they're gone;
- verifies the result across every text file, not just Rust, and reports what
was removed, the Cargo edits, upstream's Enterprise flags, and the feature
gates left for §2.3 to replace;
- lists the third-party code left in the stripped tree, as upstream's
comments mark it: another copyright holder or license, or a note that code
was ported or adapted from elsewhere. Any file `THIRD-PARTY.md` doesn't
cover yet is flagged as new. It's reported, not a failure: the notice goes
into `THIRD-PARTY.md` in the merge that brings the release in, since the
fork redistributes that code and its license requires the notice.
### 2.2a Snapshots, not a git fork
Upstream's git history contains the Enterprise code, so the fork can never
share it. There's no `git fork`, and no upstream remote in the fork's
repository. Instead:
- An **`upstream`** branch holds one commit per upstream release: the
stripped tree, and its strip report in the commit message (release tag,
upstream commit, counts, verification result). Nothing else is ever
committed there.
- **`main`** is the fork. A new release arrives by stripping it onto
`upstream`, then merging `upstream` into `main`. Git's three-way merge then
does what a fork's rebase would, without the history.
- The upstream clone the tool reads from lives outside the fork's repository
and is never pushed anywhere.
- Each import's full strip report is committed on `main` under
`docs/fork/strip-reports/<ref>.md` (and `.json`), beside the merge that
brought the release in.
- The snapshot includes upstream's `.github/`: its CI and release workflows,
workflows that auto-close issues and PRs from anyone not on its allowlist,
issue templates and Dependabot. On `main` the whole directory is moved to
`.github-upstream/`, so GitHub never runs it. Upstream changes to it still
merge there on each sync. INBUXA writes its own `.github/` when the
repository is first published.
### 2.2b What the first import proved (v0.16.22, 2026-09-18)
- The stripped tree builds (`cargo build -p stalwart`, 3 min 14 s on 16
cores) and the binary runs. There are 6 warnings, all unused imports left
where Enterprise snippets were cut out.
- Every crate's unit tests pass: 263 passed, 0 failed, 2 ignored. They build
with the integration crate in the build, because that's what switches on the
test features the unit tests rely on, and run from each crate's own directory
(`dav-proto` loads fixtures by relative path).
- The integration crate (`tests/`) compiles once the shared tests of
unrebuilt features are gated behind its `pending-rebuild` feature, off by
default: the OIDC-directory, tenant and archiving suites; the four
telemetry suites (alerts, metrics, tracing, webhooks); and the LLM parts of
the spam-filter suite. Each gate carries an `inbuxa: pending-rebuild` comment
and comes off when its feature is rebuilt. All nine features were rebuilt by
2026-09-19, so every gate came off and the feature itself was removed from
`tests/Cargo.toml` that day. A spec that still describes a suite as gated is
describing the state at its own date.
- Those gated suites are shared, AGPL test code that exercises Enterprise
behavior. That makes them a clean source for the feature specs, and ready-made
acceptance tests for the rebuilds.
- First boot (2026-09-18): the stripped debug binary, run in a local
container with the upstream `v0.16.22` image used only as a Debian runtime
(its glibc 2.41 covers the binary's 2.39), through ihasmail-oneshot's
sequence over JMAP. Every step worked:
- it started in bootstrap mode on port 8080;
- `x:Bootstrap/get` confirmed bootstrap mode, and `x:Bootstrap/set` returned
a permanent admin and wrote `config.json`;
- after a restart, the admin signed in, the server reported edition `oss`
(Enterprise reports `enterprise`) with 637 permissions against
Enterprise's 641, and the domain existed with DKIM keys generated;
- two users were created, and the DNS zone exported;
- one user's message went over SMTP submission (465), was DKIM-signed, and
was read by the other over IMAP (993) and JMAP;
- `x:MaskedEmail` and `x:ArchivedItem` answered `forbidden` with an
Enterprise upsell (`crates/jmap/src/registry/mod.rs`), and `x:Tenant/query`
answered an empty list.
- Not yet run: the integration suite itself, which needs its database
services.
Layout on `main`: the server source at the root, exactly as upstream lays it
out (so merges line up), the specs under `docs/spec/`, strip reports under
`docs/fork/`, and the fork tooling under `tools/fork/`. Upstream has no
top-level `docs/` or `tools/`, so these never collide.
### 2.3 Where the rebuilt features go
Upstream's published registry schema (`resources/schema/schema.json.gz`)
flags every Enterprise object and field with `"enterprise": true`: 9 objects
and 30 fields at `d9dee0a`. That list is the checklist for §4, and the wrapper
in §2.2 reports it on every sync, so a newly flagged field is noticed.
Rebuilt features live in one new crate of the fork's own, licensed
AGPL-3.0-only, plus the smallest possible hooks in upstream files. The hooks
sit where upstream's `enterprise` Cargo feature and `is_enterprise_edition()`
checks already are: 30 checks across 18 files at the commit above. That keeps
each sync's conflicts few and predictable.
The `enterprise` Cargo feature and every edition check are removed. There is
one edition. Done for the code on 2026-09-19: the last 14 gates, all of them
`#[cfg(not(feature = "enterprise"))]` on the path the fork always takes, came
off across 11 files, and no `is_enterprise_edition()` check survived the
import. The feature's *definitions* stay in the manifests, inert now that
nothing turns them on and nothing reads them, because removing them would
widen every sync's diff for no gain (§2.2). An import that changes a gated
file brings its gates back in the merge; they come off again with it.
One exception: scale-out storage lives in new AGPL files inside
`crates/store` (`backend/scaleout/`), because the rebuilt-features crate
depends on `store` and can't be called from it (`features/scale-out-storage.md`).
### 2.4 Name and marks
- No "Stalwart" in product names, binaries, images, UI text, packaging or
domains.
- Factual statements are allowed and required: "a fork of Stalwart",
"compatible with Stalwart 0.16 data". Upstream copyright notices stay on
every file they cover.
- Protocol identifiers stay as upstream has them, for example the JMAP
capability `urn:stalwart:jmap` and the `x:` object names. They're
interoperability, not branding, and renaming them breaks every existing
client. Anything the fork adds uses its own namespace (open: which one).
### 2.5 Packaging
Done 2026-09-18:
- The package and binary are `inbuxa` (`cargo build -p inbuxa`). The binary's
help, banner and every protocol greeting say INBUXA (the branding module,
`types::brand!()`).
- Settings come from `INBUXA_*` environment variables. Each still falls back
to its `STALWART_*` name, with a startup warning to rename it
(`types::branding::env_var`). That covers all nine the server reads:
`HOSTNAME`, `RECOVERY_MODE`, `RECOVERY_ADMIN`, `RECOVERY_MODE_PORT`,
`RECOVERY_MODE_LOG_LEVEL`, `ROLE`, `PUSH_SHARD`, `PUBLIC_URL`, `HTTPS_PORT`.
- **Not renamed, on purpose:** `STALWART_APP_` and the two `STALWART_SPAM_...`
names. They look like environment variables, but they're keys inside the
data store, so renaming them would orphan existing installed apps and
spam-classifier models.
- New installs default to `/var/lib/inbuxa` for data and `/var/log/inbuxa` for
logs. Existing installs keep the paths their configuration names, so no data
moves.
- The container image runs as user `inbuxa` (uid 2000, as upstream), with
`/etc/inbuxa` and `/var/lib/inbuxa` as volumes, `INBUXA_HEALTHCHECK_URL`, and
`inbuxa --config /etc/inbuxa/config.json`. The systemd unit
(`inbuxa.service`), launchd plist and AppArmor profile are renamed to match.
- `install.sh` is a stub that says there's no release yet. Upstream's version
would download and install Stalwart itself.
### 2.6 Versioning
Decided 2026-09-18.
- **INBUXA has its own version,** dated like the rest of its family
(ihasmail, ihasmail-oneshot, stalwart-migrator): `YYYY.M.D`, tagged
`vYYYY.M.D`, with a suffix for a second release on one day. The server's is
`types::brand_version!()`, and INBUXA Admin has its own.
- **The Stalwart base stays visible** while INBUXA tracks upstream:
`inbuxa --version`, the startup banner and events, OpenTelemetry's
`service.version`, and the JMAP `implementation` string all read
`2026.9.18 (Stalwart 0.16.22)`. It matters because Stalwart's data upgrades
are one-way, so anyone upgrading needs to know which base their data will be
converted to. Release notes and the strip report say it too.
- **Cargo versions follow upstream, untouched.** Every upstream release bumps
all ~30 manifests, so overriding them would conflict on every sync. The base
shown is read from Cargo, so it's always what was actually compiled in.
Upstream's internal data-format version (`types/src/semver.rs`), which drives
store migrations, is upstream's and is never changed.
- `DAEMON_NAME` (the `Received:` header) and the IMAP `ID` response carry the
INBUXA version alone. `VERSION_PUBLIC`'s deliberately vague `1.0.0` for Sieve
is left as upstream has it.
- **Stalwart 1.0** arrives like any release (strip, merge, release under the
next date), but as its own milestone: a merge dry run, a strip-report
comparison against 0.16, and an upgrade test on a copy of real data, because
it will almost certainly bring a one-way store migration.
- **Diverging from upstream** is expected eventually, though not soon. When it
happens, the base drops out of the version string and nothing else about
versioning changes.
## 3. Clean room
INBUXA runs on a paid Stalwart Enterprise license, so its maintainer is a
licensee with access to the Enterprise code. That license forbids publishing
derivative works of it, so the burden of showing independent creation falls
on this project. Rules:
1. **Spec writers** may use Stalwart's public documentation, the RFCs and
drafts, the published JMAP and registry schema (dual-licensed, so
available under the AGPL), and the observed behavior of a running server.
They may not use Enterprise source, snippets, private documentation,
support correspondence or anything from the license portal.
2. **Implementers** work only from the written specs in `features/`. They
must never have read Enterprise-only files or snippets. An AI session that
has seen Enterprise code (including the one that drafted this document)
doesn't implement.
3. **Records.** Each feature spec is dated and committed before its
implementation starts, and names its sources. Implementation PRs link the
spec they build from.
4. **Behavioral compatibility, not code compatibility.** Where existing data
has to keep working (masked addresses already handed out, archived items
already held), the behavior is specified from stored records and observed
results, not from how upstream implements it.
5. Legal review before anything is published.
## 4. Features to rebuild
In order. Each gets its own clean-room spec under `features/` before any code
is written.
| # | Feature | What an operator gets | Notes |
|---|---|---|---|
| 1 | Multi-tenancy | Tenants with their own domains, admins, quotas and queue visibility | Needed for anybody hosting mail for others. ihasmail already has a Tenants screen. Built 2026-09-18 in `crates/features`; status in `features/multi-tenancy.md`. |
| 2 | Masked email | Per-sender disposable addresses that deliver to the account | Existing addresses must keep delivering (§3.4). Built 2026-09-18 in `crates/features`; status in `features/masked-email.md`. |
| 3 | Undelete | Deleted mail held for a set period and restorable | Existing archived items must stay restorable. Built 2026-09-18 in `crates/features`; status in `features/undelete.md`. |
| 4 | Branding and templates | Operator logo, and the text of calendar alarm and invitation emails | INBUXA's branding is the default. Built 2026-09-18 in `crates/features`; status in `features/branding-and-templates.md`. |
| 5 | AI spam classification | An optional model's opinion as one spam signal, and a Sieve function that asks a model | Local and auditable model only: no hosted API by default. Built 2026-09-19 in `crates/features`; status in `features/ai-spam-classification.md`. |
| 6 | Monitoring history, live tracing, alerts | Stored metrics and traces, a live trace view, and threshold alerts | ihasmail's dashboard shows them. Spec: `features/monitoring.md`. |
| 7 | SCIM 2.0 provisioning | Accounts and groups managed by an identity provider | From RFC 7643 and RFC 7644. The largest piece. Spec: `features/scim.md`. |
| 8 | Scale-out storage | SQL read replicas; sharded blob and in-memory stores | For large installs only. Spec: `features/scale-out-storage.md`. |
| 9 | Per-domain directories | A domain signs in against its own LDAP, SQL or OIDC directory | Added 2026-09-18. Signing in through an OIDC provider as the server's directory is already AGPL; only the per-domain choice is Enterprise. Spec: `features/per-domain-directories.md`. |
| — | Seat limits, license keys | Nothing: there's no license | Removed, not rebuilt. |
## 5. The web front ends
**Which ihasmail.** Public ihasmail stays Stalwart-facing: its code, docs,
site and releases serve Stalwart users, and don't mention INBUXA. Everything
in these specs that changes ihasmail (the contract below, the "ihasmail"
sections of the feature specs, branding) goes into an **INBUXA-specific fork
of ihasmail**, until one product can confidently support both servers. In
this spec, "ihasmail" means that fork unless it says "public ihasmail".
Improvements that also help Stalwart users can still go to public ihasmail on
their own merits, framed for Stalwart. (Decided 2026-09-18. The fork's name and
location are open, §8.)
Decided 2026-09-18: two front ends, each with its own job.
- **ihasmail**: webmail, account self-service, and the day-to-day account and
tenant work done from inside the mail app by helpdesk staff and tenant
administrators. Its Administration stays as it is (accounts, domains, groups,
lists, roles, tenants, the dashboard), not grown to cover the whole server.
- **INBUXA Admin** (repository `inbuxa-admin`): full server administration,
first-boot setup and recovery-mode repair. It's a fork of Stalwart's own
web interface, `stalwartlabs/webui`, taken at `v1.0.11`.
Why fork `webui` rather than grow ihasmail: `webui` is entirely schema-driven,
building every form, list and menu from the schema the server serves, so it
already covers all ~125 setting types where ihasmail covers 18. It's the same
stack as ihasmail (React, Radix). And it contains no Enterprise-only code: all
111 files are dual-licensed AGPL, with no Enterprise-only files or snippets
anywhere in its 28-commit history. So it's an ordinary git fork with upstream's
history, needs no stripping and has no clean-room concerns. Its Enterprise
gating was client-side (hide or disable items when the server reports
`oss`/`community`), and the fork removes it at the one place the edition is
stored.
### 5.1 Shape
Both front ends run as their own containers, never installed onto the mail
server. INBUXA Admin is a static build. Its server address is set at build
time (`VITE_API_BASE_URL`) or deploy time (`<meta name="api-base-url">`), so
the same build can point at any server.
ihasmail stays a separate process: its Node server and its web app, run as a
container. It can run:
- **on the same host**, on a private network or loopback, which is the
default the installer sets up; or
- **on another host**, for operators who want no web front end near the mail
store.
It isn't compiled into the server. Its server side (sessions, image and
calendar-link proxies, push relay, rate limits, settings policy) would have to
be rewritten in Rust and would put a web front end back on the mail host,
which this design exists to avoid.
### 5.2 The contract between them
The full contract is in `contract.md` (drafted 2026-09-18). What follows is
the summary it expands.
Versioned, and advertised in the JMAP session so either side can check it.
- **Discovery.** The server's session names its webmail URL and the contract
version it speaks. ihasmail refuses a server outside its supported range,
with a clear message. Its current Stalwart 0.16 check becomes this check.
- **Sign-in by OAuth.** The server pre-registers ihasmail as a first-party
OAuth client (authorization code with PKCE). ihasmail holds tokens, never
passwords. Today it signs in with Basic auth and keeps the password in its
session.
- **Token revocation.** The server can revoke individual tokens and every
token for a client. Upstream's tokens are stateless and can only be revoked
by changing the password. ihasmail's "sign out other sessions" relies on
this.
- **Two-factor.** Handled in the OAuth flow, not by appending codes to
passwords.
- **Admin lane.** Administrative JMAP calls can be limited to ihasmail's
address or network, so an admin credential is useless from anywhere else.
- **Push.** Unchanged: JMAP push with VAPID, as ihasmail uses today.
- **INBUXA Admin's client.** INBUXA Admin signs in by OAuth (authorization
code with PKCE) as upstream's `webui` does, as client `stalwart-webui` for
now. The fork registers a first-party `inbuxa-admin` client with the
admin's own redirect URIs, and the admin switches to it (its
`<meta name="oauth-client-id">`).
- **Cross-origin access.** Verified 2026-09-18 against a separate
inbuxa-server: the whole admin flow works from another origin (sign-in, code
for the admin's redirect URI, token exchange, preflights, JMAP session,
`/api/schema`, admin calls). But the server answers
`Access-Control-Allow-Origin: *` on all of it. The fork restricts that to the
configured INBUXA Admin and ihasmail origins, since an admin API shouldn't be
callable from any web page.
- **A server without a public URL** returns relative OAuth endpoints (`/login`,
`/auth/token`) and an empty issuer. INBUXA Admin now resolves them against
the server's address. The installer sets the public URL (`INBUXA_PUBLIC_URL`)
so the server returns absolute ones.
### 5.3 No web interface on the mail host
Upstream installs its web interface at first boot, serving `/admin` and
`/account` from the mail server itself, by inserting a default web application
that downloads a release of `stalwartlabs/webui` from GitHub. The fork:
- inserts no default web application, and downloads no web interface;
- keeps the generic web-application mechanism for operators who want it,
documented as unsupported.
Done 2026-09-18. A first boot of the debug build answered 404 on `/admin` and
`/account` in bootstrap mode and after setup, held no web application and no
OAuth client, and logged no download. Setup worked over JMAP. An install
upgraded from Stalwart keeps any web application it has, and contract C-6
still registers its OAuth client.
Where each of the web interface's jobs goes:
| Was | Becomes |
|---|---|
| `/account`: password, app passwords, two-factor | ihasmail Settings: password and app passwords today; its two-factor work is written but backlogged |
| `/admin`: server administration | INBUXA Admin, deployed separately (§5.4) |
| First-boot setup | the installer, or INBUXA Admin's setup wizard against a server in bootstrap mode (§6) |
| Fixing a server the web front end can't reach | INBUXA Admin in recovery mode, or the host-side CLI (§6.3) |
### 5.4 Administration: who does what
- **INBUXA Admin** covers the whole server, every object type, generated from
the schema the server serves at `/api/schema`. New types upstream adds
appear with no admin release. Hand-built screens stay where upstream has
them: dashboards, live tracing, the delivery tester, the Sieve editor.
- **ihasmail Administration** keeps its scope: accounts, domains, groups,
lists, roles, tenants and the dashboard, for people who work from the mail
app. Where an ihasmail screen stops short, it links to the matching INBUXA
Admin page instead of growing to match it.
- **Overlap is fine** where both need it (accounts and domains appear in
both). Both are permission-gated by the server, so they can't disagree about
what anyone is allowed to do.
## 6. First boot, setup and recovery
### 6.1 The installer
Two workflows, and they differ (John, 2026-09-19): a **fresh installation**,
below, and a **migration** of an existing Stalwart server, which keeps its
accounts, passwords, DNS records and certificates and is specified in
`migration.md`. INBUXA's own cutover is the migration tool's first run.
ihasmail-oneshot is the starting point: a Go binary using only the standard
library, which drives `docker compose`. Its commands and options carry over.
Commands: `deploy`, `certs`, `destroy`, `version`.
`deploy` options:
| Option | Default | Meaning |
|---|---|---|
| `--domain` | (required; `example.test` with `--local`) | Mail domain |
| `--mail-host` | `mail.DOMAIN` | Mail server's hostname |
| `--webmail-host` | `webmail.DOMAIN` | Webmail's hostname |
| `--email` | `postmaster@DOMAIN` | ACME contact |
| `--local` | off | Loopback-only evaluation: no mail ports, no Caddy, no certificates |
| `--dir` | `./PROJECT` | Deployment directory, new or empty |
| `--project` | `ihasmail-DOMAIN` | Compose project name |
| `--stalwart-image` | pinned | Server image; renamed for the fork |
| `--ihasmail-image` | newest release, written as its dated tag | Webmail image |
| `--caddy-image` | pinned | Caddy image |
| `--webmail-bind` | `127.0.0.1:8080` | Host address for ihasmail's port |
| `--stalwart-bind` | `127.0.0.1:8081` | Host address for the server's plain HTTP. Renamed; it no longer carries an admin UI |
| `--subnet` | `172.31.253.0/24` | Private network for the stack |
| `--acme-directory`, `--acme-ca-root` | Let's Encrypt | Private CA for ACME |
| `--yes` | off | Skip confirmation |
### 6.2 What first boot does
Oneshot's sequence, all over JMAP, with no web interface involved:
1. Start the server with an empty configuration, which puts it in bootstrap
mode with a temporary admin pinned by environment variable.
2. Check it really is in bootstrap mode. A configured server refuses
bootstrap credentials, and oneshot stops.
3. Complete bootstrap through `x:Bootstrap`, which returns the permanent
administrator.
4. Look up the domain; turn on ACME explicitly (upstream's certificate flag
alone creates no ACME provider); retry certificates.
5. Trust forwarded addresses from the reverse proxy, and allow its IP, so the
auto-ban doesn't ban the proxy.
6. Create the first user; write the credentials file and the DNS zone file.
Found at the fork's first boot, and to fix:
- On first boot the server downloads Stalwart's web interface
(`webui.zip` from GitHub) before anything is configured. Removed
2026-09-18 (§5.3).
- Whenever `STALWART_RECOVERY_ADMIN` is set, its password is a full admin
login, outside bootstrap and recovery mode too. oneshot is safe, because
it recreates the container without the variable once setup is done, but a
plain restart keeps it. The fork honors the variable only in bootstrap and
recovery mode, and logs a warning when it's set otherwise.
- Default listeners after setup: HTTP 8080, HTTPS 443, SMTP 25, submissions
465, IMAPS 993, POP3S 995, ManageSieve 4190. There's no plain 587 or 143.
- The Enterprise upsell error (`crates/jmap/src/registry/mod.rs`) goes. With
every feature built in, there's nothing to sell.
Additions for the fork:
- Register the first-party OAuth clients (§5.2). Done 2026-09-18: see
contract.md C-6.
- Behavior fixes where upstream's first boot needs workarounds: an ACME order
that fails isn't retried on restart, some network settings need a restart,
and the default log path doesn't exist in the image. Each is a candidate for
fixing in the fork, and oneshot's workaround drops out once it is.
- For operators who install without the installer, INBUXA Admin's setup
wizard (upstream's bootstrap wizard, rebranded) performs the same steps
against a server in bootstrap mode.
### 6.3 Recovery without a web front end
When a settings change cuts ihasmail off from the server (a bad listener, a
bad certificate, a lockout), there has to be a way back in on the host:
- A CLI subcommand of the server binary that runs locally and edits the
registry directly, or starts the server in upstream's recovery mode with
only a loopback listener.
- Minimum it covers: list and edit listeners and certificates, reset an
administrator's password, lift IP bans, and export or import the
configuration.
- Upstream documents a CLI with `get`, `describe` and `snapshot` commands
(public docs, `management/cli`). Check how much of the above it already
covers before building anything.
- Open: whether this lives in the server binary or in the installer as
`oneshot recover`.
## 7. INBUXA cutover
INBUXA moves to the fork **before** the fork is announced. Its Enterprise
subscription can be ended at 30 days' notice, and the server falls back to
the Community Edition 15 days later. So the fork must be carrying INBUXA
before anything that could prompt that notice happens.
1. Fork builds and passes upstream's test suite, minus the removed Enterprise
tests, plus the fork's own.
2. INBUXA's data opens in the fork on a copy: tenants, masked addresses,
archived items, every account.
3. Masked addresses already handed out still deliver; archived items still
restore. Moot as things stand: INBUXA has no masked addresses and
retention is off (2026-09-19), so steps 2 and 3 come down to the tenant,
the domains and the accounts. It applies again the moment either feature
is switched on (`compat-tests.md`).
4. Cutover on the running host, with the Enterprise build kept for rollback.
Steps 1 to 3 are met as of 2026-09-19: the fork builds and its suites pass
(`container-tests.md`), and all eight compat tests pass against a copy of
INBUXA's store, with the caveat that two of them have nothing to read and
the tenant administrators' own view is not yet recorded
(`compat-tests.md`). What remains is step 4, drafted as `cutover.md`.
5. Only then: public repository and announcement, with the INBUXA fork of
ihasmail released alongside. Public ihasmail stays Stalwart-facing (§5).
## 8. Open decisions
- The INBUXA fork of ihasmail is **ihasmail-inbuxa** (named 2026-09-18). Open: its repository, and how it tracks
public ihasmail (§5).
- Product name: whether the shipped product is called inbuxa-server or
something else inside the INBUXA brand.
- The namespace for the fork's own JMAP capabilities.
- Whether the recovery CLI lives in the server binary or in the installer.
- Whether the installer stays its own repository or joins the fork's.
- Governance: solo, or set up for outside contributors from the start
(contributing guide, sign-off on contributions).
- Whether the fork follows upstream's version numbers or has its own.
- The Enterprise License text (`LICENSES/LicenseRef-SEL.txt`) stays while
upstream files still name it in their headers (`AGPL-3.0-only OR
LicenseRef-SEL`, 1,112 files at v0.16.22). Deferred (John, 2026-09-18) until
the files are cleaned up: then the headers become `AGPL-3.0-only`, the text
goes, and the question joins the legal review (§3, rule 5).