Skip to content

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.

Koltrix Team4 min read
A laptop screen displaying colorful code
Photo by Mohammad Rahmani on Unsplash
On this page(9 sections)
  1. What an SMTP catcher does
  2. Setting one up
  3. If your app uses an HTTP email API
  4. What to look at in the catcher
  5. A development workflow that works
  6. What local testing cannot tell you
  7. Guardrails worth adding
  8. Checklist
  9. Key takeaways

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:

  1. Your app connects to localhost on the catcher's SMTP port.
  2. The catcher accepts every recipient, never relays anything, and stores the message.
  3. 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 .eml file is crude but effective; most mail clients can open .eml files 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

  1. Seed realistic data. Long names, non-ASCII characters, empty optional fields, many line items. Most template bugs show up only with awkward data.
  2. 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.
  3. Review in the catcher. HTML, text and headers.
  4. Iterate. Edit the template, resend, refresh.
  5. 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 development and the configured SMTP host is not localhost or 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.

SharePost on XLinkedIn
More in Guides →