Definition-of-ready is met: repo pushed to both remotes, CI green on all three jobs, openmail.karti.ai live, and all three tier-1 crates pass cargo publish --dry-run. Two items left open on purpose and written down rather than dropped: no x86_64 CI job while GitHub-hosted runners are unavailable for billing, and the repo stays private until v0.1 closes the ingest loop per ADR 0004. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JkyvfNJGTshJNE9FtwPLk7
147 lines
7.3 KiB
Markdown
147 lines
7.3 KiB
Markdown
# OpenMail — goal and roadmap
|
|
|
|
## The goal
|
|
|
|
**Be the mail server agents run on.** Self-hosted, Apache-2.0, agent-native —
|
|
the option an enterprise can point MX at without trusting us, and a builder can
|
|
build a commercial product on without asking us.
|
|
|
|
Not a hosted product. Not open-core. See
|
|
[`docs/adr/0006-self-hosted-first.md`](docs/adr/0006-self-hosted-first.md).
|
|
|
|
### How we'll know it worked
|
|
|
|
| | Signal |
|
|
|---|---|
|
|
| **Technically** | An agent creates its own inbox, receives real mail from Gmail, and replies — with the reply landing in the inbox, not spam — with no human in the loop. |
|
|
| **In the ecosystem** | `mail-dane` and `mail-mta-sts` are depended on by a Rust project that is not ours. |
|
|
| **Commercially** | One business we did not talk into it is running OpenMail on their own infrastructure, on their own domain. |
|
|
|
|
None of those are v0.1. They are what v0.1 is aimed at.
|
|
|
|
---
|
|
|
|
## Definition of ready: what "ready to build" means
|
|
|
|
The scaffold is done. This is the checklist that says the *foundation* is
|
|
finished and feature work can start without stopping to fix plumbing.
|
|
|
|
- [x] Apache-2.0, `NOTICE`, licence policy enforced in CI (`cargo-deny`)
|
|
- [x] 12-crate workspace; `check`/`test`/`clippy`/`fmt` green on x86_64 and aarch64
|
|
- [x] `unsafe_code = "forbid"` workspace-wide
|
|
- [x] Postgres schema as an embedded migration
|
|
- [x] Six decisions recorded as ADRs, each with its cost stated
|
|
- [x] Multi-stage Dockerfile, non-root, no shell in the runtime image
|
|
- [x] **OCI outbound port block measured, not assumed** — 25 blocked, 587/465/2525 open
|
|
- [x] Repo pushed to `karti-ai/openmail` (and mirrored to Gitea `OSS/openmail`)
|
|
- [x] CI green — all three jobs on the spark-1 aarch64 runner
|
|
- [x] `openmail.karti.ai` live
|
|
- [x] `cargo publish --dry-run` clean on all three tier-1 crates
|
|
|
|
**The scaffold is done. Feature work can start.**
|
|
|
|
### Two things deliberately left open
|
|
|
|
- **GitHub-hosted runners are unavailable on the account** — *"recent account
|
|
payments have failed or your spending limit needs to be increased."* Every
|
|
job runs on spark-1 instead, which is also the deployment architecture, so
|
|
this is not purely a workaround. An **x86_64 job should be added back** once
|
|
hosted runners work (or once the repo is public, which makes them free), to
|
|
catch width- and endianness-dependent bugs that aarch64-only CI cannot.
|
|
- **The repo is still private.** ADR 0004 argues for going public once v0.1
|
|
closes the ingest loop, so the first impression is a working thing rather
|
|
than a scaffold.
|
|
|
|
---
|
|
|
|
## v0.1 — the agent layer, provable without mail
|
|
|
|
**Thesis to prove:** the agent-native model is worth having, independent of
|
|
whether we can move mail yet. Everything here is testable in CI with no mail
|
|
infrastructure at all.
|
|
|
|
- [ ] `openmail-core::extract::strip_quoted` — the first real algorithm. Prefer
|
|
under-stripping to over-stripping; losing new content is unrecoverable,
|
|
keeping a few quoted lines only costs tokens.
|
|
- [ ] `openmail-core::thread` — both bases (`Headers`, `SubjectHeuristic`) with
|
|
the time window that bounds the wrong answer
|
|
- [ ] `openmail-store` — inbox/message/thread queries, S3 blob client
|
|
- [ ] `openmail-api` — the v0 handlers behind bearer auth
|
|
- [ ] **`POST /v0/inboxes/{id}/ingest`** — the loop-closer. Feed it a raw
|
|
`.eml`, get back a threaded, extracted message. This is the milestone.
|
|
- [ ] A corpus of ugly real-world `.eml` fixtures. Not synthetic — Outlook,
|
|
Gmail, mobile clients, and at least one message that is legally MIME and
|
|
practically deranged.
|
|
|
|
**Done when:** `docker compose up`, POST a real message, get correct threading
|
|
and clean `extracted_text` back.
|
|
|
|
## v0.2 — receive
|
|
|
|
- [ ] `openmail-smtpd` on :25 with `Limits` enforced and a timeout on every state
|
|
- [ ] `openmail-guard` — iprev, DNSBL, rate limit, in that order (never spend a
|
|
DNS round trip on a connection a counter can reject)
|
|
- [ ] `mail-auth` SPF/DKIM/DMARC verdicts recorded per message
|
|
- [ ] `openmail-junk` per-inbox scoring
|
|
- [ ] `message.received` webhooks + WebSocket
|
|
- [ ] Real MX for a test domain; open OCI inbound :25 and prove mail arrives
|
|
|
|
## v0.3 — send, and the surface agents actually use
|
|
|
|
- [ ] Relay send via **both** SES and Oracle from the start — two providers is
|
|
what forces the abstraction to stay data-driven
|
|
- [ ] Local DKIM signing
|
|
- [ ] `mail-dsn` bounce handling wired to `outbox` state
|
|
- [ ] `openmail-mcp` served — an agent creates an inbox, receives, and replies,
|
|
with no human involved
|
|
|
|
## v0.4 — direct-to-MX, and the crates ship
|
|
|
|
- [ ] `mail-dane` complete — DNSSEC-validated TLSA, and the property that an
|
|
unauthenticated set can never return `Match`
|
|
- [ ] `mail-mta-sts` complete — policy cache where a fetch failure never evicts
|
|
a valid policy
|
|
- [ ] **Publish both to crates.io.** First permissive implementations in Rust.
|
|
- [ ] Direct MX delivery with both enforced. Not on OCI — that host blocks :25.
|
|
- [ ] IP warmup, FBL enrolment, suppression lists
|
|
|
|
## v1.0
|
|
|
|
- [ ] IMAP front-end so a human uses their own client against the same mailbox
|
|
- [ ] Multi-tenancy beyond `pods`
|
|
- [ ] A deliverability track record worth publishing
|
|
|
|
---
|
|
|
|
## Standing rules
|
|
|
|
1. **Never copy from an AGPL/GPL tree.** Stalwart's server, maddy, BillionMail
|
|
are read-only reference. Work from RFCs. ADR 0003.
|
|
2. **Silent failures are the highest-severity class.** A DANE downgrade or a
|
|
mis-cached MTA-STS policy does not error — it quietly removes a security
|
|
property or defers mail nobody sees. Every outcome in those crates is an
|
|
explicit enum with no `Default` and no `bool`.
|
|
3. **Measure, don't assume.** The OCI port block was a documented "fact" that
|
|
community reports contradicted; ten minutes of testing settled it. Do that
|
|
every time.
|
|
4. **`pods` is not dead code.** It is the only thing keeping a future hosted
|
|
offering migration-free.
|
|
5. **Zero clippy warnings is the gate**, not an aspiration.
|
|
|
|
---
|
|
|
|
## Traps already paid for
|
|
|
|
Recorded because each cost real time, and the next person (or the next
|
|
session) should not pay again.
|
|
|
|
| Trap | What happens | Rule |
|
|
|---|---|---|
|
|
| `Swatinem/rust-cache` on a **self-hosted** runner | Its post-step prunes `~/.cargo`, deleting the real toolchain. The first run passes, then every later run fails `exit 127`. | Never use it on self-hosted. Toolchain lives at `/opt/rust`. |
|
|
| Runner `.path` / `.env` files | Were not applied by this runner. | Set `PATH`, `RUSTUP_HOME`, `CARGO_HOME` in the workflow `env:` block. |
|
|
| Symlinking `cargo-clippy` into `/usr/local/bin` | Breaks clippy's sysroot resolution — `cargo clippy` silently behaves like `rustc` and rejects `--workspace`. | Use a full rustup install and put its `bin` on PATH; do not symlink individual toolchain binaries. |
|
|
| `mail-auth`'s `generate` feature | Pulls in `rsa`, carrying RUSTSEC-2023-0071 (Marvin Attack, no fix available). | Don't enable it. Generate DKIM keys with `openssl genpkey`, out of process. |
|
|
| cloud-2 Caddy without `bind 10.0.0.2` | Serves a valid certificate and an empty 200 — looks deployed, returns nothing. | Always include the bind; verify with `curl`, never by eye. |
|
|
| GitHub push with a real email | `remote rejected … email privacy restrictions`. | Repo is configured with the `users.noreply.github.com` address. |
|
|
| `git.karti.ai:2223` from amd-server | Times out; Gitea SSH is tailnet-only. | Use the `gitea` SSH alias (`gitea:OSS/openmail.git`). |
|