Email infrastructure for AI agents. Send, receive, search, and extract verification codes.
Agent Integration: Use mails-skills to give your Claude Code, OpenClaw, or any AI agent email capabilities with one command.
Unlike raw email APIs that only send, mails gives your agent a complete email identity — send, receive, search, and extract verification codes in one package. Deploy on your own domain with Cloudflare (free tier). Full control, no third-party dependency.
- Send emails — via Resend with attachment support
- Receive emails — via Cloudflare Email Routing → Worker → D1
- Search inbox — FTS5 full-text search across subject, body, sender, code
- Semantic search — AI-powered vector search via Workers AI + Cloudflare Vectorize (keyword, semantic, hybrid modes)
- Dashboard console — visual email management UI at
mails0.com/console - Verification code extraction — auto-extracts 4-8 char codes (EN/ZH/JA/KO)
- Email threading — auto-assign
thread_idvia In-Reply-To / References headers - Auto labels — rule-based classification: newsletter, notification, code, personal
- Structured data extraction — extract orders, shipping, calendar, receipts from emails (rule-based, no LLM)
- Attachments — send via CLI (
--attach) or SDK; receive with R2 storage for large files - Webhook notifications — POST to your URL on email receive, with HMAC-SHA256 signature
- Mailbox isolation — per-token mailbox binding via
auth_tokensD1 table - Delete API — remove processed emails with cascade cleanup (attachments + R2)
- Storage providers — local SQLite (dev) or remote Worker API (production)
- Zero runtime dependencies — all providers use raw
fetch() - Self-hosted — deploy your own Worker on Cloudflare (free tier), full control over your data
npm install -g mails-agent
# or
bun install -g mails-agent
# or use directly
npx mails-agent# 1. Deploy your Worker (see Self-Hosted Deployment guide below)
cd worker && wrangler deploy
# 2. Configure the CLI
mails config set worker_url https://your-worker.example.com
mails config set worker_token YOUR_TOKEN
mails config set mailbox agent@yourdomain.com
mails config set default_from agent@yourdomain.com
# 3. Use it
mails send --to user@example.com --subject "Hello" --body "World"
mails inbox # List received emails
mails inbox --query "password" # Search emails
mails code --to agent@yourdomain.com # Wait for verification code SENDING RECEIVING
Agent External
| |
| mails send --to user@example.com | email to agent@yourdomain.com
| |
v v
+--------+ +-------------------+
| CLI |------ /api/send ----------------->| Cloudflare Email |
| /SDK |<----- /api/inbox -----------------| Routing |
+--------+ +-------------------+
| |
v v
+--------------------------------------------------+
| Your Cloudflare Worker |
| /api/send → Resend API → SMTP delivery |
| /api/inbox, /api/code → D1 query (FTS5 search) |
| email() handler → parse MIME → store in D1 |
+--------------------------------------------------+
| |
v v
+--------+ +------------+
| D1 | | R2 |
| emails | | attachments|
+--------+ +------------+
|
| query via CLI/SDK
v
Agent
mails inbox
mails inbox --query "code"
mails code --to agent@yourdomain.com
mails send --to <email> --subject <subject> --body <text>
mails send --to <email> --subject <subject> --html "<h1>Hello</h1>"
mails send --from "Name <email>" --to <email> --subject <subject> --body <text>
mails send --to <email> --subject "Report" --body "See attached" --attach report.pdfmails inbox # List recent emails
mails inbox --mailbox agent@test.com # Specific mailbox
mails inbox --query "password reset" # Search emails
mails inbox --query "invoice" --direction inbound --limit 10
mails inbox <id> # View email details + attachmentsmails code --to agent@test.com # Wait for code (default 30s)
mails code --to agent@test.com --timeout 60 # Custom timeoutThe code is printed to stdout for easy piping: CODE=$(mails code --to agent@test.com)
mails config # Show all config
mails config set <key> <value> # Set a value
mails config get <key> # Get a valueimport { send, getInbox, searchInbox, getEmail, deleteEmail, waitForCode } from 'mails-agent'
// Send
const result = await send({
to: 'user@example.com',
subject: 'Hello',
text: 'World',
})
// Send with attachment
await send({
to: 'user@example.com',
subject: 'Report',
text: 'See attached',
attachments: [{ path: './report.pdf' }],
})
// List inbox
const emails = await getInbox('agent@yourdomain.com', { limit: 10 })
// Search inbox
const results = await searchInbox('agent@yourdomain.com', {
query: 'password reset',
direction: 'inbound',
})
// Get email details (with attachments)
const email = await getEmail('email-id')
// Delete email (cascade: attachments + R2)
await deleteEmail('email-id')
// Wait for verification code
const code = await waitForCode('agent@yourdomain.com', { timeout: 30 })
if (code) console.log(code.code) // "123456"The CLI auto-detects the storage provider:
worker_urlin config → remote (queries Worker API)- Otherwise → local SQLite (
~/.mails/mails.db)
Config Keys
| Key | Set by | Description |
|---|---|---|
mailbox |
manual | Your receiving address |
worker_url |
manual | Worker URL (enables remote provider) |
worker_token |
manual | Auth token for Worker |
resend_api_key |
manual | Resend API key (not needed when worker_url is set) |
default_from |
manual | Default sender address |
storage_provider |
auto | sqlite or remote (auto-detected) |
Self-Hosted Deployment (Full Guide)
Run the entire email system on your own domain using Cloudflare + Resend. Full control, no third-party dependency.
| What | Why | Cost |
|---|---|---|
A domain (e.g. example.com) |
Email address agent@example.com |
You already own one |
| Cloudflare account | DNS, Email Routing, Worker, D1 | Free tier is enough |
| Resend account | SMTP delivery | Free 100 emails/day |
If your domain's DNS is not already on Cloudflare, add it at dash.cloudflare.com. Update your registrar's nameservers to the ones Cloudflare provides.
- Create a Resend account
- Go to Domains → Add Domain → enter your domain (e.g.
example.com) - Resend will give you DNS records to add. Go to Cloudflare DNS and add:
- SPF —
TXTrecord on@:v=spf1 include:amazonses.com ~all(Resend uses SES) - DKIM —
CNAMErecords as provided by Resend (usually 3 records) - DMARC —
TXTrecord on_dmarc:v=DMARC1; p=none;(start withnone, tighten later)
- SPF —
- Wait for Resend to verify your domain (usually minutes, can take up to 48h)
- Copy your Resend API key (
re_...) from the Resend dashboard
cd worker
bun install
# Create D1 database
wrangler d1 create mails
# → Copy the database_id from the output
# Edit wrangler.toml — paste your database_id
# Replace REPLACE_WITH_YOUR_DATABASE_ID with the actual ID
# Initialize database schema
wrangler d1 execute mails --file=schema.sql
# Set secrets
wrangler secret put AUTH_TOKEN # Choose a strong random token
wrangler secret put RESEND_API_KEY # Paste your re_... key from Resend
# Deploy
wrangler deploy
# → Note the Worker URL: https://mails-worker.<your-subdomain>.workers.dev- Go to Cloudflare Dashboard → your domain → Email → Email Routing
- Click Enable Email Routing (Cloudflare will add MX records automatically)
- Go to Routing rules → Catch-all address → set action to Send to a Worker → select your deployed Worker
- Now all emails to
*@example.comwill be routed to your Worker
wrangler r2 create mails-attachmentsThe R2 binding is already configured in wrangler.toml. Redeploy after creating the bucket:
wrangler deploymails config set worker_url https://mails-worker.<your-subdomain>.workers.dev
mails config set worker_token YOUR_AUTH_TOKEN # Same token from Step 3
mails config set mailbox agent@example.com # Your email address
mails config set default_from agent@example.com # Default sender# Check Worker is reachable
curl https://mails-worker.<your-subdomain>.workers.dev/health
# Check inbox (should be empty)
mails inbox
# Send a test email
mails send --to your-personal@gmail.com --subject "Test" --body "Hello from self-hosted mails"
# Send an email TO your mailbox from any email client, then:
mails inboxYour Agent External sender
| |
| mails send / mails inbox | email to agent@example.com
v v
+--------+ +-------------------+
| CLI |------ /api/send ------->| Cloudflare Email |
| /SDK |<----- /api/inbox -------| Routing |
+--------+ +-------------------+
| |
v v
+--------------------------------------------------+
| Your Cloudflare Worker |
| /api/send → Resend API → SMTP delivery |
| /api/inbox, /api/code → D1 query (FTS5 search) |
| email() handler → parse MIME → store in D1 |
+--------------------------------------------------+
| |
v v
+--------+ +------------+
| D1 | | R2 |
| emails | | attachments|
+--------+ +------------+
| Secret | Required | Description |
|---|---|---|
AUTH_TOKEN |
Recommended | API authentication token. If set, all /api/* endpoints require Authorization: Bearer <token> |
RESEND_API_KEY |
Yes (for sending) | Resend API key (re_...). The Worker uses this to send emails via /api/send |
WEBHOOK_SECRET |
Optional | HMAC-SHA256 key for signing webhook payloads (X-Webhook-Signature header) |
| Endpoint | Description |
|---|---|
POST /api/send |
Send email (requires RESEND_API_KEY secret) |
GET /api/inbox?to=<addr>&limit=20 |
List emails |
GET /api/inbox?to=<addr>&query=<text> |
Search emails (FTS5 full-text search) |
GET /api/code?to=<addr>&timeout=30 |
Long-poll for verification code |
GET /api/email?id=<id> |
Get email by ID (with attachments) |
DELETE /api/email?id=<id> |
Delete email (and its attachments + R2 objects) |
GET /api/attachment?id=<id> |
Download attachment |
GET /api/threads?to=<addr> |
List conversation threads |
GET /api/thread?id=<id>&to=<addr> |
Get all emails in a thread |
GET /api/search?to=<addr>&q=<text>&mode=hybrid |
Semantic/hybrid search (alias for inbox with mode=hybrid) |
POST /api/extract |
Extract structured data (order, shipping, calendar, receipt, code) |
GET /api/me |
Worker info and capabilities |
GET /health |
Health check (always public, no auth) |
When the CLI/SDK sends an email, it checks config in this order:
worker_url→ sends via your Worker's/api/send(recommended)resend_api_key→ sends directly to Resend API
Once worker_url is set, you don't need resend_api_key on the client — the Worker holds the Resend key as a secret.
Testing
bun test # Unit + mock E2E tests
bun test:coverage # With coverage report
bun test:live # Live E2E with real Resend + Cloudflare (requires .env)298 tests across 29 test files.
┌─────────────────────────────────────────────────────────────┐
│ mails ecosystem │
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌───────────┐ │
│ │ mails CLI │ │ mails Worker │ │ mails │ │
│ │ & SDK │───▶│ (Cloudflare) │◀───│ -skills │ │
│ │ │ │ │ │ │ │
│ │ npm i mails- │ │ Receive + Send │ │ Agent │ │
│ │ agent │ │ │ │ │ │
│ │ │ │ + Search + Code │ │ Skills │ │
│ └──────────────┘ └──────────────────┘ └───────────┘ │
│ Human / Script Infrastructure AI Agents │
└─────────────────────────────────────────────────────────────┘
| Project | What it is | Who uses it |
|---|---|---|
| mails (this repo) | Email server (Worker) + CLI + SDK | Developers deploying email infra |
| mails-agent-mcp | MCP Server for AI agents | Claude Desktop, Cursor, any MCP client |
| mails-agent (Python) | Python SDK | Python developers, async agents |
| mails-skills | Skill files for AI agents | AI agents (Claude Code, OpenClaw, Cursor) |
Quick agent setup:
# MCP Server (Claude Desktop / Cursor)
npm install -g mails-agent-mcp
# Python SDK
pip install mails-agent
# Agent Skills
git clone https://github.com/Digidai/mails-skills && cd mails-skills && ./install.shSee CONTRIBUTING.md for development setup, project structure, and PR guidelines.
This project is based on mails by turing, originally created as email infrastructure for AI agents. We forked and extended it with mailbox isolation, webhook notifications, delete API, R2 attachment storage, Worker file refactoring, and comprehensive test coverage (298 tests). Thank you to the original author for the excellent foundation.
MIT — see LICENSE for details. Original copyright retained per MIT terms.