Local auth testing without the flaky inbox dance
Pine Mail catches email OTPs, magic links, password resets, and SMS 2FA codes for local development, Playwright tests, CI pipelines, and AI agents. One tiny Rust binary — long-polling waits, Twilio webhooks, and structured signal extraction.
- SMTP mail catcher + SMS catcher
- OTP and magic-link extraction
- 14 native MCP tools for agents
$ docker run -d --name pinemail \ -p 1025:1025 -p 8025:8025 yoosuf/pinemail:latest $ curl "http://localhost:8025/api/[email protected]&timeout_ms=10000" { "id": "msg_01J6XYZ", "subject": "Confirm your account", "codes": ["849201"] } $ curl -X POST http://localhost:8025/api/sms/webhook \ -d "From=%2B15550199&To=%2B15550100&Body=Your code is 839201" ✓ sms_01A2B3 captured in 3ms
Built for agents & CI-first development
Why developers switch to Pine Mail
It replaces the messy parts of auth testing with one local tool: email testing, SMS 2FA testing, webhook capture, OTP extraction, and agent-readable APIs in the same process.
-
For local dev
A MailHog and Mailpit alternative that understands modern auth
Catch SMTP like the classic tools, then go further: parse verification codes, magic links, reset URLs, and SMS messages without asking your app or your browser to pretend.
-
For E2E and CI
Deterministic Playwright auth tests with zero
sleep()Long-polling waits return when the matching email or text arrives. Use
since, assert the extracted code, clean the inbox, and keep the pipeline boring. -
For AI agents
MCP-native email and SMS tools for full MFA flows
Agents can wait for email, extract a link, wait for SMS, read the OTP, and finish login through 14 structured MCP tools instead of screen-scraping webmail.
The problem: auth testing is still too fragile
Your test suite signs up a user. The app sends an email verification code, then a text-message 2FA code. The whole thing should take seconds, but it often turns into waits, retries, inbox scraping, and one very suspicious timeout.
-
Cloud sandboxes add coupling
Mailtrap, SendGrid, and phone-verification SaaS reduce the blast radius, but your local suite still depends on the network, API keys, account state, and a pricing tier.
-
Classic catchers are email-only
MailHog and Mailpit are great SMTP catchers, but they stop at email. Modern auth testing needs SMS, OTP extraction, magic links, and clean wait APIs too.
-
Agents hit a wall at OTPs
An autonomous agent can't casually read a phone or browse webmail like a patient human. It needs a native, structured channel for email and SMS.
How it works
One binary, two channels, one long-polling engine. Point your app at the listeners — your tests and agents just wait and read.
Your app
-
SMTP · :1025email triggers -
Twilio → /api/sms/webhookform + JSON parsing -
REST · POST /api/smsdirect push
Pine Mail
- Email store MIME & HTML · auto signal extraction
- SMS store JSON & Twilio · auto signal extraction
- SMTP 0421 · received
- extract → OTP 812904
- analysis · spam 0.02
- delivered → agent 7712
- mailbox idle · 0 waiting
Tests & agents
-
14 MCP toolsnative stdio server -
/api/wait · /api/sms/waitlong-poll, zero sleep() -
Web UI · :8025dual-tab dashboard
- 1
Point your app at Pine Mail
SMTP relay at
localhost:1025; Twilio webhook at/api/sms/webhook. - 2
It catches and parses everything
Stores messages in SQLite and extracts codes, magic links, and reset URLs automatically.
- 3
Your test or agent waits, then reads
Long-poll
/api/waitor/api/sms/wait, pull the OTP, keep going. Zero sleep().
Everything you need, nothing you don't
Four capabilities that make Pine Mail feel boring in the best developer-product sense: predictable, local, scriptable, and easy to delete from your mental stack.
Long-polling wait API
/api/wait and /api/sms/wait hold the request open until the matching message arrives. Use since timestamps so fast delivery never slips through the gap.
Signal extraction
Pull OTP codes, email verification links, magic login links, and reset URLs out of email bodies and SMS text. Structured JSON — no regex, no MIME debugging.
SMS + Twilio webhooks
Catch texts via JSON or a native Twilio webhook parser. Full SMS 2FA flows locally — no phone, no SMS gateway in your test path.
Email analysis
Run an 11-factor HTML email compatibility check plus a SpamAssassin-style score. Assert template quality before a broken transactional email ships.
Developer guide
Plain HTTP with JSON responses on localhost:8025 . No SDK, no auth, no API keys — just curl, or whatever your test suite already speaks.
Conventions
-
Long-polling waits
/api/waitand/api/sms/waithold the request open and resolve the instant a matching message lands. On timeout they return a clean408— no hangs, no polling loops. -
Race-free
sinceBoth wait endpoints accept an ISO-8601
sincetimestamp. Capture it before triggering the action, and a message that lands before your fetch connects still gets returned (received_at >= since). -
Error codes
400for invalid parameters,404for unknown message IDs,408on wait timeout.
GET /api/wait long-poll
Wait for an email matching the filters, then return it.
curl "http://localhost:8025/api/[email protected]&subject=Verify&since=2026-09-04T10:00:00Z&timeout_ms=15000"
| Query param | Type | Required | Description |
|---|---|---|---|
to | string | no | Filter by recipient address. |
subject | string | no | Filter by subject substring. |
since | ISO-8601 | no | Only messages with received_at >= since . |
timeout_ms | int | no | Max wait before returning 408 . |
Response — the full message payload
{
"id": "msg_01J6XYZ",
"from": "[email protected]",
"to": "[email protected]",
"subject": "Confirm your account",
"text": "Your verification code is 849201",
"html": "<html><body>...</body></html>",
"received_at": "2026-09-04T10:00:01.123Z"
}
GET /api/messages/:id fetch
Fetch a single stored email by ID, including the raw HTML body.
curl "http://localhost:8025/api/messages/msg_01J6XYZ"
GET /api/messages/:id/extract signals
Pull verification codes, magic login links, and password reset URLs into structured JSON. Recognizes 4–8 digit numeric and alphanumeric OTPs.
curl "http://localhost:8025/api/messages/msg_01J6XYZ/extract"
{
"codes": ["849201"],
"links": ["http://localhost:3000/auth/verify?token=d8f1e09a8b2c4d5e"],
"action_urls": ["http://localhost:3000/auth/verify?token=d8f1e09a8b2c4d5e"]
}
GET /api/messages/:id/analysis CI assertions
11-factor compatibility check for legacy clients plus a heuristic spam score. Deterministic enough to assert against in CI before merge.
curl "http://localhost:8025/api/messages/msg_01J6XYZ/analysis"
{
"spam_score": 12,
"issues": ["missing doctype", "un-inlined styles"]
}
Cleanup: POST /api/messages/bulk-delete and PATCH /api/messages/bulk-read reset or mark inbox state between test runs.
SMS
POST /api/sms ingest
Push a structured SMS straight from a test driver or notification service.
curl -X POST "http://localhost:8025/api/sms" \
-H "Content-Type: application/json" \
-d '{"from": "+15550199", "to": "+15550100", "body": "Your code is 839201"}'
POST /api/sms/webhook Twilio
Point your app's Twilio client or webhook dispatcher here. Standard x-www-form-urlencoded fields ( From , To , Body ) are parsed automatically.
curl -X POST "http://localhost:8025/api/sms/webhook" \ -d "From=%2B15550199" -d "To=%2B15550100" \ -d "Body=Your verification code is 839201"
GET /api/sms/wait long-poll
Long-poll for an SMS sent to a number. Same since and timeout_ms semantics as email.
curl "http://localhost:8025/api/sms/wait?to=%2B15550100&since=2026-09-04T10:00:00Z&timeout_ms=10000"
GET /api/sms/:id/extract signals
Extract the OTP and any embedded URLs from an SMS body.
curl "http://localhost:8025/api/sms/sms_01A2B3/extract"
{ "codes": ["839201"], "links": [] }
Cleanup: POST /api/sms/bulk-delete and PATCH /api/sms/bulk-read .
Pine Mail vs. MailHog, Mailpit, and Mailtrap
Honest comparison. MailHog and Mailpit are still useful. Mailtrap is polished. Pine Mail is built for the local auth-testing jobs they were never quite designed to own.
| Feature | Pine Mail | MailHog | Mailpit | Mailtrap |
|---|---|---|---|---|
| Email catching | ✓ | ✓ | ✓ | ∼ |
| SMS catching + Twilio webhooks | ✓ | ✗ | ✗ | ∼ |
Long-polling wait API ( /api/wait , /api/sms/wait ) | ✓ | ✗ | ∼ | ✗ |
| Signal extraction (OTP / magic links) | ✓ | ✗ | ✗ | ✗ |
| Litmus-style email analysis | ✓ | ✗ | ✗ | ✗ |
| 14 MCP tools for AI agents | ✓ | ✗ | ✗ | ✗ |
| Single static binary (<15 MB) | ✓ | ✗ | ✓ | ✗ |
| Free & open source (MIT) | ✓ | ✓ | ✓ | ✗ |
Running locally in under 30 seconds
Same binary, several install paths. Start with Docker, Homebrew, Cargo, or the one-liner and point your app at localhost:1025 .
docker run -d --name pinemail \ -p 1025:1025 -p 8025:8025 \ yoosuf/pinemail:latest
Pulls the image from Docker Hub and starts in one shot — SMTP on 1025 , dashboard on 8025 .
brew tap yoosuf/tap brew install pinemail
Runs as a service with brew services start pinemail — no container runtime needed.
curl -fsSL https://raw.githubusercontent.com/\ yoosuf/pinemail/main/install.sh | sh
Installs the server and MCP binary to ~/.local/bin — no root, no dependencies.
cargo install --git \ https://github.com/yoosuf/pinemail \ pinemail-server pinemail-mcp
Builds pinemail-server and pinemail-mcp from source; requires a Rust toolchain.
Built for AI agents
A native Model Context Protocol (MCP) server exposes the whole local inbox as 14 tools — 7 for email, 7 for SMS. An agent can complete signup, email verification, password reset, and SMS 2FA without touching a browser inbox.
{
"mcpServers": {
"pinemail": {
"command": "pinemail-mcp",
"env": {
"PINEMAIL_URL": "http://localhost:8025"
}
}
}
}
Email tools
-
wait_for_email
Long-poll until a matching email arrives.
-
extract_signals
Pull OTP codes and URLs from an email.
-
list_emails
Query emails with search & pagination.
-
get_email
Full HTML, text, and attachment data.
-
send_test_email
Inject a synthetic test email.
-
delete_email
Delete a specific email by ID.
-
clear_inbox
Reset email state between runs.
SMS tools
-
wait_for_sms
Long-poll until a matching SMS arrives.
-
extract_sms_signals
Pull OTP codes and links from an SMS.
-
list_sms
Query captured SMS with pagination.
-
get_sms
Get a single SMS message details.
-
send_test_sms
Inject a synthetic test SMS.
-
delete_sms
Delete a specific SMS by ID.
-
clear_sms_inbox
Reset SMS state between runs.
One prompt, a full multi-factor flow
In your test suite
Deterministic, race-free, and fast. If delivery fails, the test fails on a clean timeout — it never hangs and never waits on someone else's API.
const since = new Date().toISOString(); // capture BEFORE triggering
await fetch("http://localhost:3000/api/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "[email protected]", password: "..." }),
});
const email = await (await fetch(
`http://localhost:8025/api/[email protected]&since=${since}&timeout_ms=10000`
)).json();
const { codes } = await (await fetch(
`http://localhost:8025/api/messages/${email.id}/extract`
)).json();
await fetch("http://localhost:3000/api/verify-email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "[email protected]", code: codes[0] }),
});
console.log("verified with code", codes[0]);
-
<15 MB
Single binary
-
<5 ms
Startup time
-
2
Channels: email + SMS
-
14
MCP tools for AI agents
Frequently asked questions
Does Pine Mail need Docker, Mailtrap, or a cloud account?
Neither. It's a single static binary with embedded SQLite and an embedded web UI. Docker is one convenient install option among several — Homebrew, one-liners, and Cargo all work too.
Is Pine Mail a MailHog or Mailpit alternative?
Yes, if you want a local SMTP mail catcher with a sharper testing API. Pine Mail catches email like those tools, but also adds SMS catching, Twilio webhook parsing, OTP and magic-link extraction, race-free long-polling waits, email analysis, and MCP tools for agents.
How does SMS catching actually work?
Two ways: POST structured JSON to /api/sms , or point your app's Twilio client at /api/sms/webhook — Pine Mail parses standard form payloads (From/To/Body) automatically. Then long-poll /api/sms/wait and extract OTPs via /api/sms/:id/extract .
Can AI agents really use this?
Yes. The pinemail-mcp server exposes 7 email and 7 SMS tools over Model Context Protocol. Configure it once in Claude Desktop, Cursor, Windsurf, or Claude Code, and agents can wait, extract, and assert — all natively.
Is it safe to run in CI?
Yes. Pass --memory for an in-memory database, long-poll with a timeout (clean 408 on miss), and use the bulk read/delete endpoints to reset between tests. No temp files left on disk.
Will it accidentally send real emails or relay spam?
No. It's a dead-end local SMTP listener with no auth, no relaying, and no external calls. That's exactly what makes it safe for test environments.
Make auth testing boring again.
Pine Mail catches email OTPs, magic links, password resets, and SMS 2FA codes — free, open source under the MIT license, running locally in under 30 seconds.
Let's build something that scales.
Planning an AI product, automating a workflow, or taking a platform to production? I'd love to hear what you're building.