Testing email locally with a fake SMTP server
Point your app at a local SMTP catcher during development to see every message without risking real delivery. Setup, workflow and what it cannot test.

On this page(9 sections)
The fastest way to email a real customer by accident is to test against real SMTP from a laptop. The fastest way to develop email features is to never send real mail at all: point your app at a local SMTP catcher that accepts everything, delivers nothing and shows you every message in a browser.
What an SMTP catcher does
An SMTP catcher is a small server that speaks just enough SMTP to accept messages, stores them, and gives you a web interface or API to view them. Popular open source options include Mailpit and MailHog, and several frameworks ship with their own. They all work the same way:
- Your app connects to
localhoston the catcher's SMTP port. - The catcher accepts every recipient, never relays anything, and stores the message.
- You open the catcher's web UI and see the message as a recipient would, including HTML, plain text, headers and attachments.
Because nothing leaves your machine, you can use real-looking addresses, send hundreds of test messages, and iterate on templates without risk.
Setting one up
Most catchers run as a single binary or a container. With Docker, a typical setup for Mailpit looks like this:
# docker-compose.override.yml
services:
mailpit:
image: axllent/mailpit
ports:
- "1025:1025" # SMTP
- "8025:8025" # web UI
Then configure your application's development environment to send through it:
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_TLS=false
Open http://localhost:8025 and send something. Check the project's documentation for current ports and options, since defaults differ between tools.
If your app uses an HTTP email API
Many applications send through a provider's HTTP API rather than SMTP. You have three reasonable choices for local development:
- Abstract the sender. Put email sending behind an interface in your code with two implementations: the real API client and an SMTP (or file) sender for development. Configuration chooses which one runs.
- Use the provider's test mode if it offers one, accepting that messages still travel to an external service.
- Write messages to disk. A development sender that writes each message to a
.emlfile is crude but effective; most mail clients can open.emlfiles directly.
The abstraction approach is the most useful long term, because the same interface also makes automated tests easy.
What to look at in the catcher
Do not just glance at the rendered HTML. A good local review covers:
| Check | What you are looking for |
|---|---|
| HTML view | Layout, broken images, merge fields filled correctly |
| Plain-text view | Exists, readable, contains every key fact and link |
| Headers | Correct From, Reply-To, Subject, Message-ID, List-Unsubscribe where relevant |
| Source | Sensible MIME structure, UTF-8 declared, attachments in the right place |
| Links | Absolute HTTPS URLs pointing at the right environment |
| Size | Total message size, especially HTML size |
Several catchers also include HTML compatibility checks, link checking and spam-score style analysis. Treat those as hints: a local spam score has little to do with how Gmail or Outlook will treat the message, because real filtering depends heavily on your domain's reputation and authentication, which a local tool cannot see.
A development workflow that works
- Seed realistic data. Long names, non-ASCII characters, empty optional fields, many line items. Most template bugs show up only with awkward data.
- Trigger every email path from the UI or a script. A small developer-only page or command that sends each template with fixture data saves a lot of clicking.
- Review in the catcher. HTML, text and headers.
- Iterate. Edit the template, resend, refresh.
- Clear the inbox regularly so you are not confusing old messages with new ones.
Some teams add a preview route to the app that renders a template directly in the browser without sending it. That is great for fast visual iteration, but still send through the catcher before you consider a template done; previews skip MIME assembly, headers and encoding, which is where many real bugs live.
What local testing cannot tell you
A catcher accepts everything, which is exactly why it cannot test delivery. It tells you nothing about:
- Authentication. No SPF, DKIM or DMARC evaluation happens locally.
- Filtering and placement. Whether real receivers deliver to the inbox depends on reputation and content signals a catcher does not have.
- Client rendering. The catcher's browser view is a modern engine. Outlook for Windows, Gmail's sanitizer and dark mode will render differently.
- Real SMTP failures. Rejections, deferrals, greylisting and TLS issues never occur.
- Provider API behavior. Rate limits, idempotency and webhooks only appear against the real service.
Cover those gaps elsewhere: a staging environment with a real sending domain and allowlisted recipients, a few real test accounts at major providers, and rendering tests across clients for your shared layout.
Guardrails worth adding
Local catchers make development safe only if the app cannot accidentally use real credentials. A few cheap protections:
- Keep production SMTP credentials and API keys out of development environment files entirely.
- Fail fast at startup if the environment is
developmentand the configured SMTP host is notlocalhostor the catcher's container name. - Make the catcher part of the default developer setup, so nobody has to opt in.
Checklist
- A local SMTP catcher runs as part of the standard development stack.
- Development configuration points all mail at it, with no real credentials present.
- HTTP API senders sit behind an interface with a local implementation.
- Every template can be triggered quickly with realistic fixture data.
- Reviews cover HTML, plain text, headers and source.
- Delivery, authentication and client rendering are tested elsewhere.
Key takeaways
- An SMTP catcher accepts every message and delivers none, making local email development safe and fast.
- Abstract your sender so HTTP-API-based apps can use the catcher or a file sender in development.
- Review headers, plain text and MIME structure, not just the rendered HTML.
- Local tools cannot test authentication, filtering, real client rendering or provider behavior.
Start with Koltrix
Your domain, one inbox, and an API that sends.
A team inbox where AI sorts and drafts (nothing is sent without your click), plus the transactional API and SMTP relay your product sends with. 7 days free, no card.

