Self-hosted email for humans and AI agents, entirely on Cloudflare
TypeScript
328
69 commits
updated Oct 1, 2026
A self-hosted email system shared by humans and AI agents. Humans read, compose, and reply through a Gmail-style web UI; agents read and send through MCP. Inbound mail can be triaged with automatic labels and answered by an agent-drafted reply, held for one-click human approval.
Runs entirely on Cloudflare: Workers, Email Routing, D1, R2, and Web Push.

Email workspace
AI assistance
Self-hosted on Cloudflare
Requirements: a domain on Cloudflare, R2 enabled, and Workers Paid for outbound email. The deploy button requires this repository to be public.
Every step, plus updates, manual setup, and troubleshooting, is in the deployment guide.
One Worker serves the web app, the API, inbound email, the draft queue, and the MCP server.
inbound email ──► Email Routing ──► email() handler ──► D1 + R2 (raw MIME / attachments)
│
Queue ──► Draft Run ──► Agent Draft
│
web UI (React SPA) ──► /api (Hono) ───────────────────────┤
external agents ──► OAuth 2.1 ──► /mcp (MCP 2026-07-28) ─┤
└─ /authorize ──► Access (same app as web UI)
outbound Reply Attempt ──► Cloudflare Email Sending ──────┤
outbound Send Attempt ──► Cloudflare Email Sending ───────┘
new inbound Message ──► Web Push ──► subscribed browsers
mailboxes; the unified view queries across all of them. Mailboxes are added
explicitly in Settings, and unknown recipient addresses are rejected.In-Reply-To / References) with a
sender-aware, reply-only normalized-subject fallback. New inbound mail
reopens an archived Conversation.Reply-To address.guest-post,
link-exchange) with a natural-language match condition. New inbound mail is
evaluated once with the typesafe/jev model and tagged with every matching
label; replies are never labeled.Precedence: bulk,
list mail) are flagged and never receive automated replies.Browser notifications are off by default. Deployment automatically provisions
VAPID keys and uses the deploying Cloudflare user's email as the push contact
(an existing VAPID_SUBJECT is preserved). Turn notifications on under
Settings → General; each browser must grant permission and subscribe once.
Turning the global switch off removes all stored subscriptions.
The MCP server runs in the same Worker as the web app, at
https://<your-hostname>/mcp; the URL is shown in Settings → General. It
calls the Inbox domain modules directly and does not proxy or expose the Web
API. It uses the stateless MCP 2026-07-28 handler and stays compatible with
published 2025 stateless clients.
| Tool | Scope | Description |
|---|---|---|
list_inboxes | inbox.read | List registered Inboxes |
search_conversations | inbox.read | Search and filter Conversations |
get_conversation | inbox.read | Read a Conversation and its Messages |
reply_to_conversation | inbox.send | Reply to an inbound Message |
send_email | inbox.send | Send new mail from an Inbox |
Sending. The two send tools are an explicit owner-level capability: they
send immediately, require a stable idempotency_key, and only send from an
Inbox already registered in Mailroom. Replies require the exact inbound Message
and reviewed reply target; the server calculates RFC threading. Send Attempts
are limited to MCP_DAILY_SEND_LIMIT (default 100) per Access identity per UTC
day. Set MCP_SEND_ENABLED=false to remove the send tools from every client
immediately.
Authorization. The Worker is its own OAuth 2.1 authorization server. It
supports Client ID Metadata Documents (the MCP 2026 preferred registration
mechanism) and Dynamic Client Registration as a fallback, so clients need no
preconfigured callback URLs. Authorization Code uses S256 PKCE; access tokens
last 15 minutes and refresh tokens 30 days. Read access is always required; the
send tools are registered only when the token includes inbox.send.
Enabling it. MCP is deployed with the app. Because Access protects the
whole Worker, MCP clients need a Bypass application for /mcp and
/.well-known; the /authorize consent page stays behind the same Access
application as the web UI. See
connect an AI agent over MCP.
WEB_ACCESS_TEAM_DOMAIN and WEB_ACCESS_AUD; the app's setup screen
shows both values (see deployment setup).Origin header./mcp itself
accepts only OAuth access tokens.src/worker/dev.ts) that skips
JWT verification. Never deploy wrangler.dev.jsonc or expose the dev server.Requires Node.js 22.18+ or 24+.
npm ci
cp .dev.vars.example .dev.vars
# Local resources are emulated; no Cloudflare login or resource creation needed.
npm run db:migrate:local
npm run db:seed:local
npm run dev
Local development uses wrangler.dev.jsonc, which omits the AI and outbound
email bindings. Inbound handling, the API, and the web UI remain available;
sending a real reply requires the deployed Worker.
With npm run dev running (Vite on port 5173):
npm run email:test # plain message
npm run email:test:attachment # message with an attachment
npm run email:test:reopen # reply that reopens an archived conversation
These POST the .eml files in scripts/ to the local email handler. Use
npm run dev, not npx wrangler dev: the tests always target port 5173 and
wrangler.dev.jsonc.
npm run check # TypeScript
npm test # unit tests in tests/
/inbox and /inbox/:threadId — unified inbox and a selected conversation/mailboxes/:mailboxId — one Inbox/mailboxes/:mailboxId/threads/:threadId — a conversation within that Inbox/settings/inboxes/:mailboxId — Base Instructions and Playbooks for an Inbox/settings/general — workspace-wide settings, including browser notificationsSearch and conversation filters are URL parameters (?q=...&filter=unread|drafts),
so refresh, browser history, and shared links preserve the current view.
src/web/ React SPA
src/worker/ Worker: API, inbound email, drafting agent, notifications
src/mcp/ MCP server and OAuth authorization
src/shared/ Code shared by the web app and Worker
migrations/ D1 schema migrations
scripts/ Deploy script, seed data, and test emails
docs/ Deployment guide
typesafe/jev classification on new inbound mail/api/search on messages_fts)Apache-2.0
12 followers · starred Sep 2026
668 followers · starred Sep 2026
Self-hosted email for humans and AI agents, entirely on Cloudflare
TypeScript
328
69 commits
updated Oct 1, 2026
A self-hosted email system shared by humans and AI agents. Humans read, compose, and reply through a Gmail-style web UI; agents read and send through MCP. Inbound mail can be triaged with automatic labels and answered by an agent-drafted reply, held for one-click human approval.
Runs entirely on Cloudflare: Workers, Email Routing, D1, R2, and Web Push.

