Store: the full Postgres schema as an embedded migration. UUIDv7 keys so `ORDER BY id` is a free chronological index; raw MIME and attachments live in object storage with only a key in the row; `pods` present from day one because retrofitting tenancy costs more than an unused column. API keys are stored as a SHA-256 hash — a database dump must not be a set of live credentials. API: the v0 route table, including `ingest`, which closes the receive→thread→extract loop with zero mail infrastructure and is what makes the agent layer testable in CI. Scopes are a closed enum rather than strings, so "can send mail" and "can mint keys" are not one typo apart. Internal errors are logged in full and reported as a bare string. MCP: the tool catalogue, six tools. Adding a row here is the only way an agent gains a capability — a new REST route is invisible until someone opts it in. Three tests guard the rule that no tool can ever reach key management; CI fails rather than production. ADR 0006: enterprise self-hosted first. A hosted offering comes only after we have run this ourselves long enough to have a deliverability record worth selling. `pods` stays in the schema as the thing that keeps that path open — do not remove it as dead code. Also: multi-stage Dockerfile running as a non-root system user with no shell in the runtime image, and the openmail.karti.ai static page. 17 tests, zero clippy warnings, fmt clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JkyvfNJGTshJNE9FtwPLk7
223 lines
9.5 KiB
HTML
223 lines
9.5 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>OpenMail — agent-native mail you host yourself</title>
|
|
<meta name="description" content="An agent-native, self-hosted mail server in Rust. Apache-2.0. Give an AI agent a real email address on infrastructure you control.">
|
|
<meta property="og:title" content="OpenMail">
|
|
<meta property="og:description" content="Agent-native, self-hosted mail server. Rust, Apache-2.0.">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:url" content="https://openmail.karti.ai">
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✉️</text></svg>">
|
|
<style>
|
|
:root {
|
|
--bg:#0b0d10; --panel:#12151a; --line:#232830;
|
|
--fg:#e6e9ee; --dim:#98a2b3; --faint:#6b7480;
|
|
--accent:#7dd3a0; --warn:#e8b464;
|
|
--mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
|
|
--sans: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", system-ui, sans-serif;
|
|
}
|
|
@media (prefers-color-scheme: light) {
|
|
:root { --bg:#fbfbfa; --panel:#fff; --line:#e4e4e0; --fg:#16181d;
|
|
--dim:#5a626e; --faint:#8a919c; --accent:#1a7f4e; --warn:#8a5a10; }
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body { margin:0; background:var(--bg); color:var(--fg); font-family:var(--sans);
|
|
line-height:1.6; -webkit-font-smoothing:antialiased; }
|
|
.wrap { max-width: 820px; margin:0 auto; padding: 0 24px; }
|
|
code, pre, .mono { font-family: var(--mono); }
|
|
|
|
header { padding: 88px 0 56px; border-bottom:1px solid var(--line); }
|
|
.logo { font-family:var(--mono); font-size:13px; letter-spacing:.14em;
|
|
text-transform:uppercase; color:var(--accent); margin-bottom:26px; }
|
|
h1 { font-size: clamp(30px, 5.4vw, 46px); line-height:1.12; margin:0 0 18px;
|
|
letter-spacing:-0.025em; font-weight:640; }
|
|
.lede { font-size:19px; color:var(--dim); max-width:60ch; margin:0 0 26px; }
|
|
.badges { display:flex; gap:10px; flex-wrap:wrap; font-family:var(--mono); font-size:12px; }
|
|
.badge { border:1px solid var(--line); border-radius:5px; padding:4px 10px; color:var(--dim); }
|
|
.badge.wip { color:var(--warn); border-color:color-mix(in srgb, var(--warn) 40%, var(--line)); }
|
|
|
|
section { padding: 52px 0; border-bottom:1px solid var(--line); }
|
|
h2 { font-size:13px; font-family:var(--mono); text-transform:uppercase;
|
|
letter-spacing:.13em; color:var(--faint); margin:0 0 24px; font-weight:500; }
|
|
h3 { font-size:17px; margin:0 0 6px; font-weight:600; letter-spacing:-0.01em; }
|
|
p { margin:0 0 16px; }
|
|
p.sub { color:var(--dim); }
|
|
a { color:var(--accent); text-decoration:none; border-bottom:1px solid transparent; }
|
|
a:hover { border-bottom-color: currentColor; }
|
|
|
|
.grid { display:grid; gap:20px; grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); }
|
|
.card { background:var(--panel); border:1px solid var(--line); border-radius:9px; padding:20px; }
|
|
.card p { margin:0; color:var(--dim); font-size:14.5px; }
|
|
|
|
pre { background:var(--panel); border:1px solid var(--line); border-radius:9px;
|
|
padding:18px; overflow-x:auto; font-size:13.5px; line-height:1.65; margin:0 0 16px; }
|
|
pre .c { color:var(--faint); }
|
|
|
|
table { width:100%; border-collapse:collapse; font-size:14.5px; }
|
|
th { text-align:left; font-family:var(--mono); font-size:11.5px; text-transform:uppercase;
|
|
letter-spacing:.1em; color:var(--faint); font-weight:500;
|
|
padding:0 12px 10px 0; border-bottom:1px solid var(--line); }
|
|
td { padding:11px 12px 11px 0; border-bottom:1px solid var(--line); vertical-align:top; }
|
|
td:first-child { font-family:var(--mono); font-size:13px; white-space:nowrap; }
|
|
td.note { color:var(--dim); }
|
|
.first { color:var(--accent); font-size:12.5px; font-family:var(--mono); }
|
|
.tablewrap { overflow-x:auto; }
|
|
|
|
.callout { background:var(--panel); border:1px solid var(--line);
|
|
border-left:3px solid var(--warn); border-radius:0 9px 9px 0;
|
|
padding:16px 20px; margin:0 0 16px; }
|
|
.callout p { margin:0; font-size:14.5px; color:var(--dim); }
|
|
|
|
footer { padding:44px 0 72px; color:var(--faint); font-size:13.5px; }
|
|
footer a { color:var(--dim); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<div class="wrap">
|
|
<div class="logo">OpenMail</div>
|
|
<h1>Give an agent a real email address — on a box you own.</h1>
|
|
<p class="lede">
|
|
An agent-native, self-hosted mail server in Rust. Receive, parse, thread,
|
|
search and send real SMTP mail behind a REST API and an MCP server.
|
|
Agents and humans are both first-class users.
|
|
</p>
|
|
<div class="badges">
|
|
<span class="badge">Apache-2.0</span>
|
|
<span class="badge">Rust</span>
|
|
<span class="badge">Self-hosted</span>
|
|
<span class="badge wip">v0.1 — in development</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<section>
|
|
<div class="wrap">
|
|
<h2>The gap</h2>
|
|
<p>
|
|
The hard part of an agent mailbox was never the API. It is the plumbing:
|
|
receiving over SMTP, sending with real deliverability, parsing genuinely
|
|
broken MIME, threading, storage.
|
|
</p>
|
|
<p class="sub">
|
|
Hosted agent-mail products solve that well — closed, on someone else's
|
|
infrastructure. Self-hostable mail servers solve it too — with no notion
|
|
of an agent: no per-agent inbox provisioning, no threads as API
|
|
resources, no MCP.
|
|
</p>
|
|
<p><strong>OpenMail is the intersection nobody occupies:</strong> agent-native, self-hostable, permissively licensed.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<div class="wrap">
|
|
<h2>What agent-native means</h2>
|
|
<div class="grid">
|
|
<div class="card">
|
|
<h3>Inboxes are API resources</h3>
|
|
<p>Provisioned in one call. Not Unix accounts, not aliases, not a mailbox someone creates by hand.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Threads, not folders</h3>
|
|
<p>Stitched from <code>In-Reply-To</code> and <code>References</code>. An agent asks for a conversation.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Quoted history stripped</h3>
|
|
<p><code>extracted_text</code> is the new content only. An agent reading full bodies re-reads the whole thread every turn.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>MCP, natively</h3>
|
|
<p>An agent owns and operates its mailbox as tools. Credential routes can never become tools.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Push, not poll</h3>
|
|
<p>Webhooks and a WebSocket stream for <code>message.received</code>.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Humans too</h3>
|
|
<p>Standard IMAP/SMTP is a goal, not an afterthought. Point Apple Mail at the mailbox an agent is driving.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<div class="wrap">
|
|
<h2>Built on crates that did not exist</h2>
|
|
<p class="sub">
|
|
Two implementations of mail plumbing exist under a permissive licence: Mox
|
|
(MIT, Go), and Stalwart's primitives (Apache-2.0, Rust). Stalwart's
|
|
<em>server</em> is AGPL — which is why no permissively licensed Rust mail
|
|
server exists.
|
|
</p>
|
|
<p class="sub">
|
|
Building one means writing what the ecosystem is missing. These ship
|
|
standalone so any Rust mail project can use them.
|
|
</p>
|
|
<div class="tablewrap">
|
|
<table>
|
|
<thead><tr><th>Crate</th><th>What</th><th>Prior art</th></tr></thead>
|
|
<tbody>
|
|
<tr><td>mail-dane</td><td class="note">DANE / TLSA for SMTP (RFC 7672)</td><td class="first">first in Rust</td></tr>
|
|
<tr><td>mail-mta-sts</td><td class="note">MTA-STS policy discovery and cache (RFC 8461)</td><td class="first">first in Rust</td></tr>
|
|
<tr><td>mail-dsn</td><td class="note">Delivery Status Notifications (RFC 3464)</td><td class="first">first in Rust</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<div class="wrap">
|
|
<h2>Sending: rent reputation, or build your own</h2>
|
|
<pre><span class="c"># relay — someone else's IP reputation, inbox placement on day one</span>
|
|
provider = "ses" <span class="c"># or oracle, sendgrid, postmark, resend, custom</span>
|
|
region = "us-east-1"
|
|
|
|
<span class="c"># or direct-to-MX — your reputation, MTA-STS and DANE enforced</span>
|
|
mode = "direct"</pre>
|
|
<p class="sub">Receiving is always yours.</p>
|
|
<div class="callout">
|
|
<p>
|
|
<strong>Oracle Cloud blocks outbound TCP/25</strong> for tenancies created
|
|
after June 2021 — inbound is unaffected. There you receive directly and
|
|
relay outbound on 587. Direct-to-MX is not possible on that host at all.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<div class="wrap">
|
|
<h2>Self-hosted first</h2>
|
|
<p>
|
|
OpenMail is a server you run. Not a trial of a hosted product, not an
|
|
open-core teaser with the useful half behind a licence key.
|
|
</p>
|
|
<p class="sub">
|
|
Apache-2.0 means you can run it, fork it, and build a commercial product
|
|
on top — including one that competes with anything we might host later.
|
|
That is the intent, not an oversight.
|
|
</p>
|
|
<pre>git clone https://github.com/karti-ai/openmail
|
|
cd openmail && docker compose up</pre>
|
|
</div>
|
|
</section>
|
|
|
|
<footer>
|
|
<div class="wrap">
|
|
<p>
|
|
<a href="https://github.com/karti-ai/openmail">GitHub</a> ·
|
|
<a href="https://github.com/karti-ai/openmail/tree/main/docs/adr">Decision records</a> ·
|
|
<a href="https://github.com/karti-ai/openmail/blob/main/SECURITY.md">Security</a>
|
|
</p>
|
|
<p>Apache-2.0. Not affiliated with any similarly named project.</p>
|
|
</div>
|
|
</footer>
|
|
|
|
</body>
|
|
</html>
|