diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..31cb04b --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,115 @@ +# 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 +- [ ] Repo pushed to `karti-ai/openmail` +- [ ] CI actually running (GitHub-hosted x86 + spark-1 aarch64 runner) +- [ ] `openmail.karti.ai` live +- [ ] `cargo publish --dry-run` clean on the three tier-1 crates + +--- + +## 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. diff --git a/docs/adr/0005-oracle-cloud.md b/docs/adr/0005-oracle-cloud.md index 349ddbc..812135e 100644 --- a/docs/adr/0005-oracle-cloud.md +++ b/docs/adr/0005-oracle-cloud.md @@ -34,11 +34,34 @@ Direct-to-MX (v0.4) is developed and tested on a host without the block. against a mechanism the provider does not honour and mail still fails SPF — silently. Same for Resend. -## ⚠️ Open — verify before committing to the host +## Measured, 2026-09-02 — the block is port 25 only -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. +Community reports claimed 587 and 2525 were also blocked, which would have made +OCI unable to relay and changed the host choice entirely. Tested directly from +our own OCI instance (cloud-2, `us-sanjose-1`, aarch64): -**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. +``` +BLOCKED 25 gmail-smtp-in.l.google.com +BLOCKED 25 aspmx.l.google.com +OPEN 587 smtp.gmail.com +OPEN 465 smtp.gmail.com +OPEN 587 smtp.sendgrid.net +OPEN 2525 smtp.sendgrid.net +OPEN 587 email-smtp.us-east-1.amazonaws.com +OPEN 587 smtp.email.us-sanjose-1.oci.oraclecloud.com +``` + +**The reports are wrong.** Only 25 is blocked, exactly as Oracle documents. +Submission ports are open, including both relay providers we ship. The host is +viable and this ADR stands. + +Note that cloud-1 (`phx`) and cloud-2 (`us-sanjose-1`) are both already OCI +aarch64 instances — the same shape as the launch target, and the same +architecture spark-1 builds for. + +### Still unverified + +**Inbound :25 reachability** has not been tested end-to-end. Oracle blocks +outbound only, but the VCN security list and the instance firewall must still +be opened before mail can arrive. That is a configuration change, not a +platform limit.