Delivery checks
HTML preview, text, headers, raw source. Plus SPF/DKIM/DMARC results via API.
Test emails like you debug code. SMTP in, API out. Inspect headers, MIME and raw source. Nothing leaves the sandbox.
# create an inbox
curl -X POST http://localhost:8004/api/accounts \
-H "X-API-KEY: *** \
-H "Content-Type: application/json" \
-d '{"address": "qa@example.test", "password": "use-a-secret"}'
# your app sends to it, then read what arrived
curl "http://localhost:8004/api/accounts/1/mailboxes/1/messages" \
-H "X-API-KEY: ***
After delivery
HTML preview, text, headers, raw source. Plus SPF/DKIM/DMARC results via API.
Standard SMTP — any CI that sends email works. GitHub Actions, Jenkins, GitLab CI, whatever you use.
SPF/DKIM/DMARC results, Message-ID, size, and Authentication-Results checks. Grouped by severity.
Quickstart
Your app sends over SMTP. Your test reads the result over the API. The whole loop fits on one screen.
# 1. Register in the browser, then sign in:
# http://localhost:8004/register
# 2. Create a token from the web session.
curl -X POST http://localhost:8004/api/tokens \
-H "Content-Type: application/json" \
-d '{"name":"quickstart"}'
export SMTPDEV_API_KEY="paste-token"
# 3. Add a domain and create an account.
curl -X POST http://localhost:8004/api/domains \
-H "X-API-KEY: *** \
-H "Content-Type: application/json" \
-d '{"domain":"dev.example.test","mx_verified":true}'
curl -X POST http://localhost:8004/api/accounts \
-H "X-API-KEY: *** \
-H "Content-Type: application/json" \
-d '{"domain_id":1,"address":"qa@dev.example.test","password":"account-password"}'
# 4. Send through local SMTP on port 2525.
printf "Subject: Hello\nFrom: app@example.test\nTo: qa@dev.example.test\n\nIt works." | \
nc localhost 2525
# 5. List messages from the Inbox mailbox.
curl -H "X-API-KEY: *** \
"http://localhost:8004/api/accounts/1/mailboxes/1/messages?page=1&limit=25"
import requests
import smtplib
from email.message import EmailMessage
base = "http://localhost:8004/api"
token = "paste-token-from-/tokens-or-POST-/api/tokens"
headers = {"X-API-KEY": token}
domain = requests.post(
f"{base}/domains",
headers={**headers, "Content-Type": "application/json"},
json={"domain": "dev.example.test", "mx_verified": True},
).json()
account = requests.post(
f"{base}/accounts",
headers={**headers, "Content-Type": "application/json"},
json={
"domain_id": domain["id"],
"address": "qa@dev.example.test",
"password": "account-password",
},
).json()
msg = EmailMessage()
msg["From"] = "app@example.test"
msg["To"] = account["address"]
msg["Subject"] = "Hello"
msg.set_content("It works.")
with smtplib.SMTP("localhost", 2525) as smtp:
smtp.send_message(msg)
mailboxes = requests.get(f"{base}/accounts/{account['id']}/mailboxes", headers=headers).json()
inbox = next(box for box in mailboxes["items"] if box["path"] == "Inbox")
messages = requests.get(
f"{base}/accounts/{account['id']}/mailboxes/{inbox['id']}/messages",
headers=headers,
params={"page": 1, "limit": 25},
).json()
What you can test
Send from the test suite, poll the API, assert on subject and body.
The Playwright test clicks the real reset link from the real email.
Authentication verdicts from staging before you touch production DNS.
A catch-all account hands every test run a fresh address on your domain.
The same message in Thunderbird and Apple Mail, over plain IMAP.
Agents read OTP codes and confirmation links over the API. In dev, not in your inbox.
Features
Bring your own domains and subdomains. One MX record each.
Create an address per test run over the API. Delete it after.
No daily caps, no metering.
Receives real email from any service. Sends only within the sandbox.
Checked on every inbound message. Results in the UI and the API.
Messages kept for your plan's window, up to 90 days. Per-message expiry for shorter.
Domains, accounts, mailboxes, messages, tokens. OpenAPI spec included.
New messages pushed via SSE. No polling.
Wildcard accounts and plus-sign addressing. Capture everything or route by tag.
Your existing mail clients work out of the box. No vendor SDK, no lock-in.
Connection settings
The Flask web app stays on port 8004 and the inbound SMTP listener stays on port 2525.
| Protocol | Host | Port | Security |
|---|---|---|---|
| SMTP in | localhost | 2525 | Local plain SMTP |
| REST API | localhost | 8004 | X-API-KEY header |
Start testing
Local-first
This clone has no billing, subscriptions, or hosted plan limits. Run it locally for development, or put it on your own VPS when your team needs a shared sandbox.