Add ROADMAP; settle ADR 0005 with a measured OCI port test

cloud-1 (phx) and cloud-2 (us-sanjose-1) are both already OCI aarch64
instances, so the open question in ADR 0005 was answerable directly rather
than by reading forum posts. Measured from cloud-2:

  BLOCKED  25    gmail-smtp-in.l.google.com, aspmx.l.google.com
  OPEN     587/465/2525  gmail, sendgrid, SES, OCI Email Delivery

The community reports claiming 587 and 2525 are also blocked are wrong.
Only 25 is blocked, exactly as Oracle documents, so relay works and the
launch host is viable. Inbound :25 reachability is still unverified — that
is a VCN security-list change, not a platform limit.

ROADMAP.md carries the goal, three success signals that are deliberately
not v0.1, a definition-of-ready checklist, and five standing rules.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkyvfNJGTshJNE9FtwPLk7
This commit is contained in:
Karti Tripathi
2026-09-02 13:38:01 -07:00
co-authored by Claude Opus 5
parent a42b798a0e
commit f0ec4ad691
2 changed files with 144 additions and 6 deletions
+115
View File
@@ -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.
+29 -6
View File
@@ -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.