Two failures, two causes. GitHub-hosted jobs never started: 'recent account payments have failed or your spending limit needs to be increased'. Not a config problem. Public repos get hosted runners free, so this may resolve itself; meanwhile every job runs on spark-1, which is also the deployment architecture (Oracle Ampere A1 is aarch64), so this is an improvement rather than a workaround. The aarch64 job failed with exit 127 — cargo not found — and kept failing after each fix. Cause: Swatinem/rust-cache's post-step prunes ~/.cargo. That is correct on a GitHub-hosted runner where ~/.cargo is disposable, and destructive on a self-hosted one where it is the real toolchain. It deleted ~/.cargo/bin/rustup after the first successful run, leaving every shim a dangling symlink. The action is removed and the toolchain moved to /opt/rust, wired through the runner's .env and .path, so it is out of reach even if someone re-adds it. Caching bought nothing here anyway — the runner keeps its target dir between runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JkyvfNJGTshJNE9FtwPLk7
OpenMail
An agent-native, self-hosted mail server, written in Rust.
One binary that gives an AI agent its own real email address — receive, parse, thread, search and send actual SMTP mail on infrastructure you control — behind a clean REST API and an MCP server. Humans and agents are both first-class users.
Status: v0.1, work in progress, not yet released. The workspace compiles and the domain model is taking shape; it does not yet send or receive mail. Follow
docs/adr/0004-milestones.md.
Why this exists
The hard part of an agent-mailbox product was never the API. It is the mail plumbing: receiving over SMTP/MX, sending with real deliverability (SPF/DKIM/DMARC, DANE/MTA-STS, IP reputation), parsing genuinely broken MIME, threading, and storage.
Hosted agent-mail products solve this well and run on someone else's infrastructure, closed. The self-hostable mail servers that exist — Postfix+Dovecot, iRedMail, Stalwart — solve the plumbing but have no notion of an agent: no per-agent inbox provisioning, no threads as API resources, no MCP, no way for an agent to own a mailbox.
OpenMail is the intersection nobody occupies: agent-native, self-hostable, and permissively licensed.
What makes it agent-native
- Inboxes are API resources, provisioned in one call — not Unix accounts.
- Threads are first-class, stitched from
In-Reply-To/References. An agent asks for a conversation, not a folder listing. extracted_text— the reply with quoted history stripped. An agent that reads full bodies re-reads the whole thread every turn and burns its context window on text it already has.- MCP server — an agent owns and operates its own mailbox as tools.
- Webhooks + WebSocket
message.receivedevents. Push, not poll. - Humans too — standard IMAP/SMTP access is a first-class goal, not an afterthought, so a person can point Apple Mail or Thunderbird at the same mailbox an agent is driving.
Why Rust, and why our own crates
Two implementations of the mail plumbing exist in a permissive licence: Mox (MIT, Go) and — for the primitives only — Stalwart's published crates (Apache-2.0/MIT, Rust). Stalwart's server is AGPL-3.0, which is why nobody has shipped a permissively licensed Rust mail server.
We are building one. See docs/adr/0001-rust.md for
the decision and its honest costs.
Concretely, this means writing what the Rust ecosystem does not have. At the
time of writing, dane and mta-sts do not exist on crates.io at all —
Stalwart keeps its implementations inside AGPL server crates. Ours ship
standalone and permissive, so any Rust mail project can use them.
The workspace
Twelve crates in three tiers. Tier 1 is published to crates.io as a contribution to the Rust mail ecosystem and depends on nothing else here.
Tier 1 — standalone, publishable
| Crate | What | Prior art in Rust |
|---|---|---|
mail-dane |
DANE / TLSA verification for SMTP (RFC 7672) | none — first permissive implementation |
mail-mta-sts |
MTA-STS policy discovery, fetch, parse, cache (RFC 8461) | none — first permissive implementation |
mail-dsn |
Delivery Status Notifications (RFC 3464) | none |
Tier 2 — the mail engine
| Crate | What |
|---|---|
openmail-smtpd |
Inbound SMTP: session state machine, STARTTLS, AUTH, PIPELINING |
openmail-relay |
Outbound: smarthost relays (SES, Oracle, generic) and direct-to-MX |
openmail-guard |
Abuse gate: iprev, DNSBL, rate limiting |
openmail-junk |
Per-inbox Bayesian spam classification |
Tier 3 — the agent-native layer (the product)
| Crate | What |
|---|---|
openmail-core |
Domain model, threading, quote-stripping. No I/O. |
openmail-store |
Postgres metadata + S3-compatible blobs |
openmail-api |
The v0 REST API |
openmail-mcp |
MCP server |
openmail |
The binary: serve, smtpd, sender, mcp, migrate |
Third-party
mail-parser, mail-builder, mail-auth (DKIM/DKIM2/SPF/DMARC/ARC),
smtp-proto — all Apache-2.0 OR MIT, all from Stalwart Labs' separately
published primitive crates — plus hickory-resolver for DNS and DNSSEC.
No AGPL, GPL, or LGPL code is linked into any OpenMail binary. We use none
of the Stalwart server. See NOTICE and
docs/adr/0003-own-crates.md.
Sending: bring your own reputation, or build your own
Outbound sits behind one interface with two paths:
- Relay — SES, Oracle Cloud Email Delivery, SendGrid, Postmark, Resend, or
any smarthost. Rents someone else's IP reputation; inbox placement on day
one. Providers are declarative data, not special cases —
crates/openmail-relay/src/providers.rs. - Direct-to-MX — we resolve MX and deliver ourselves, with MTA-STS and DANE enforced. Our reputation, our control, and a months-long IP warmup.
Receiving is always ours.
⚠️ Oracle Cloud blocks outbound TCP/25 for tenancies created after 2021-06-23. Inbound :25 is unaffected. So on OCI you receive directly and relay outbound on 587 — direct-to-MX is not possible there at all.
docs/adr/0005-oracle-cloud.md.
Build
cargo check --workspace # ~21s cold on a Ryzen 7 5800X
cargo test --workspace
cargo clippy --workspace --all-targets # zero warnings is the gate
unsafe_code = "forbid" across the workspace. This code parses hostile input
from the open internet on port 25; there is no exception worth the risk.
Licence
Apache-2.0. Permissive on purpose: the point is that other people can build
commercial products on top of this, including ones that compete with anything
we might host later. See docs/adr/0002-apache-2.md
for why Apache-2.0 rather than MIT or AGPL.