Architecture v2: embed Mox (MIT) as a library; native Postgres agent layer

- Choose Option B: single binary embedding Mox's stateless protocol/crypto/
  delivery packages (dkim, spf, dmarc, dane, mtasts, message, smtpclient, junk,
  dsn, anti-abuse) while owning server loop (go-smtp) + Postgres data model.
- Boundary rule: never import mox smtpserver/imapserver/queue/store/config
  (bbolt/global-config coupled) — that's the line between embed and fork.
- Add MIT LICENSE; reframe README as a real MIT OSS product.
- Deliverability moat leans on mox smtpclient+dane+mtasts; relay fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
karti-ai
2026-06-21 12:07:37 -07:00
parent 9812e25973
commit aab2a8558e
3 changed files with 195 additions and 117 deletions
+132 -86
View File
@@ -1,112 +1,145 @@
# OpenMail — Architecture # OpenMail — Architecture
Self-hosted, AI-native mailbox for agents. One Go service, Postgres, and object storage on a VPS, Agent-native, self-hosted mail server. **One Go binary** that embeds [Mox](https://github.com/mjl-/mox)'s
fronted by Caddy for TLS. This document is the design of record; it favors an MVP that actually (MIT) mail internals for the correctness-critical 95% — DKIM, SPF/DMARC, DANE + MTA-STS delivery,
receives and sends real mail over a perfect feature set. 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. This document is the design of record. It favors an
MVP that actually receives and sends real mail over a perfect feature set.
## 0. The honest framing ## 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**: 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%:
| Hard part | Why it's hard | 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.
| **Inbound** | You must run an SMTP server on port 25 with MX records pointing at it, accept from arbitrary senders, survive spam/abuse, and parse messy real-world MIME. | 2. **Sidecar an existing server** (run Mox as a separate process, read it over IMAP). Fast, but we
| **Outbound deliverability** | Anyone can send SMTP; getting it to the *inbox* needs SPF + DKIM signing + DMARC alignment, PTR/rDNS, warmed IP reputation, feedback loops (FBLs), and bounce/complaint handling. This is the real moat. | don't own the data model — threads/labels/`extracted_text` would be re-derived from IMAP, and we
| **MIME parsing** | Multipart trees, encodings, inline vs attachment, calendar parts, broken senders. | ship two coupled processes.
| **Threading** | Stitching messages into conversations from `Message-ID` / `In-Reply-To` / `References` plus subject heuristics. | 3. **Embed Mox's packages** into our own binary and own the storage + API. ← **chosen.**
| **Quoted-history stripping** | The `extracted_text` feature (reply content without the quoted thread) is its own parsing problem. |
OpenMail is structured so each of these is an isolated, swappable component, and so v1 can lean on a Why (3) is viable: **Mox has no `internal/` directory** — every package is importable — and the
relay for deliverability while the self-hosted SMTP path matures. 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.
## 1. Component overview ## 1. Component overview
``` ```
┌──────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────
Internet senders ───▶ │ smtpd (inbound, port 25, go-smtp) Internet senders ───▶ │ smtpd (go-smtp on :25)
(MX → this VPS) │ → parse MIME → normalize → enqueue (MX → this VPS) │ iprev/dnsbl/ratelimit → SPF/DKIM/DMARC verify
└───────────────┬──────────────────────────────┘ │ → mox/message parse → mox/junk score → enqueue │ ← mox pkgs
│ internal queue (Postgres LISTEN/NOTIFY or NATS) └───────────────┬──────────────────────────────────────┘
┌───────────────▼──────────────────────────────┐ │ internal queue (Postgres LISTEN/NOTIFY)
┌───────────────▼──────────────────────────────────────┐
│ core (Go) │ │ core (Go) │
Agents / SDKs ──REST──▶ • inbox/message/thread/draft services │ Agents / SDKs ──REST──▶ • inbox/message/thread/draft services │
MCP clients ───MCP──▶ │ • threading, extraction, labels, search │ MCP clients ───MCP──▶ │ • threading, extracted_text, labels, FTS search │
│ • auth (API keys, pods), webhooks/WS │ │ • auth (API keys, pods), webhooks/WS │
└──────┬──────────────────────────────────────┘ └──────┬───────────────────────────────┬────────────────┘
│ │ │ │
┌────────────▼─────┐ ───────────▼────────────── ┌────────────▼─────┐ ┌─────────────▼──────────────┐
│ Postgres │ │ Object store (S3/MinIO) │ │ Postgres │ │ Object store (S3/MinIO) │
│ metadata + FTS │ │ raw MIME + attachments │ │ metadata + FTS │ │ raw .eml + attachments │
└──────────────────┘ └────────────────────────────┘ └──────────────────┘ └────────────────────────────┘
┌───────────────┴──────────────────────────────┐ ┌───────────────┴──────────────────────────────────────
Outbound to world ◀── │ sender (DKIM-sign → self-host SMTP OR relay) Outbound to world ◀── │ sender (mox/dkim sign → backend)
└──────────────────────────────────────────────┘ │ self-host: mox/smtpclient + dane + mtasts (MX) │ ← mox pkgs
│ relay: SES / Postmark / Resend API │
└──────────────────────────────────────────────────────┘
``` ```
All four roles (`smtpd`, `core` API, `sender`, MCP) ship in one Go binary with subcommands; deploy All roles (`smtpd`, `core` API, `sender`, `mcp`) ship in one binary with subcommands; deploy together
together or split later. Caddy terminates TLS for the HTTP API and auto-manages certs. or split later. Caddy terminates TLS for the HTTP API and auto-manages certs.
## 2. Tech stack ## 2. Tech stack
- **Language:** Go (single static binary; matches the deployment-simplicity goal and OpenMail's - **Language:** Go single static binary, and the whole mature self-hosted-mail ecosystem (Mox,
systems nature). 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. - **HTTP:** `chi` router; `sqlc` + `pgx` for typed Postgres access.
- **Inbound SMTP:** `emersion/go-smtp` (server) — listens on :25, hands raw bytes to the parser. - **Inbound server:** `emersion/go-smtp` listener → handed to mox packages for verify/parse/score.
- **MIME:** `emersion/go-message` (+ `enmime` for lenient real-world parsing); a quote-stripper - **Mail correctness/delivery:** mox `message`, `dkim`, `spf`, `dmarc`, `dane`, `mtasts`, `dns`,
(port of `talon`/`EmailReplyParser` heuristics) for `extracted_text`/`extracted_html`. `smtpclient`, `dsn`, `junk`, `iprev`, `dnsbl`, `ratelimit`, `sasl`, `scram` (see §0 table).
- **DKIM:** `emersion/go-msgauth/dkim` for signing (outbound) and verifying (inbound). - **Quote stripping:** a `talon`/`EmailReplyParser`-style heuristic pass for `extracted_text`.
- **Outbound:** pluggable `Sender` interface — `smtp` (self-host, MX delivery) or `relay` - **Storage:** Postgres (metadata + `tsvector` FTS); S3-compatible object store (MinIO self-host, or
(SES / Postmark / Resend API). Default to relay in v1 for deliverability; self-host is opt-in. S3) for raw `.eml` and attachment blobs.
- **Storage:** Postgres (metadata + full-text search via `tsvector`); S3-compatible object store - **Events:** Postgres `LISTEN/NOTIFY` for the internal inbound queue and to fan out webhooks + a
(MinIO self-hosted, or AWS S3) for raw `.eml` and attachment blobs. WebSocket stream. (Swap to NATS only if volume warrants.)
- **Events:** Postgres `LISTEN/NOTIFY` for the internal inbound queue and for fanning out
webhooks + a WebSocket stream. (Swap to NATS if volume warrants.)
- **Deploy:** `docker compose``caddy`, `openmail` (one image, multiple commands), `postgres`, - **Deploy:** `docker compose``caddy`, `openmail` (one image, multiple commands), `postgres`,
`minio`. MX + SPF/DKIM/DMARC DNS documented in `deploy/`. `minio`. MX + SPF/DKIM/DMARC/DANE/MTA-STS DNS documented in `deploy/`.
## 3. Data model (Postgres) ## 3. Data model (Postgres)
Mirrors the resource shapes agent tooling already expects, so SDKs/MCP map cleanly. Native and agent-shaped (not Mox's per-account bbolt index). Resource shapes mirror what agent tooling
expects so SDKs/MCP map cleanly.
```sql ```sql
pods (id, name, created_at) -- tenant isolation pods (id, name, created_at) -- tenant isolation
inboxes (id, pod_id, address UNIQUE, display_name, client_id, metadata jsonb, created_at, updated_at) 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) 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[], messages (id, inbox_id, thread_id, message_id_hdr, in_reply_to, references text[],
from_addr, to_addrs text[], cc text[], bcc text[], subject, from_addr, to_addrs text[], cc text[], bcc text[], subject,
preview, text, html, extracted_text, extracted_html, preview, text, html, extracted_text, extracted_html,
raw_object_key, -- pointer to raw .eml in object store raw_object_key, -- pointer to raw .eml in object store
labels text[], size_bytes, headers jsonb, ts tsvector, -- FTS spf, dkim, dmarc, -- inbound auth verdicts (from mox pkgs)
junk_score, labels text[], size_bytes, headers jsonb, ts tsvector, -- FTS
created_at, updated_at) created_at, updated_at)
attachments (id, message_id, filename, content_type, size_bytes, object_key, inline bool, content_id) 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) 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 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 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 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 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)`, Indexes: `inboxes(address)`, `messages(thread_id)`, `messages(inbox_id, created_at desc)`,
GIN on `messages.ts` (search) and `*.labels`. Raw MIME and attachments live in object storage, not GIN on `messages.ts` (search) and `*.labels`. Raw MIME + attachments live in object storage; rows
the DB — rows hold pointers. hold pointers.
## 4. Request / data flows ## 4. Request / data flows
### Inbound (receive) ### Inbound (receive)
1. `smtpd` accepts on :25, enforces basic anti-abuse (rate limit, SPF check, size cap), and writes 1. `go-smtp` accepts on :25; anti-abuse gate via mox `iprev`/`dnsbl`/`ratelimit` + size cap; raw `.eml`
the raw `.eml` to the object store. written to object store.
2. Parser builds the MIME tree → extracts `text`/`html`, attachments, headers; computes 2. mox `spf`/`dkim`/`dmarc` verify the message; verdicts recorded. mox `message` builds the MIME tree
`extracted_text` (strip quoted history) and a `preview`. `text`/`html`, attachments, headers. mox `junk` scores spam. Quote-stripper computes
3. Threading resolves `thread_id` from `In-Reply-To`/`References` → fallback to normalized-subject + `extracted_text` + `preview`.
participants within a window. 3. Threading resolves `thread_id` from `In-Reply-To`/`References`, falling back to normalized-subject
4. Row inserted; `LISTEN/NOTIFY` fires → webhook + WebSocket `message.received` event. + participants within a window.
4. Row inserted; `LISTEN/NOTIFY` fires → `message.received` webhook + WebSocket event.
### Outbound (send / reply) ### Outbound (send / reply)
1. API builds the RFC 5322 message (sets `In-Reply-To`/`References` for replies), persists a 1. API builds the RFC 5322 message (sets `In-Reply-To`/`References` on replies); persists a `messages`
`messages` row + `outbox` entry. row + `outbox` entry.
2. `sender` DKIM-signs and dispatches via the configured backend (self-host SMTP or relay API). 2. `sender` mox-`dkim`-signs, then dispatches via the configured backend:
3. Bounces/complaints (SMTP DSN, or relay webhook) update `outbox.status` and emit events. - **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) ### API surface (v1 — AgentMail-shaped)
``` ```
@@ -116,56 +149,69 @@ POST /v0/inboxes/{id}/messages/send send
GET /v0/inboxes/{id}/messages list (limit, page_token, labels) GET /v0/inboxes/{id}/messages list (limit, page_token, labels)
GET /v0/inboxes/{id}/messages/{id} get GET /v0/inboxes/{id}/messages/{id} get
POST /v0/inboxes/{id}/messages/{id}/reply POST /v0/inboxes/{id}/messages/{id}/reply
GET /v0/inboxes/{id}/threads /{id} list / get GET /v0/inboxes/{id}/threads / /{id} list / get
…drafts, webhooks, search… …drafts, webhooks, search…
``` ```
Bearer auth (`Authorization: Bearer`). Keeping paths/shapes close to AgentMail's public v0 means an Bearer auth (`Authorization: Bearer`). Keeping paths/shapes close to AgentMail's public v0 lets an
existing Go/Python/Node client (or MCP server) can target a self-hosted OpenMail by base-URL swap. 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 ### MCP server
A thin MCP front-end over the same core: tools `create_inbox`, `list_messages`, `get_thread`, A thin MCP front-end over `core`: tools `create_inbox`, `list_messages`, `get_thread`, `send_message`,
`send_message`, `reply`, `search` — so an agent (Claude Code, etc.) drives its own mailbox directly. `reply`, `search` — so an agent drives its own mailbox directly.
## 5. Deliverability plan (the part that actually matters) ## 5. Deliverability plan (the moat — and why embedding Mox helps most here)
- **v1: relay-by-default.** Send through SES/Postmark/Resend for inbox placement on day one; OpenMail - **Self-host send is achievable from day one for the protocol parts** because mox `smtpclient` +
still owns inbound, storage, threading, API. Lowest-risk way to be useful immediately. `dane` + `mtasts` + `dkim` already implement authenticated, DANE/MTA-STS-secured MX delivery. What
- **Self-host SMTP send (opt-in):** dedicated IP with PTR/rDNS, SPF (`v=spf1 ip4:… -all`), DKIM remains is *operational*, not code: dedicated IP with PTR/rDNS, SPF (`v=spf1 ip4:… -all`), published
signing (publish the public key), DMARC (`p=quarantine``p=reject`), gradual IP warmup, FBL DKIM key, DMARC (`p=quarantine``p=reject`), gradual IP warmup, FBL enrollment, suppression lists.
enrollment, and bounce/complaint suppression lists. Documented honestly as a multi-week reputation - **Relay backend** (SES/Postmark/Resend) ships alongside for instant inbox placement while a
effort, not a flag flip. self-host IP warms up — chosen per-domain.
- **Inbound auth:** verify SPF/DKIM/DMARC on receipt; expose pass/fail in message metadata; optional - **Inbound auth:** SPF/DKIM/DMARC verified on receipt (mox pkgs); verdicts exposed in message
reject/quarantine policy per inbox. metadata; optional reject/quarantine policy per inbox.
## 6. MVP milestones ## 6. MVP milestones
1. **Core API + storage** — inboxes/messages/threads/drafts CRUD against Postgres + MinIO; bearer 1. **Core API + storage** — inboxes/messages/threads/drafts CRUD on Postgres + MinIO; bearer auth;
auth; no real mail yet (seed via an ingest endpoint). *Proves the data model + API.* ingest endpoint to seed without real mail. *Proves the data model + API.*
2. **Inbound**`go-smtp` on :25, MIME parse, extraction, threading, `message.received` webhook. 2. **Inbound**`go-smtp` on :25 + mox verify/parse/junk + threading + `message.received` webhook.
MX a test domain at the VPS. *Proves receive.* MX a test domain at the VPS. *Proves receive.*
3. **Outbound via relay**`send`/`reply` through SES/Postmark with DKIM. *Proves the loop: 3. **Outbound via relay**`send`/`reply`, mox-DKIM-signed, through a relay. *Proves the loop: an
an agent receives and replies.* agent receives and replies.*
4. **MCP server** — agent owns and operates an inbox end-to-end. 4. **MCP server** — agent owns and operates an inbox end-to-end.
5. **Self-host SMTP send + deliverability hardening** — the long tail. 5. **Self-host SMTP send** — wire mox `smtpclient`/`dane`/`mtasts`; the deliverability/warmup long tail.
## 7. Repo layout (planned) ## 7. Repo layout (planned)
``` ```
cmd/openmail/ # single binary: `serve`, `smtpd`, `sender`, `mcp` subcommands cmd/openmail/ # single binary: `serve`, `smtpd`, `sender`, `mcp` subcommands
internal/core/ # inbox/message/thread/draft services internal/core/ # inbox/message/thread/draft services
internal/smtp/ # inbound server + MIME parse + extraction + threading internal/smtp/ # go-smtp inbound server; calls mox verify/parse/junk + threading
internal/sender/ # Sender interface + smtp & relay backends + DKIM internal/sender/ # Sender interface: smtp (mox smtpclient+dane+mtasts) & relay backends; mox dkim
internal/store/ # sqlc queries, object-store client internal/store/ # sqlc queries, object-store client
internal/api/ # chi HTTP handlers (v0 surface) internal/api/ # chi HTTP handlers (v0 surface)
internal/mcp/ # MCP server over core internal/mcp/ # MCP server over core
deploy/ # docker-compose.yml, Caddyfile, DNS (MX/SPF/DKIM/DMARC) notes deploy/ # docker-compose.yml, Caddyfile, DNS (MX/SPF/DKIM/DMARC/DANE/MTA-STS) notes
db/migrations/ # SQL migrations db/migrations/ # SQL migrations
``` ```
## 8. Open questions ## 8. Licensing & attribution
- Relay vs self-host SMTP as the *default* — relay is pragmatic, self-host is the point. Likely ship OpenMail is **MIT** (see `LICENSE`). Every dependency is permissive: Mox (MIT), `emersion/go-smtp`
relay default + self-host documented. and `go-message` (MIT), `pgx`/`chi` (MIT/BSD). We import Mox as a Go module (no vendored source in
- How far to chase AgentMail API compatibility vs a cleaner native shape. 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. - Search: Postgres FTS is enough for v1; revisit (Meilisearch/Typesense) only if needed.
- Multi-tenancy depth — `pods` now, full RBAC later. - 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).
```
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Karti Tripathi and the OpenMail contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+35 -24
View File
@@ -1,38 +1,49 @@
# OpenMail # OpenMail
A self-hosted, AI-native mailbox for agents — open-source infrastructure that gives an AI agent its **An agent-native, self-hosted mail server.** One Go binary that gives an AI agent its own real
own real email inbox (receive, parse, thread, search, send) on a box you control. email address — receive, parse, thread, search, and send actual SMTP mail on a box you control
behind a clean REST API and an MCP server.
Think "Gmail for agents, self-hosted." A single Go service + Postgres + object storage you run on a Think "AgentMail, but self-hosted and MIT-licensed." OpenMail embeds the battle-tested mail
VPS, exposing a clean REST API (and an MCP server) so any agent can own an address, read its mail as internals of [Mox](https://github.com/mjl-/mox) (also MIT) for the hard, correctness-critical
structured threads, and reply. plumbing — DKIM, SPF/DMARC, DANE + MTA-STS secure delivery, real-world MIME parsing, spam
filtering — and layers a native, agent-shaped data model (Postgres + object storage) and API on top.
> **Status: private WIP.** This is a personal learning project and a self-hosting alternative in a > **Status: early WIP, private during initial build.** Will be released MIT-licensed and public.
> space that hosted products (e.g. AgentMail) serve well. Keep it private. Don't reference it in any > Designed only from public RFCs and public API surfaces — nothing proprietary.
> job application, PR, or interview, and shelve it if it would ever conflict with an employer. It was
> designed only from public API surfaces and public RFCs — nothing proprietary.
See **[ARCHITECTURE.md](./ARCHITECTURE.md)** for the design. License: **MIT** — see [LICENSE](./LICENSE). Builds on Mox (MIT) and the `emersion/go-*` mail
libraries. See **[ARCHITECTURE.md](./ARCHITECTURE.md)** for the design of record.
## Why ## Why
The hard, valuable part of an agent-mailbox product is not the API — it's the mail plumbing: The valuable, hard part of an agent-mailbox product is not the API — it's the mail plumbing:
receiving over SMTP/MX, sending with real deliverability (SPF/DKIM/DMARC, IP reputation), parsing receiving over SMTP/MX, *sending with real deliverability* (SPF/DKIM/DMARC, DANE/MTA-STS, IP
MIME, threading, and storage at scale. OpenMail is an exercise in building that plumbing properly, reputation), parsing messy MIME, threading, and storage. Hosted products (AgentMail and similar)
self-hostable, with an agent-first API on top. solve this well but are closed and run on someone else's infrastructure. OpenMail's bet: you can
**embed** an existing MIT-licensed, production-grade Go mail stack instead of rebuilding it, and
spend your effort on the part nobody has done well — the **agent-native** layer.
## What makes it agent-native
- **Persistent inboxes as first-class API resources**, provisioned in one call.
- **Structured threads**, not raw IMAP — `In-Reply-To`/`References` stitched into conversations.
- **`extracted_text`** — reply content with quoted history stripped, so an agent reads the new part.
- **MCP server** — an agent (Claude Code, etc.) owns and operates its mailbox directly as tools.
- **Webhooks + WebSocket** `message.received` events — agents react to mail in real time.
- **AgentMail-API-shaped** REST where reasonable, so existing tooling points at a self-hosted base URL.
## Goals ## Goals
- **Self-hostable** in one `docker compose up` on a single VPS, scaling to a small fleet later. - **Self-hostable** in one `docker compose up` on a single VPS; scales to a fleet later.
- **Agent-first API** — persistent inboxes, structured threads, `extracted_text` (quoted-history - **Deliverability taken seriously** — self-host SMTP send with DKIM + DANE + MTA-STS via Mox's
stripped), labels, search, drafts, webhooks/WebSocket events. delivery stack, *or* a relay backend (SES/Postmark/Resend) for inbox placement on day one.
- **AgentMail-API-shaped** where reasonable, so existing agent tooling/MCP can point at a self-hosted - **Single static Go binary** with subcommands; Postgres + S3-compatible object store as the only deps.
endpoint with minimal change. (Compatibility is a non-goal where it conflicts with a cleaner design.) - **Genuinely MIT** — every embedded dependency is MIT/BSD; no GPL/AGPL anywhere in the tree.
- **Deliverability taken seriously** — DKIM signing, SPF/DMARC guidance, and a pluggable send path
(self-host SMTP **or** relay through SES/Postmark) because reputation is the real moat.
## Non-goals (for v1) ## Non-goals (for v1)
- A hosted multi-tenant SaaS. OpenMail is self-host-first. - A hosted multi-tenant SaaS. OpenMail is self-host-first (multi-tenant `pods` exist, but you run it).
- Beating a mature provider on deliverability out of the box — that takes IP warmup and time. - A full webmail UI. The product is the API + MCP; humans use their own client.
- A webmail UI. The product is the API + MCP; humans use their own client or the CLI. - Beating a mature provider's deliverability on day one — self-host IP reputation takes warmup + time;
the relay backend exists for exactly that gap.