The Problem
AI agents need to send and receive email programmatically, but email infrastructure is fragmented: sending requires provider SDKs, receiving requires polling or webhooks, and extracting verification codes from HTML emails is bespoke work. mails wraps send, receive, search, and code extraction into one CLI and SDK with a pluggable provider chain.
What This Does
The repo is a TypeScript monorepo with two projects: src/ (CLI + SDK) and worker/ (Cloudflare Worker for receiving and sending). The CLI handles send, inbox, code, claim, and sync commands; the worker handles incoming email via Cloudflare Email Routing and exposes /api/send and /api/inbox endpoints.
Storage is provider-based: local SQLite (src/providers/storage/sqlite.ts), cloud PostgreSQL via db9.ai (src/providers/storage/db9.ts), or a remote Worker API (src/providers/storage/remote.ts). Sending uses a chain of Cloudflare Email Service then Resend, configurable via EMAIL_PROVIDERS.
How It Is Wired
Execution starts at main in src/cli/index.ts, which reaches 80 functions. The call graph shows loadConfig is called from 11 places — the widest blast radius — followed by fetch (9) and esc (8). src/core/types.ts is a hub with 28 modules importing it; changes there ripple broadly.
A typical inbox command traces: main -> inboxCommand -> getEmail, which hits the database via db.prepare('SELECT * FROM emails WHERE id = ?') — two hops to a DB effect. The worker's fetch handler in worker/src/index.ts routes to handleInbox, writing via env.DB.prepare(...). The worker also owns email, handleGetCode, and handleSync, and is the only file making outbound network calls.
The worker/src/index.ts file is the operational core: 19 functions, called from 7 files. Storage providers are split by responsibility — db9.ts owns column handling and SQL escaping, sqlite.ts owns local persistence, remote.ts owns the API client.
How To Use It
Setup: npm install -g mails or bun install -g mails.
Configuration: Copy .env.example to .env for local storage. For the worker, copy worker/.dev.vars.example to worker/.dev.vars and set EMAIL_PROVIDERS, API keys, and mailbox auth tokens. The CLI stores config in a user-level file managed by src/core/config.ts.
Running it: The CLI entry point is src/cli/index.ts, invoked as mails send --to user@example.com --subject "..." or mails inbox. The worker runs via wrangler dev in the worker/ directory, with worker/wrangler.toml defining routes and bindings.
Real-World Use
A support agent that watches a shared inbox: deploy the worker with Cloudflare Email Routing, run mails sync to pull new messages into local SQLite, extract verification codes with mails code, and auto-reply via mails send. The provider field in responses tells you whether Cloudflare or Resend delivered each message.
Code Health & Issues
Static analysis found 16 issues (3 high, 13 medium). High severity: hub modules in src/core/types.ts (28 dependents) and src/core/config.ts (19 dependents) — churn there is high-blast-radius; deep nesting in src/cli/commands/help.ts (indentation depth 10); and duplicated code blocks — 76 repeated 6-line blocks across 28 files.
Medium: high branching density in src/providers/storage/remote.ts, sqlite.ts, and core/storage.ts (59 branch points over 142 lines), plus an oversized test file test/unit/worker.test.ts at 735 lines.
Beyond the measured findings: no LICENSE file (default all-rights-reserved), wildcard CORS origin in worker/src/index.ts (Access-Control-Allow-Origin: '*'), unpinned GitHub Actions in .github/workflows/release.yml, continue-on-error on a correctness step, no Dependabot, and no dependency vulnerability scan in CI. The repo has a bun.lock but no npm lockfile.
The Bottom Line
The architecture is sound — clean provider abstraction, zero runtime dependencies, and a clear separation between CLI, worker, and storage. The main risks are operational: unpinned CI actions, wildcard CORS, and no license. Worth using for agent email workflows, but fix the security items before production deployment.