Receive-only temp email server with automatic OTP extraction for security research, QA automation, and disposable identity workflows.
- SMTP catch-all — accepts all inbound email on your domain (port 25, no auth)
- Real-time inbox — SSE stream pushes new emails instantly to the browser
- OTP extraction — automatic detection of verification codes (4–8 digit, alphanumeric)
- Link extraction — parse all URLs from plain-text and HTML email bodies
- REST API — authenticated endpoints for email generation, reading, OTP, and link extraction
- MCP integration — stdio-based Model Context Protocol server for AI agents (Hermes, Claude, etc.)
- Docker — multi-stage build, SQLite persistence, health check included
- Dark/light mode — built with Tailwind CSS + shadcn/ui
- DNS verification — built-in
/api/dns-checkendpoint validates MX, SPF, DMARC records - Multi-key support — manage API keys via the web UI or
/api/keysendpoint
- Node.js 20+ (with
tsxfor MCP;npmfor package management) - Domain name — configured with MX, SPF, and DMARC records
- Port 25 access — required for SMTP receiving; unavailable on most home ISPs. Use a VPS (Oracle Cloud Free Tier, AWS EC2, DigitalOcean) or port forwarding if supported
git clone <repo-url> website-email
cd website-email
npm installcp .env.example .envEdit .env with your domain and a strong API key:
DOMAIN=mail.yourdomain.com
API_KEY=your-generated-api-keyGenerate a secure API key:
openssl rand -hex 32Configure MX, SPF, and DMARC records for your domain. See the complete guide:
Quick verification after DNS propagates:
dig MX yourdomain.com +short
dig TXT yourdomain.com +short | grep spf
dig TXT _dmarc.yourdomain.com +shortTerminal 1 — Next.js web UI + API:
npm run devTerminal 2 — SMTP server (receives mail):
npx tsx src/infrastructure/smtp/start.tshttp://localhost:3000
Generate a temp email address, send a test email to it, and watch it appear in real-time.
Same as Quick Start — fill in DOMAIN and API_KEY.
docker compose up -dcurl http://localhost:3000/api/config
# {"success":true,"data":{"domain":"mail.yourdomain.com"}}
# Note: never returns API_KEY — paste API_KEY from .env into the UI when promptedNote: Docker Compose exposes port 3000 only (web UI + API). Run the SMTP server directly on the host for port 25:
npx tsx src/infrastructure/smtp/start.tsFor production, use a reverse proxy (nginx/Caddy) for TLS termination and run SMTP via a process manager (systemd, pm2).
| Variable | Required | Default | Description |
|---|---|---|---|
DOMAIN |
Yes | — | Your domain for receiving email (e.g. thefreelancer.web.id) |
API_KEY |
Yes | — | Secret key for API, MCP, and Cloudflare Worker (must match Worker secret) |
SMTP_PORT |
No | 25 |
Port for inbound SMTP |
SMTP_HOST |
No | 0.0.0.0 |
Bind address for SMTP server |
DB_PATH |
No | ./data/emails.db |
SQLite database file path |
NEXT_PUBLIC_APP_URL |
No | http://localhost:3000 |
Public URL of the application |
CLOUDFLARE_WORKER_URL |
No | (empty) | Cloudflare Worker base URL for cloud inbox fallback. Empty = local SQLite only. Example: https://freelancer-mail.<subdomain>.workers.dev |
Do not use legacy URLs such as
tempik.email1-f03.workers.dev. Current worker name isfreelancer-mail. Seecloudflare-email-worker/README.mdand rootAGENTS.md.
Most endpoints require an x-api-key header matching server API_KEY or an active key created via /api/keys.
GET /api/config is public and returns { domain } only (never the secret).
Response format: { "success": true, "data": ... } or { "success": false, "error": "..." }.
npm test # unit tests (vitest)
npm run lint
npm run build| Method | Endpoint | Description |
|---|---|---|
GET |
/api/addresses |
List all addresses |
POST |
/api/addresses |
Generate new address (optional body: { "address": "custom@domain" }) |
DELETE |
/api/addresses/:id |
Delete an address and its emails |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/emails/generate |
Generate address (shorthand) |
GET |
/api/emails/:address |
List emails for an address |
GET |
/api/emails/:address/:id |
Read a specific email |
DELETE |
/api/emails/:address/:id |
Delete a specific email |
POST |
/api/emails/:address/:id/otp |
Extract OTP from email |
GET |
/api/emails/:address/:id/links |
Extract links from email |
GET |
/api/emails/:address/stream |
SSE stream for real-time email events |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/config |
Get server domain only (no auth, no secrets) |
GET |
/api/dns-check |
Verify DNS records (MX, SPF, DMARC; requires API key) |
GET |
/api/keys |
List API keys |
POST |
/api/keys |
Create new API key |
KEY="your-api-key"
DOMAIN="mail.example.com"
# Generate an address
curl -sH "x-api-key: $KEY" -X POST http://localhost:3000/api/emails/generate | jq
# List emails
curl -sH "x-api-key: $KEY" "http://localhost:3000/api/emails/temp-abc123@$DOMAIN" | jq
# Read email and extract OTP
curl -sH "x-api-key: $KEY" -X POST \
"http://localhost:3000/api/emails/temp-abc123@$DOMAIN/eml_xxx/otp" | jqThe MCP server exposes 5 tools for AI agents via stdio transport:
| Tool | Description |
|---|---|
temp_email_generate |
Generate a new temporary email address |
temp_email_list |
List emails for an address |
temp_email_read |
Read a specific email by ID |
temp_email_extract_otp |
Extract OTP/verification code from an email |
temp_email_extract_links |
Extract all links from an email |
Configuration and workflow documentation:
→ docs/hermes-integration.md — Hermes MCP configuration → docs/chrome-devtools-mcp.md — Browser automation with Chrome DevTools MCP → docs/workflow-examples.md — Copy-paste ready prompts
Quick test:
DOMAIN=mail.example.com API_KEY=your-key npx tsx src/infrastructure/mcp/start.tsTwo independent inbound paths. App reads local SQLite first, then optionally pulls from the Worker.
Internet mail
/ \
v v
┌──────────────────┐ ┌────────────────────────────┐
│ Host SMTP :25 │ │ CF Email Routing (zone) │
│ smtp-server │ │ → Worker freelancer-mail │
│ → local SQLite │ │ → D1 freelancer-mail-db │
└────────┬─────────┘ └─────────────┬──────────────┘
│ │
│ GET /emails/:addr (x-api-key)
│ │
v v
┌────────────────────────────────────────────┐
│ Next.js app (UI + REST + SSE) │
│ getEmailsByAddress: local first, then │
│ CLOUDFLARE_WORKER_URL if set │
│ OTP/link extraction happens HERE only │
└──────────────────┬─────────────────────────┘
│
v
MCP stdio (npm run mcp)
Domain is configurable: set app DOMAIN + Cloudflare Email Routing on that zone. Worker name is not the email domain.
Stack: Next.js 14 + TypeScript + Tailwind CSS + shadcn/ui + better-sqlite3 + smtp-server + mailparser + Cloudflare Workers/D1
Separate package: cloudflare-email-worker/.
| Piece | Value |
|---|---|
| Worker | freelancer-mail |
| D1 | freelancer-mail-db |
| Deploy | cd cloudflare-email-worker && npx wrangler deploy (or ./deploy-fresh.sh) |
| App wire | CLOUDFLARE_WORKER_URL in .env + same API_KEY as Worker secret |
| Dashboard | Email Routing catch-all → Send to Worker freelancer-mail |
Details: cloudflare-email-worker/README.md, docs/deployment.md, AGENTS.md.
- docs/README.md — Doc index
- docs/architecture.md — Dual ingest + components
- docs/api.md — REST + Worker HTTP
- SECURITY.md — Reporting + secrets policy
- AGENTS.md — AI / agent ground truth
- openspec/ — Spec-driven requirements (
openspec/specs/) - docs/dns-setup.md — MX, SPF, DMARC
- docs/deployment.md — Production, Docker, Worker
- docs/troubleshooting.md — Common issues
- docs/hermes-integration.md — Hermes MCP
- docs/chrome-devtools-mcp.md — Browser automation
- docs/workflow-examples.md — End-to-end workflows
MIT