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:
Karti Tripathi
2026-09-02 13:08:05 -07:00
co-authored by Claude Opus 5
parent 428040d964
commit 36b15ddcaf
59 changed files with 5540 additions and 1762 deletions
+68
View File
@@ -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.
+46
View File
@@ -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.
+34
View File
@@ -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.
+51
View File
@@ -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.
+44
View File
@@ -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.
+273
View File
@@ -0,0 +1,273 @@
# OpenMail — Architecture
Agent-native, self-hosted mail server. **One Go binary** that embeds [Mox](https://github.com/mjl-/mox)'s
(MIT) mail internals for the correctness-critical 95% — DKIM, SPF/DMARC, DANE + MTA-STS delivery,
real-world MIME, spam filtering — and owns a **native, agent-shaped data model** (Postgres + object
store) with a REST API and MCP server on top. Mail I/O sits behind a pluggable **`MailBackend`**
interface so OpenMail is useful on day one against a relay or an existing mailbox, while embedded-Mox
self-host is the flagship, in-process path. This document is the design of record. It favors an MVP
that actually receives and sends real mail over a perfect feature set.
> **Scope:** OpenMail is a **self-hostable app, not a SaaS** — no billing/payments, no Stripe, no
> hosted control plane. Everything runs on the operator's hardware.
## 0. The decision: embed Mox, don't rebuild and don't just wrap it
An agent-mailbox API is ~5% of the work. The other 95% is mail infrastructure (inbound :25 hardening,
outbound deliverability, MIME, threading, quote-stripping, anti-abuse). Three ways to get that 95%:
1. **Build from scratch** — re-implement DKIM/SPF/DMARC/DANE/MTA-STS/MIME ourselves. Correct but slow,
and we'd do it worse than a production server at first.
2. **Sidecar an existing server** (run Mox as a separate process, read it over IMAP). Fast, but we
don't own the data model — threads/labels/`extracted_text` would be re-derived from IMAP, and we
ship two coupled processes.
3. **Embed Mox's packages** into our own binary and own the storage + API. ← **chosen.**
Why (3) is viable: **Mox has no `internal/` directory** — every package is importable — and the
README states *"most non-server Go packages are written to be reusable."* The real boundary is not
visibility, it's **storage coupling**: Mox's *server + store* packages are welded to its embedded
`bstore`/`bbolt` store and global config; its *protocol/crypto/parsing* packages are stateless and
clean. We embed the latter and replace the former with our own Postgres-backed server.
### What we embed from Mox vs. what we own
| Concern | Source | Notes |
|---|---|---|
| MIME parsing | **mox `message`** | Battle-tested on real, broken senders. Better than enmime for our needs. |
| DKIM sign + verify | **mox `dkim`** | |
| SPF / DMARC | **mox `spf`, `dmarc`** | Inbound auth verification + policy. |
| Secure outbound delivery | **mox `smtpclient`, `dane`, `mtasts`, `dns`** | The deliverability moat — DANE/MTA-STS MX delivery, mostly solved by reusing this. |
| Bounces (DSN) | **mox `dsn`** | Parse/generate delivery status notifications. |
| Anti-abuse | **mox `iprev`, `dnsbl`, `ratelimit`** | rDNS check, blocklists, rate limiting on :25. |
| Spam filtering | **mox `junk`** | Built-in Bayesian filter; per-inbox train/score. |
| SMTP auth | **mox `sasl`, `scram`** | For the submission (send) path. |
| — | — | — |
| Inbound SMTP **server loop** | **`emersion/go-smtp`** | Clean, embeddable listener/session. We do NOT import mox `smtpserver` (store-coupled). |
| Storage | **OpenMail (Postgres + S3)** | Native agent data model. NOT mox `store`/bbolt. |
| API, MCP, threading, extraction, webhooks, auth, multi-tenancy | **OpenMail** | The agent-native layer — our differentiation. |
> **Boundary rule:** never import `smtpserver`, `imapserver`, `queue`, `store`, or `mox-` (config) —
> they assume Mox's storage/config. Importing them is the line between "embed as a library" and
> "fork Mox." We stay on the library side.
## 0.1 Competitive positioning (why OpenMail exists)
The agent-mailbox concept is already taken; the *implementation niche* is open.
| Project | Lang | License | How mail runs | Gap OpenMail fills |
|---|---|---|---|---|
| **agenticmail** (closest competitor) | TypeScript | MIT | TS orchestrator + **Stalwart in a Docker sidecar**, SQLite | Multi-container, AGPL mail engine, SQLite — vs. **single Go binary, all-MIT, in-process, Postgres** |
| cloudflare/agentic-inbox | TS | Apache-2 | **Locked to Cloudflare Workers** + Email Routing | Vendor-neutral, runs anywhere |
| Mox / Maddy / Stalwart | Go/Rust | MIT/GPL/AGPL | Full mail servers | Not agent-native (no agent API/MCP, no threads/extract) |
OpenMail's three differentiators, all downstream of the Go + embed-Mox choice:
1. **Single static binary** (vs. sidecar/multi-container).
2. **Fully MIT stack** — Mox is MIT, vs. agenticmail's AGPL Stalwart — so others can build commercial agents on top.
3. **In-process mail engine** — nobody else does this; it's only possible because Mox is importable Go.
## 0.2 The `MailBackend` interface (pluggable mail I/O)
All inbound delivery and outbound sending sit behind one interface, so the agent layer (API, MCP,
store, threading) never depends on *how* mail moves. Three backends, shipped in order of effort:
```go
// MailBackend abstracts where mail comes from and how it leaves. The core never
// knows which implementation is active.
type MailBackend interface {
// Send dispatches an already-built, DKIM-signable RFC 5322 message.
Send(ctx context.Context, msg *OutgoingMessage) (SendResult, error)
// Start begins delivering inbound messages to the sink until ctx is cancelled.
// (relay/imap: poll or webhook; embedded: go-smtp on :25.)
Start(ctx context.Context, sink InboundSink) error
Capabilities() Caps // self-host? inbound-push? custom-domain? throwaway-addrs?
}
```
| Backend | Inbound | Outbound | Ops burden | Ships |
|---|---|---|---|---|
| **relay** | provider webhook / poll | SES / Postmark / Resend API (mox `dkim` sign) | lowest | **v1 default** |
| **imap_smtp** | IMAP IDLE on a BYO mailbox | SMTP submission to BYO server | low | v1 |
| **embedded** (flagship) | `go-smtp` :25 + mox verify/parse/junk | mox `smtpclient` + `dane` + `mtasts` (MX) | highest | milestone 5 |
This makes the deliverability slog *opt-in*: a user gets a working agent mailbox immediately via
relay or their existing mailbox, and graduates to fully self-hosted SMTP only when they want to own
the whole stack. The `embedded` backend is the differentiator; the other two are the on-ramp.
## 1. Component overview
```
┌──────────────────────────────────────────────────────┐
Internet senders ───▶ │ smtpd (go-smtp on :25) │
(MX → this VPS) │ iprev/dnsbl/ratelimit → SPF/DKIM/DMARC verify │
│ → mox/message parse → mox/junk score → enqueue │ ← mox pkgs
└───────────────┬──────────────────────────────────────┘
│ internal queue (Postgres LISTEN/NOTIFY)
┌───────────────▼──────────────────────────────────────┐
│ core (Go) │
Agents / SDKs ──REST──▶ • inbox/message/thread/draft services │
MCP clients ───MCP──▶ │ • threading, extracted_text, labels, FTS search │
│ • auth (API keys, pods), webhooks/WS │
└──────┬───────────────────────────────┬────────────────┘
│ │
┌────────────▼─────┐ ┌─────────────▼──────────────┐
│ Postgres │ │ Object store (S3/MinIO) │
│ metadata + FTS │ │ raw .eml + attachments │
└──────────────────┘ └────────────────────────────┘
┌───────────────┴──────────────────────────────────────┐
Outbound to world ◀── │ sender (mox/dkim sign → backend) │
│ self-host: mox/smtpclient + dane + mtasts (MX) │ ← mox pkgs
│ relay: SES / Postmark / Resend API │
└──────────────────────────────────────────────────────┘
```
All roles (`smtpd`, `core` API, `sender`, `mcp`) ship in one binary with subcommands; deploy together
or split later. Caddy terminates TLS for the HTTP API and auto-manages certs.
## 2. Tech stack
- **Language:** Go — single static binary, and the whole mature self-hosted-mail ecosystem (Mox,
go-smtp, go-message) is Go, keeping us MIT-compatible. Rust (Stalwart) is AGPL → ruled out.
- **HTTP:** `chi` router; `sqlc` + `pgx` for typed Postgres access.
- **Inbound server:** `emersion/go-smtp` listener → handed to mox packages for verify/parse/score.
- **Mail correctness/delivery:** mox `message`, `dkim`, `spf`, `dmarc`, `dane`, `mtasts`, `dns`,
`smtpclient`, `dsn`, `junk`, `iprev`, `dnsbl`, `ratelimit`, `sasl`, `scram` (see §0 table).
- **Quote stripping:** a `talon`/`EmailReplyParser`-style heuristic pass for `extracted_text`.
- **Storage:** Postgres (metadata + `tsvector` FTS); S3-compatible object store (MinIO self-host, or
S3) for raw `.eml` and attachment blobs.
- **Events:** Postgres `LISTEN/NOTIFY` for the internal inbound queue and to fan out webhooks + a
WebSocket stream. (Swap to NATS only if volume warrants.)
- **Deploy:** `docker compose``caddy`, `openmail` (one image, multiple commands), `postgres`,
`minio`. MX + SPF/DKIM/DMARC/DANE/MTA-STS DNS documented in `deploy/`.
## 3. Data model (Postgres)
Native and agent-shaped (not Mox's per-account bbolt index). Resource shapes mirror what agent tooling
expects so SDKs/MCP map cleanly.
```sql
pods (id, name, created_at) -- tenant isolation
inboxes (id, pod_id, address UNIQUE, display_name, dkim_selector, metadata jsonb, created_at, updated_at)
threads (id, inbox_id, subject, last_message_id, message_count, labels text[], updated_at, created_at)
messages (id, inbox_id, thread_id, message_id_hdr, in_reply_to, references text[],
from_addr, to_addrs text[], cc text[], bcc text[], subject,
preview, text, html, extracted_text, extracted_html,
raw_object_key, -- pointer to raw .eml in object store
spf, dkim, dmarc, -- inbound auth verdicts (from mox pkgs)
junk_score, labels text[], size_bytes, headers jsonb, ts tsvector, -- FTS
created_at, updated_at)
attachments (id, message_id, filename, content_type, size_bytes, object_key, inline bool, content_id)
drafts (id, inbox_id, thread_id, to_addrs text[], cc, bcc, subject, text, html, send_at, client_id)
api_keys (id, pod_id, hash, scopes text[], created_at) -- bearer tokens; store only the hash
webhooks (id, pod_id, url, event_types text[], secret) -- HMAC-signed deliveries
outbox (id, message_id, status, attempts, last_error, next_attempt_at) -- send queue + retries
events (id, inbox_id, type, payload jsonb, created_at) -- audit + WS replay
domains (id, pod_id, name, dkim_privkey_ref, verified bool) -- per-domain DKIM keys + DNS state
```
Indexes: `inboxes(address)`, `messages(thread_id)`, `messages(inbox_id, created_at desc)`,
GIN on `messages.ts` (search) and `*.labels`. Raw MIME + attachments live in object storage; rows
hold pointers.
## 4. Request / data flows
### Inbound (receive)
1. `go-smtp` accepts on :25; anti-abuse gate via mox `iprev`/`dnsbl`/`ratelimit` + size cap; raw `.eml`
written to object store.
2. mox `spf`/`dkim`/`dmarc` verify the message; verdicts recorded. mox `message` builds the MIME tree
`text`/`html`, attachments, headers. mox `junk` scores spam. Quote-stripper computes
`extracted_text` + `preview`.
3. Threading resolves `thread_id` from `In-Reply-To`/`References`, falling back to normalized-subject
+ participants within a window.
4. Row inserted; `LISTEN/NOTIFY` fires → `message.received` webhook + WebSocket event.
### Outbound (send / reply)
1. API builds the RFC 5322 message (sets `In-Reply-To`/`References` on replies); persists a `messages`
row + `outbox` entry.
2. `sender` mox-`dkim`-signs, then dispatches via the configured backend:
- **self-host:** mox `smtpclient` + `dane` + `mtasts` for authenticated, secure MX delivery.
- **relay:** SES/Postmark/Resend API.
3. Bounces/complaints (mox `dsn` parse, or relay webhook) update `outbox.status` and emit events.
### API surface (v1 — AgentMail-shaped)
```
POST /v0/inboxes create inbox
GET /v0/inboxes / /v0/inboxes/{id} list / get
POST /v0/inboxes/{id}/messages/send send
GET /v0/inboxes/{id}/messages list (limit, page_token, labels)
GET /v0/inboxes/{id}/messages/{id} get
POST /v0/inboxes/{id}/messages/{id}/reply
GET /v0/inboxes/{id}/threads / /{id} list / get
…drafts, webhooks, search…
```
Bearer auth (`Authorization: Bearer`). Keeping paths/shapes close to AgentMail's public v0 lets an
existing client (or MCP server) target a self-hosted OpenMail by base-URL swap. Compatibility yields
to a cleaner native shape where they conflict.
### MCP server
A thin MCP front-end over `core`: tools `create_inbox`, `list_messages`, `get_thread`, `send_message`,
`reply`, `search` — so an agent drives its own mailbox directly.
## 5. Deliverability plan (the moat — and why embedding Mox helps most here)
- **Self-host send is achievable from day one for the protocol parts** because mox `smtpclient` +
`dane` + `mtasts` + `dkim` already implement authenticated, DANE/MTA-STS-secured MX delivery. What
remains is *operational*, not code: dedicated IP with PTR/rDNS, SPF (`v=spf1 ip4:… -all`), published
DKIM key, DMARC (`p=quarantine``p=reject`), gradual IP warmup, FBL enrollment, suppression lists.
- **Relay backend** (SES/Postmark/Resend) ships alongside for instant inbox placement while a
self-host IP warms up — chosen per-domain.
- **Inbound auth:** SPF/DKIM/DMARC verified on receipt (mox pkgs); verdicts exposed in message
metadata; optional reject/quarantine policy per inbox.
## 6. MVP milestones
Backend-first ordering: get an end-to-end loop on the *lowest-ops* backend, then add the differentiator.
1. **Core API + storage** — inboxes/messages/threads/drafts CRUD on Postgres + MinIO; bearer auth;
ingest endpoint to seed without real mail. *Proves the data model + API.* `MailBackend` interface
defined; a `null`/ingest backend satisfies it.
2. **Relay backend (loop closed)**`imap_smtp` and/or `relay` backend: inbound via IMAP IDLE or
provider webhook → mox `message` parse + threading + `message.received`; outbound via SMTP
submission / relay API, mox-DKIM-signed. *Proves receive+reply against real mail with near-zero ops.*
3. **MCP server** — agent owns and operates an inbox end-to-end (tools over the core).
4. **Embedded inbound**`go-smtp` on :25 + mox verify/parse/junk + anti-abuse; MX a test domain.
*Proves OpenMail can receive directly.*
5. **Embedded outbound + deliverability** — mox `smtpclient`/`dane`/`mtasts`; the flagship self-host
path and the IP-warmup/reputation long tail.
## 7. Repo layout (planned)
```
cmd/openmail/ # single binary: `serve`, `smtpd`, `sender`, `mcp` subcommands
internal/core/ # inbox/message/thread/draft services
internal/mail/ # MailBackend interface + shared parse/thread/DKIM helpers (mox pkgs)
internal/mail/relay/ # relay backend (SES/Postmark/Resend)
internal/mail/imapsmtp/ # imap_smtp backend (BYO mailbox: IMAP IDLE in, SMTP submit out)
internal/mail/embedded/ # embedded backend (go-smtp :25 in; mox smtpclient+dane+mtasts out)
internal/store/ # pgx/sqlc queries, object-store client, embedded migrations
internal/store/migrations/ # SQL migrations (go:embed'd into the binary)
internal/api/ # chi HTTP handlers (v0 surface)
internal/mcp/ # MCP server over core
deploy/ # docker-compose.yml, Caddyfile, DNS (MX/SPF/DKIM/DMARC/DANE/MTA-STS) notes
spike/ # throwaway feasibility spikes (mimecheck: mox standalone parse — PASSED)
```
## 8. Licensing & attribution
OpenMail is **MIT** (see `LICENSE`). Every dependency is permissive: Mox (MIT), `emersion/go-smtp`
and `go-message` (MIT), `pgx`/`chi` (MIT/BSD). We import Mox as a Go module (no vendored source in
this repo), but credit it prominently in the README; if we ever vendor Mox source we must retain its
MIT notice. **No GPL/AGPL** code anywhere — Maddy (GPL-3.0) and Stalwart (AGPL) are deliberately
avoided so the project stays MIT.
## 9. Open questions
- **mox `message` ⇄ go-smtp seam:** confirm mox's parser consumes a raw reader cleanly outside mox's
store (spike in milestone 2). If any Tier-1 package drags in `mox-`/`store` transitively, isolate
behind a thin adapter or vendor just that file.
- How far to chase AgentMail API compatibility vs. a cleaner native shape.
- Search: Postgres FTS is enough for v1; revisit (Meilisearch/Typesense) only if needed.
- Multi-tenancy depth — `pods` now, full RBAC later.
- Whether to expose an optional IMAP read path (via mox `imapserver` over our store would mean forking
it — more likely a thin standalone IMAP front-end later, kept out of v1).
```