Email workspace
AI assistance
Self-hosted on Cloudflare
Requirements: a domain on Cloudflare, R2 enabled, and Workers Paid for outbound email. The deploy button requires this repository to be public.
Every step, plus updates, manual setup, and troubleshooting, is in the deployment guide.
One Worker serves the web app, the API, inbound email, the draft queue, and the MCP server.
inbound email ──► Email Routing ──► email() handler ──► D1 + R2 (raw MIME / attachments)
│
Queue ──► Draft Run ──► Agent Draft
│
web UI (React SPA) ──► /api (Hono) ───────────────────────┤
external agents ──► OAuth 2.1 ──► /mcp (MCP 2026-07-28) ─┤
└─ /authorize ──► Access (same app as web UI)
outbound Reply Attempt ──► Cloudflare Email Sending ──────┤
outbound Send Attempt ──► Cloudflare Email Sending ───────┘
new inbound Message ──► Web Push ──► subscribed browsers
mailboxes; the unified view queries across all of them. Mailboxes are added
explicitly in Settings, and unknown recipient addresses are rejected.In-Reply-To / References) with a
sender-aware, reply-only normalized-subject fallback. New inbound mail
reopens an archived Conversation.Reply-To address.guest-post,
link-exchange) with a natural-language match condition. New inbound mail is
evaluated once with the typesafe/jev model and tagged with every matching
label; replies are never labeled.Precedence: bulk,
list mail) are flagged and never receive automated replies.Browser notifications are off by default. Deployment automatically provisions
VAPID keys and uses the deploying Cloudflare user's email as the push contact
(an existing VAPID_SUBJECT is preserved). Turn notifications on under
Settings → General; each browser must grant permission and subscribe once.
Turning the global switch off removes all stored subscriptions.
The MCP server runs in the same Worker as the web app, at
https://<your-hostname>/mcp; the URL is shown in Settings → General. It
calls the Inbox domain modules directly and does not proxy or expose the Web
API. It uses the stateless MCP 2026-07-28 handler and stays compatible with
published 2025 stateless clients.
| Tool | Scope | Description |
|---|---|---|
list_inboxes | inbox.read | List registered Inboxes |
search_conversations | inbox.read | Search and filter Conversations |
get_conversation | inbox.read | Read a Conversation and its Messages |
reply_to_conversation | inbox.send | Reply to an inbound Message |
send_email | inbox.send | Send new mail from an Inbox |
Sending. The two send tools are an explicit owner-level capability: they
send immediately, require a stable idempotency_key, and only send from an
Inbox already registered in Mailroom. Replies require the exact inbound Message
and reviewed reply target; the server calculates RFC threading. Send Attempts
are limited to MCP_DAILY_SEND_LIMIT (default 100) per Access identity per UTC
day. Set MCP_SEND_ENABLED=false to remove the send tools from every client
immediately.
Authorization. The Worker is its own OAuth 2.1 authorization server. It
supports Client ID Metadata Documents (the MCP 2026 preferred registration
mechanism) and Dynamic Client Registration as a fallback, so clients need no
preconfigured callback URLs. Authorization Code uses S256 PKCE; access tokens
last 15 minutes and refresh tokens 30 days. Read access is always required; the
send tools are registered only when the token includes inbox.send.
Enabling it. MCP is deployed with the app. Because Access protects the
whole Worker, MCP clients need a Bypass application for /mcp and
/.well-known; the /authorize consent page stays behind the same Access
application as the web UI. See
connect an AI agent over MCP.
WEB_ACCESS_TEAM_DOMAIN and WEB_ACCESS_AUD; the app's setup screen
shows both values (see deployment setup).Origin header./mcp itself
accepts only OAuth access tokens.src/worker/dev.ts) that skips
JWT verification. Never deploy wrangler.dev.jsonc or expose the dev server.Requires Node.js 22.18+ or 24+.
npm ci
cp .dev.vars.example .dev.vars
# Local resources are emulated; no Cloudflare login or resource creation needed.
npm run db:migrate:local
npm run db:seed:local
npm run dev
Local development uses wrangler.dev.jsonc, which omits the AI and outbound
email bindings. Inbound handling, the API, and the web UI remain available;
sending a real reply requires the deployed Worker.
With npm run dev running (Vite on port 5173):
npm run email:test # plain message
npm run email:test:attachment # message with an attachment
npm run email:test:reopen # reply that reopens an archived conversation
These POST the .eml files in scripts/ to the local email handler. Use
npm run dev, not npx wrangler dev: the tests always target port 5173 and
wrangler.dev.jsonc.
npm run check # TypeScript
npm test # unit tests in tests/
/inbox and /inbox/:threadId — unified inbox and a selected conversation/mailboxes/:mailboxId — one Inbox/mailboxes/:mailboxId/threads/:threadId — a conversation within that Inbox/settings/inboxes/:mailboxId — Base Instructions and Playbooks for an Inbox/settings/general — workspace-wide settings, including browser notificationsSearch and conversation filters are URL parameters (?q=...&filter=unread|drafts),
so refresh, browser history, and shared links preserve the current view.
src/web/ React SPA
src/worker/ Worker: API, inbound email, drafting agent, notifications
src/mcp/ MCP server and OAuth authorization
src/shared/ Code shared by the web app and Worker
migrations/ D1 schema migrations
scripts/ Deploy script, seed data, and test emails
docs/ Deployment guide
typesafe/jev classification on new inbound mail/api/search on messages_fts)Apache-2.0
12 followers · starred Sep 2026
668 followers · starred Sep 2026