smtp.dev

Email Testing for Developers

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

Check what arrived in the UI or wire it into CI.

Delivery checks

HTML preview, text, headers, raw source. Plus SPF/DKIM/DMARC results via API.

CI-ready

Standard SMTP — any CI that sends email works. GitHub Actions, Jenkins, GitLab CI, whatever you use.

Deliverability audit

SPF/DKIM/DMARC results, Message-ID, size, and Authentication-Results checks. Grouped by severity.

Quickstart

Send, read, assert

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()

Read the CI guide

What you can test

Each card links to a guide.

CI assertions

Send from the test suite, poll the API, assert on subject and body.

E2E flows

The Playwright test clicks the real reset link from the real email.

SPF, DKIM, DMARC

Authentication verdicts from staging before you touch production DNS.

QA signups

A catch-all account hands every test run a fresh address on your domain.

Client previews planned

The same message in Thunderbird and Apple Mail, over plain IMAP.

AI agents

Agents read OTP codes and confirmation links over the API. In dev, not in your inbox.

Features

Everything the sandbox ships with.

Custom Domains

Bring your own domains and subdomains. One MX record each.

Disposable Accounts

Create an address per test run over the API. Delete it after.

Unlimited Messages

No daily caps, no metering.

Sandboxed

Receives real email from any service. Sends only within the sandbox.

SPF, DKIM & DMARC

Checked on every inbound message. Results in the UI and the API.

Retention Timers

Messages kept for your plan's window, up to 90 days. Per-message expiry for shorter.

REST API

Domains, accounts, mailboxes, messages, tokens. OpenAPI spec included.

Real-time Updates

New messages pushed via SSE. No polling.

Catch-All & Aliasing

Wildcard accounts and plus-sign addressing. Capture everything or route by tag.

SMTP, IMAP, POP3 planned

Your existing mail clients work out of the box. No vendor SDK, no lock-in.

Connection settings

Local development endpoints

The Flask web app stays on port 8004 and the inbound SMTP listener stays on port 2525.

ProtocolHostPortSecurity
SMTP inlocalhost2525Local plain SMTP
REST APIlocalhost8004X-API-KEY header

Start testing

Sign up, add a domain, receive your first test email.

Local-first

Self-hosted email testing.

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.