Rewrite as a Rust, Apache-2.0 workspace
Supersedes the Go + embed-Mox design. The Go tree is removed; its
architecture doc is preserved at docs/archive/ARCHITECTURE-go-embed-mox.md
because its competitive analysis and data model still hold.
Five decisions recorded as ADRs:
0001 Rust, not Go — accepting ~5,500 lines of protocol code that Mox
would have given us free, to get the first permissively licensed
Rust mail server. Costs stated plainly.
0002 Apache-2.0, not MIT or AGPL — patent grant, trademark, CLA-free
contribution. Public on GitHub; Gitea stays as the private fallback.
0003 Stalwart's primitive crates (Apache-2.0/MIT) yes; its AGPL server
crates never. DANE and MTA-STS sit on the AGPL side of that line,
which is why we write our own.
0004 Milestones, reordered: embedded inbound is required at launch.
0005 Oracle Cloud blocks outbound :25, so direct-to-MX is impossible on
the launch host. Split delivery is mandatory, not an on-ramp.
Twelve crates in three tiers. Tier 1 (mail-dane, mail-mta-sts, mail-dsn)
is standalone and publishable — no `dane` or `mta-sts` crate exists on
crates.io at all today.
openmail-relay ships the provider table as data, with SES and Oracle from
the start. Oracle's and Resend's SPF includes are deliberately None: a
guessed include turns the DNS check green against a mechanism the provider
does not honour, and mail still fails SPF silently.
cargo check/test/clippy/fmt all green; unsafe_code is forbidden workspace
wide; cargo-deny enforces the licence policy in CI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkyvfNJGTshJNE9FtwPLk7
This commit is contained in:
co-authored by
Claude Opus 5
parent
428040d964
commit
36b15ddcaf
@@ -0,0 +1,68 @@
|
||||
# ADR 0001 — Rust, not Go
|
||||
|
||||
**Status:** Accepted, 2026-09-02. Supersedes the Go + embed-Mox design in
|
||||
[`../archive/ARCHITECTURE-go-embed-mox.md`](../archive/ARCHITECTURE-go-embed-mox.md).
|
||||
|
||||
## Context
|
||||
|
||||
The archived design chose Go in order to embed Mox (MIT) as a library, getting
|
||||
~14,000 lines of production-tested mail correctness for free:
|
||||
|
||||
```
|
||||
message 2,884 dkim 2,051 spf 1,560 smtpclient 2,012 junk 1,302
|
||||
mtasts 703 dsn 771 dane 516 scram 796 sasl 327 …
|
||||
```
|
||||
|
||||
The Rust ecosystem does not offer an equivalent. Stalwart is the only
|
||||
production Rust mail server and its server crates are `AGPL-3.0-only OR
|
||||
LicenseRef-SEL`, which we cannot use under Apache-2.0.
|
||||
|
||||
## What Rust actually costs
|
||||
|
||||
Stalwart Labs publishes its *primitives* permissively (Apache-2.0 OR MIT), and
|
||||
those cover more than expected: `mail-parser` (MIME), `mail-auth` (DKIM1,
|
||||
**DKIM2**, ARC, SPF, DMARC, ARF, TLS-RPT), `mail-builder`, `mail-send`,
|
||||
`smtp-proto`. `hickory-resolver` covers DNS and DNSSEC.
|
||||
|
||||
What no permissive Rust crate provides, and we therefore write:
|
||||
|
||||
| | LOC (Mox equivalent) | Rust prior art |
|
||||
|---|---|---|
|
||||
| DANE | ~516 | **none on crates.io** |
|
||||
| MTA-STS | ~703 | **none on crates.io** |
|
||||
| SMTP server session loop | ~3,395 (`go-smtp`) | `smtp-proto` parses only |
|
||||
| DSN | ~771 | none |
|
||||
| Junk (beyond a toy) | ~1,302 | `bayespam` has no training persistence |
|
||||
| iprev / DNSBL / rate limit | ~370 | `dnsbl` crate abandoned since 2021 |
|
||||
|
||||
**~5,500 lines of adversarial protocol code**, versus zero in Go.
|
||||
|
||||
## Decision
|
||||
|
||||
**Rust.** Accepted with eyes open.
|
||||
|
||||
## Consequences
|
||||
|
||||
Negative, and stated plainly so nobody is surprised later:
|
||||
|
||||
- v1 is roughly a quarter further out.
|
||||
- DANE and MTA-STS move from *battle-tested* to *ours*, and both **fail
|
||||
silently**: a DANE bug downgrades TLS without erroring; an MTA-STS bug defers
|
||||
mail nobody sees. That tail does not close at ship — it closes after enough
|
||||
strangers' mail has flowed through it.
|
||||
- Mitigation: every outcome in those crates is an explicit enum with no
|
||||
`Default` and no `bool`, so a caller cannot accidentally read "no policy" as
|
||||
"verified". See `mail_dane::DaneResult`.
|
||||
|
||||
Positive:
|
||||
|
||||
- We ship the first permissively licensed DANE and MTA-STS in Rust, and the
|
||||
first permissively licensed Rust mail server.
|
||||
- `mail-auth` gives us DKIM2 and ARC, which Mox does not have.
|
||||
- One language for the mail engine and the agent layer.
|
||||
|
||||
## Rejected alternative
|
||||
|
||||
**Go now, Rust later**, with the two crates published early to plant the flag
|
||||
at low cost. Rejected: it puts the strategic position — "the permissive Rust
|
||||
agent mail server" — behind a rewrite that would probably never be scheduled.
|
||||
@@ -0,0 +1,46 @@
|
||||
# ADR 0002 — Apache-2.0
|
||||
|
||||
**Status:** Accepted, 2026-09-02. Supersedes the MIT choice in the archived
|
||||
architecture (§8).
|
||||
|
||||
## Decision
|
||||
|
||||
**Apache-2.0**, and the repository is public on GitHub
|
||||
(`karti-ai/openmail`). The Gitea mirror (`OSS/openmail`) stays as the fallback
|
||||
if the project ever needs to go private.
|
||||
|
||||
## Why not MIT
|
||||
|
||||
Same freedoms, but Apache-2.0 adds three things that matter here:
|
||||
|
||||
- **§3, express patent grant.** Email authentication is a standards thicket —
|
||||
DKIM, DKIM2, ARC, DMARC. MIT's patent grant is implicit at best, and that is
|
||||
what enterprise legal review flags.
|
||||
- **§6, trademark reservation.** "OpenMail" is a generic name with at least
|
||||
three unrelated projects already using it. Apache-2.0 protects the name while
|
||||
the code stays free.
|
||||
- **§5, contributor terms.** Inbound contributions are licensed on the same
|
||||
terms without a separate CLA.
|
||||
|
||||
Inbound compatibility is clean: every dependency is Apache-2.0 or MIT.
|
||||
|
||||
## Why not AGPL
|
||||
|
||||
AGPL + a commercial exception is the standard way to protect a future hosted
|
||||
offering — it is exactly what Stalwart does (`AGPL-3.0-only OR
|
||||
LicenseRef-SEL`). We reject it because it makes us unusable by the commercial
|
||||
agent builders who are the intended audience, and because being *the*
|
||||
permissive option is the entire competitive position. Stalwart's AGPL is the
|
||||
reason its competitors must run it in a sidecar; we do not want to be that for
|
||||
someone else.
|
||||
|
||||
## Consequences
|
||||
|
||||
- "No GPL/AGPL/LGPL anywhere" remains policy, but the *reason* changed. Under
|
||||
MIT it was a compatibility fact; under Apache-2.0 it is a deliberate choice,
|
||||
since Apache-2.0 is one-way-incompatible with GPL-2-only. Enforced in CI by
|
||||
`cargo-deny`.
|
||||
- Every source file gets no licence header (the `LICENSE` + `NOTICE` pair is
|
||||
sufficient and headers rot); `NOTICE` must be shipped with any redistribution
|
||||
and lists third-party attribution.
|
||||
- Anyone may fork this closed. That is the intent, not a leak.
|
||||
@@ -0,0 +1,34 @@
|
||||
# ADR 0003 — Write our own crates; use Stalwart's primitives, never its server
|
||||
|
||||
**Status:** Accepted, 2026-09-02.
|
||||
|
||||
## The licence boundary
|
||||
|
||||
Stalwart Labs ships two distinct things:
|
||||
|
||||
| | Licence | Us |
|
||||
|---|---|---|
|
||||
| The **server** (`stalwartlabs/stalwart`, `crates/*`) | `AGPL-3.0-only OR LicenseRef-SEL` | ❌ never |
|
||||
| The **primitives** (`mail-parser`, `mail-auth`, `mail-builder`, `mail-send`, `smtp-proto`) | `Apache-2.0 OR MIT` | ✅ dependencies |
|
||||
|
||||
DANE and MTA-STS live in `crates/smtp` and `crates/common` — **on the AGPL side
|
||||
of that line.** That is precisely why we write our own.
|
||||
|
||||
## Rule
|
||||
|
||||
- Depending on the permissive primitive crates is fine and intended.
|
||||
- Reading the AGPL server crates for *understanding* is fine.
|
||||
- Copying, adapting or transliterating any line from them is **not**, and would
|
||||
contaminate the whole workspace. When implementing DANE or MTA-STS, work from
|
||||
the RFCs (7672, 8461, 6698) — not from `stalwart/crates/smtp`.
|
||||
- The research mirror at `~/Desktop/ProjectMail/mail-servers/stalwart` is
|
||||
read-only reference. Same for `maddy` (GPL-3) and `BillionMail` (AGPL).
|
||||
|
||||
## Which of our crates get published
|
||||
|
||||
Tier 1 (`mail-dane`, `mail-mta-sts`, `mail-dsn`) are published standalone: they
|
||||
depend on nothing in this workspace, they fill real holes in the ecosystem, and
|
||||
their value to us is partly that other projects audit them. Names verified
|
||||
available on crates.io 2026-09-02, as is `openmail` itself — reserve early.
|
||||
|
||||
Tier 2 and 3 stay in-workspace until their APIs settle.
|
||||
@@ -0,0 +1,51 @@
|
||||
# ADR 0004 — Milestones
|
||||
|
||||
**Status:** Accepted, 2026-09-02.
|
||||
|
||||
Reordered from the archived Go plan. The original put embedded inbound at
|
||||
milestone 4 and treated it as optional-until-later; ADR 0005 makes it
|
||||
**required at launch**, because the launch host cannot send direct-to-MX at all.
|
||||
|
||||
## v0.1 — the agent layer, provable without mail
|
||||
|
||||
Nothing here needs a working mail server, which is the point: it is all
|
||||
testable in CI.
|
||||
|
||||
- [x] Apache-2.0 workspace, 12 crates, `cargo check` green
|
||||
- [x] `openmail-relay` provider table (SES, Oracle, SendGrid, Postmark, Resend, custom)
|
||||
- [ ] `openmail-core::extract::strip_quoted` — the first real algorithm
|
||||
- [ ] `openmail-core::thread` resolution, both bases
|
||||
- [ ] `openmail-store` — Postgres schema, embedded migrations, S3 blobs
|
||||
- [ ] `openmail-api` — v0 REST, bearer auth
|
||||
- [ ] Ingest endpoint: POST a raw `.eml` and get a threaded, extracted message
|
||||
back. Closes the loop with **zero mail infrastructure.**
|
||||
|
||||
## v0.2 — receive
|
||||
|
||||
- [ ] `openmail-smtpd` on :25, real MX for a test domain
|
||||
- [ ] `openmail-guard` gate, `mail-auth` SPF/DKIM/DMARC verdicts recorded
|
||||
- [ ] `openmail-junk` scoring
|
||||
- [ ] `message.received` webhooks + WebSocket
|
||||
|
||||
## v0.3 — send, and the MCP surface
|
||||
|
||||
- [ ] Relay send via SES and Oracle, DKIM-signed locally
|
||||
- [ ] `mail-dsn` bounce handling wired to outbox state
|
||||
- [ ] `openmail-mcp` — an agent creates an inbox, receives, and replies, alone
|
||||
|
||||
## v0.4 — direct-to-MX, and the crates ship
|
||||
|
||||
- [ ] `mail-dane` and `mail-mta-sts` complete, **published to crates.io**
|
||||
- [ ] Direct MX delivery with both enforced
|
||||
- [ ] IP warmup, FBL enrolment, suppression lists
|
||||
|
||||
## v1.0
|
||||
|
||||
- [ ] IMAP front-end so humans use their own client against the same mailbox
|
||||
- [ ] Multi-tenancy beyond `pods`
|
||||
- [ ] Deliverability track record worth publishing
|
||||
|
||||
## Not in v1
|
||||
|
||||
A hosted SaaS, billing, or a webmail UI. `pods` exists so the SaaS path stays
|
||||
open architecturally — do not remove it as unused.
|
||||
@@ -0,0 +1,44 @@
|
||||
# ADR 0005 — Oracle Cloud as the launch host, and what it forbids
|
||||
|
||||
**Status:** Accepted, 2026-09-02.
|
||||
|
||||
## The constraint
|
||||
|
||||
**OCI blocks outbound TCP/25 for every tenancy created after 2021-06-23.**
|
||||
Exemption is a service-limit request, routinely refused for free tier. Inbound
|
||||
:25 is *not* blocked.
|
||||
|
||||
| | On OCI |
|
||||
|---|---|
|
||||
| Receive on :25 | ✅ works — `openmail-smtpd` is fine |
|
||||
| Relay out on 587 | ✅ works (verify: see below) |
|
||||
| Direct-to-MX | ❌ **impossible.** `mail-dane` and `mail-mta-sts` can never run there |
|
||||
|
||||
## Decision
|
||||
|
||||
Launch on OCI in **split delivery**: receive directly, relay outbound. Support
|
||||
Oracle Cloud Email Delivery *and* SES as relay providers from day one — two
|
||||
providers at launch forces the provider abstraction to be genuinely
|
||||
data-driven instead of an SES-shaped `if`.
|
||||
|
||||
Direct-to-MX (v0.4) is developed and tested on a host without the block.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Embedded inbound moves from "milestone 4, later" to **required at launch**.
|
||||
- The relay path is not a temporary on-ramp; on our own launch host it is the
|
||||
only outbound path that exists.
|
||||
- Oracle's SPF include is region-scoped (`rp` / `eu.rp` / `ap.rp`
|
||||
`.oracleemaildelivery.com`), so `providers.rs` deliberately stores `None` and
|
||||
makes the operator paste theirs. A guessed include turns the DNS check green
|
||||
against a mechanism the provider does not honour and mail still fails SPF —
|
||||
silently. Same for Resend.
|
||||
|
||||
## ⚠️ Open — verify before committing to the host
|
||||
|
||||
Oracle's docs name only port 25. Mailcow community reports claim **587 and 2525
|
||||
are also blocked outbound on free tier**, which would make OCI unable to relay
|
||||
either, and would change the host choice entirely.
|
||||
|
||||
**Test on a free instance before building on this.** It is a 20-minute check
|
||||
and it invalidates this ADR if the reports are right.
|
||||
Reference in New Issue
Block a user