A tiny, single-binary SMTP & SMS catcher for development environments — like Mailtrap or Mailpit, but written in Rust with a minimal footprint (~15MB image), structured as a monorepo, and built for agentic development: AI agents (and e2e tests) can discover, wait for, and extract data from captured emails and SMS messages via a REST API or an MCP server.
Tags: mail • SMTP • SMS • mail-catcher • sms-catcher • testing • e2e • agents • MCP • Docker • Rust • email • development
crates/core/ pinemail-core — shared SQLite store, MIME parsing, HTML/spam analysis, signal extraction
crates/server/ pinemail — SMTP (1025) + HTTP API/UI (8025); what you deploy
crates/mcp/ pinemail-mcp — MCP stdio server exposing emails & SMS to AI agents
apps/web/ — React + Vite + TS frontend embedded into `pinemail`
For full system architecture, sequence diagrams, database schemas, and design details, see ARCHITECTURE.md.
For runtime integration details for AI agents and test suites, see AGENTS.md.
| Feature | Pine Mail 🌲 | Mailhog | Mailpit | Mailtrap (SaaS) |
|---|---|---|---|---|
| Single Static Binary | ✅ (Rust, ~15MB) | ❌ (Go + deps) | ✅ (Go) | ❌ (Cloud SaaS only) |
| Dual Email & SMS Catcher | ✅ Built-in | ❌ Email only | ❌ Email only | 🟡 Partial |
| Twilio Webhook Ingestion | ✅ Built-in | ❌ | ❌ | ❌ |
| Auto Signal Extraction (OTP / Links) | ✅ Built-in | ❌ | ❌ | ❌ |
Long-Polling Wait API (/api/wait & /api/sms/wait) |
✅ Built-in | ❌ | 🟡 Partial | ❌ |
| Litmus-Style Email Analysis | ✅ Built-in | ❌ | ❌ | ❌ |
| 14 MCP Tools for AI Agents | ✅ Built-in | ❌ | ❌ | ❌ |
| Free & Open Source | ✅ MIT | ✅ | ✅ | ❌ |
- SMTP Email Catcher (
:1025): dead-end local SMTP listener (no auth, no relaying, no external network calls). - SMS Catcher & Twilio Webhook Support (
:8025): ingest SMS via standard JSON (POST /api/sms) or Twilio webhooks (POST /api/sms/webhookwithx-www-form-urlencodedpayloads). - Dual-Tab Web UI & REST API (
:8025): clean React interface for viewing both Emails and SMS messages with live WebSocket updates. - Rich Message Inspection: view HTML/plain-text bodies, raw headers, download attachments or raw
.emlfiles. - Litmus-Style Email Analysis:
GET /api/messages/:id/analysischecks 11 HTML email-client compatibility factors (DOCTYPE, tables, inline CSS, fonts, image alt, size clipping) and provides a SpamAssassin-style heuristic spam score. - Search, Pagination & Bulk Actions: multi-select grid with bulk mark read/unread, bulk delete, and paginated lazy-loading (50 items per page).
- Agentic Long-Polling:
GET /api/waitandGET /api/sms/waitlong-poll server-side for incoming emails or SMS matching filters (to,from,subject,body,since). - Signal Extraction Engine:
GET /api/messages/:id/extractandGET /api/sms/:id/extractautomatically pull OTP codes (4–8 digits) and magic links out of captured emails and SMS bodies. - 14 MCP Agent Tools (
pinemail-mcp): Model Context Protocol stdio server exposing 7 Email tools and 7 SMS tools directly to Claude Desktop, Copilot, Cursor, and autonomous test runners. - SQLite Storage: persistent SQLite storage (
/data/pinemail.dbor:memory:) with automatic FIFO pruning pastMAX_MESSAGES. - Zero External Runtime Dependencies: single binary with embedded frontend built via
rust-embed.
Pine Mail provides official distribution packages and single static binaries for macOS, Linux, and Windows.
Install Pine Mail (both pinemail server binary and pinemail-mcp AI agent tool) via Homebrew from yoosuf/homebrew-tap:
# Add the Homebrew tap and install
brew tap yoosuf/tap
brew install pinemail
# Or install directly in a single command:
brew install yoosuf/tap/pinemailManage as a background service (macOS & Linux):
# Start Pine Mail as a background service (SMTP on :1025, Web UI & API on :8025)
brew services start pinemail
# Check service status, stop, or restart
brew services info pinemail
brew services stop pinemail
brew services restart pinemailAuto-detects OS and CPU architecture, downloads the latest binary release, and places executables in PATH.
macOS & Linux (POSIX Shell):
curl -fsSL https://raw.githubusercontent.com/yoosuf/pinemail/main/install.sh | shWindows (PowerShell):
iwr -useb https://raw.githubusercontent.com/yoosuf/pinemail/main/install.ps1 | iex- Debian / Ubuntu (
.deb):curl -LO https://github.com/yoosuf/pinemail/releases/latest/download/pinemail_amd64.deb sudo dpkg -i pinemail_amd64.deb
- Fedora / RHEL / CentOS (
.rpm):sudo rpm -i https://github.com/yoosuf/pinemail/releases/latest/download/pinemail.x86_64.rpm
- Arch Linux (AUR):
yay -S pinemail-bin
- Scoop:
scoop bucket add pinemail https://github.com/yoosuf/scoop-bucket scoop install pinemail
- Winget (Windows Package Manager):
winget install PineMail.PineMail
- Chocolatey:
choco install pinemail
Download standalone pre-built binaries from GitHub Releases:
| Operating System | Architecture | Package File |
|---|---|---|
| macOS | Apple Silicon (aarch64) |
pinemail-v*-aarch64-apple-darwin.tar.gz |
| macOS | Intel (x86_64) |
pinemail-v*-x86_64-apple-darwin.tar.gz |
| Linux | x86_64 | pinemail-v*-x86_64-unknown-linux-gnu.tar.gz |
| Linux | ARM64 (aarch64) |
pinemail-v*-aarch64-unknown-linux-gnu.tar.gz |
| Windows | x86_64 | pinemail-v*-x86_64-pc-windows-msvc.zip |
Install directly via cargo:
cargo install --git https://github.com/yoosuf/pinemail pinemail-server pinemail-mcpPull the published multi-arch image (linux/amd64 + linux/arm64) from Docker Hub:
docker run -d --name pinemail -p 1025:1025 -p 8025:8025 -v pinemail-data:/data yoosuf/pinemail:latestOr run via Docker Compose:
docker compose up --buildTo run the MCP server against it:
docker compose --profile mcp run --rm mcpDocker Hub: yoosuf/pinemail — tags latest and 0.1.0.
# 1. Long-poll for a fresh email matching criteria (up to 10s default, max 60s)
curl "http://localhost:8025/api/wait?to=user@example.com&subject=Verify&since=2026-09-04T09:00:00Z"
# 2. Extract OTP codes and links from the received email
curl "http://localhost:8025/api/messages/<MESSAGE_ID>/extract"
# Response: { "codes": ["482913"], "links": ["https://app.test/verify?token=..."] }
# 3. Analyze HTML compatibility & spam score
curl "http://localhost:8025/api/messages/<MESSAGE_ID>/analysis"
# 4. Long-poll for a fresh SMS message
curl "http://localhost:8025/api/sms/wait?to=+15550100&since=2026-09-04T09:00:00Z"
# 5. Extract OTP codes from the received SMS
curl "http://localhost:8025/api/sms/<SMS_ID>/extract"
# Response: { "codes": ["940182"], "links": [] }Point your application or local webhook relay to:
POST http://localhost:8025/api/sms/webhook
Content-Type: application/x-www-form-urlencoded
From=%2B15550199&To=%2B15550100&Body=Your+verification+code+is+839201Using Node 18+ native fetch to wait for messages and extract OTP verification codes:
// 1. Capture ISO timestamp BEFORE triggering signup / SMS 2FA action
const since = new Date().toISOString();
const userPhone = "+15550199";
// 2. Trigger your application action (e.g. request 2FA SMS code)
await triggerSmsCodeAction({ to: userPhone });
// 3. Long-poll Pine Mail server-side until SMS arrives (blocks up to timeout_ms)
const waitRes = await fetch(
`http://localhost:8025/api/sms/wait?to=${encodeURIComponent(userPhone)}&since=${since}&timeout_ms=10000`
);
const sms = await waitRes.json();
// 4. Extract 4-8 digit OTP verification code
const extractRes = await fetch(`http://localhost:8025/api/sms/${sms.id}/extract`);
const { codes } = await extractRes.json();
const otpCode = codes[0]; // e.g. "839201"
// 5. Submit extracted OTP code into your test runner / app
await submitOtpCode(otpCode);| Method | Endpoint | Description |
|---|---|---|
GET |
/api/messages |
List captured emails (search, limit, offset) |
GET |
/api/messages/:id |
Fetch full email details (headers, text/html, attachments) |
GET |
/api/messages/:id/raw |
Download raw MIME file (.eml) |
GET |
/api/messages/:id/html |
Render raw HTML body |
GET |
/api/messages/:id/attachments/:index |
Download attachment by index |
GET |
/api/messages/:id/extract |
Extract OTP codes (4-8 digits) and HTTP/HTTPS links |
GET |
/api/messages/:id/analysis |
HTML email compatibility checks & heuristic spam score |
GET |
/api/wait |
Server-side long-polling wait (to, from, subject, since, timeout_ms) |
PATCH |
/api/messages/:id/read |
Toggle read/unread state |
DELETE |
/api/messages/:id |
Delete email by ID |
POST |
/api/messages/bulk-delete |
Bulk delete emails by ID list |
PATCH |
/api/messages/bulk-read |
Bulk update read state |
DELETE |
/api/messages |
Clear all emails |
POST |
/api/test-email |
Inject synthetic test email |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/sms |
List captured SMS messages (search, limit, offset) |
GET |
/api/sms/:id |
Fetch single SMS details |
POST |
/api/sms |
Ingest SMS via JSON payload |
POST |
/api/sms/webhook |
Ingest SMS via Twilio form-urlencoded or JSON webhook |
GET |
/api/sms/wait |
Server-side long-polling wait for SMS (to, from, body, since, timeout_ms) |
GET |
/api/sms/:id/extract |
Extract OTP codes (4-8 digits) and HTTP/HTTPS links from SMS |
PATCH |
/api/sms/:id/read |
Toggle read/unread state |
DELETE |
/api/sms/:id |
Delete SMS by ID |
POST |
/api/sms/bulk-delete |
Bulk delete SMS by ID list |
PATCH |
/api/sms/bulk-read |
Bulk update SMS read state |
DELETE |
/api/sms |
Clear all SMS messages |
POST |
/api/test-sms |
Inject synthetic test SMS message |
Pine Mail includes 14 built-in MCP tools for AI agents (Claude Desktop, Copilot, Cursor, agentic E2E tests):
| Category | Tool Name | Parameters | Description |
|---|---|---|---|
| Email Tools | list_emails |
search?, limit?, offset? |
Query emails with search filter and pagination |
get_email |
id |
Get email headers, text/html content, and attachment metadata | |
wait_for_email |
to?, from?, subject?, since_ms?, timeout_ms? |
Long-poll server-side until matching email arrives | |
extract_signals |
id |
Automatically extract OTP codes and links from email | |
send_test_email |
to? |
Inject synthetic test email | |
delete_email |
id |
Delete email by ID | |
clear_inbox |
none | Clear all emails | |
| SMS Tools | list_sms |
search?, limit?, offset? |
Query captured SMS with search filter and pagination |
get_sms |
id |
Get single SMS details | |
wait_for_sms |
to?, from?, body?, since_ms?, timeout_ms? |
Long-poll server-side until matching SMS arrives | |
extract_sms_signals |
id |
Automatically extract OTP codes and links from SMS body | |
send_test_sms |
to?, from?, body? |
Inject synthetic test SMS | |
delete_sms |
id |
Delete SMS by ID | |
clear_sms_inbox |
none | Clear all SMS messages |
| Variable | Default | Description |
|---|---|---|
SMTP_PORT |
1025 |
Port the SMTP listener binds to |
HTTP_PORT |
8025 |
Port the web UI / REST API binds to |
BIND_ADDR |
0.0.0.0 |
Bind address for both listeners |
DB_PATH |
pinemail.db (/data/pinemail.db in Docker) |
SQLite database file path (:memory: for ephemeral in-memory DB) |
MAX_MESSAGES |
1000 |
Oldest messages/SMS are pruned past this count (0 = unlimited) |
SMTP_HOSTNAME |
pinemail |
Hostname advertised in the SMTP banner |
PINEMAIL_URL |
http://127.0.0.1:8025 |
(pinemail-mcp only) Base HTTP URL of the running server |
# Terminal 1 — Frontend hot reload (Vite dev server proxied to Rust API)
cd apps/web && npm install && npm run dev
# Terminal 2 — Backend server (SMTP on :1025, API on :8025)
cargo run -p pinemail-servercd apps/web && npm install && npm run build && cd ..
cargo build --release --workspaceThe frontend static assets must be built to apps/web/dist before building pinemail-server, as they are embedded directly into the binary using rust-embed.
Built by Yoosuf, who also offers fractional CTO services.