Fake SMTP Server: Mailpit, MailHog, or a Hosted Sandbox

Mailpit, MailHog, MailCatcher, and smtp4dev catch email on your machine. What each one does, where localhost stops working, and when a hosted sandbox fits.

In development, your app should send email the way it does in production, and nobody should receive it. A fake SMTP server does that: it accepts the message over SMTP and keeps it where you can read it, instead of delivering it.

There are three usual setups.

Log the Message

Most frameworks can skip SMTP and print the message instead: Laravel's log mailer, Django's console backend, Rails' :test delivery method, Nodemailer's jsonTransport.

There's nothing to run or configure. The SMTP code path never runs, though, so a wrong host or password first shows up in production, and nobody sees the HTML rendered.

Run a Catcher Locally

A local catcher is an SMTP server on your machine with a web UI that shows what it caught. Point the app at localhost and every message lands there, whatever the recipient. Four are common.

Mailpit

A single binary or Docker image. SMTP listens on port 1025, the web UI and a REST API on 8025. It can also check HTML against common email clients, test links, score spam with SpamAssassin or Rspamd, and serve POP3. It keeps the newest 500 messages, in a database that's deleted when Mailpit exits unless you give it a file.

MailHog

MailHog uses the same ports, 1025 for SMTP and 8025 for the UI, and it's the name in many older tutorials and Compose files. Messages live in memory and disappear on restart, unless you configure MongoDB or file storage. Its last release, v1.0.1, came out in August 2020. For a new setup, Mailpit covers the same ground and is still released.

MailCatcher

A Ruby gem: gem install mailcatcher, then SMTP on 1025 and the web UI on 1080. Its JSON endpoints, /messages and /messages/:id.json, are enough for simple assertions in tests.

smtp4dev

A .NET tool for Windows, macOS, and Linux, also published as a Docker image. Besides the web UI and an API, it serves IMAP and POP3, so caught mail opens in a desktop client, and it can relay messages onward by rule.

Where Localhost Stops

For one developer, and mail that a process on the same machine sends, a local catcher is the right tool. Keep it. It gets harder when:

  • CI needs it. Each runner starts its own catcher, usually as a service container, and the inbox goes away with the job.
  • Someone else sends the mail. A third-party service, or an OTP from your auth provider, can't reach a port on your laptop.
  • The team needs the same inbox. A catcher on a laptop is visible on that laptop.
  • You need authentication results. A message that never left your machine has no SPF, DKIM, or DMARC result to check.

Use a Hosted Sandbox

send.smtp.dev is an SMTP server that delivers only to accounts in your sandbox. Your laptop, CI, and staging use the same settings, and every message lands in one place, readable in the browser, over IMAP or POP3, or through the REST API.

A domain whose MX points at mx.smtp.dev also receives mail from outside: a signup confirmation from a service you don't run, an OTP from your auth provider, a message from your staging email provider. Mail that arrives that way comes with SPF, DKIM, and DMARC results.

The costs: an account, a network connection, and test mail kept on a hosted service for your plan's retention window of 7, 30, or 90 days.

Side by Side

Log mailerLocal catcherHosted sandbox
Runs your SMTP codeNoYesYes
Works in CIYesAs a service containerYes
Receives mail from outside servicesNoNoYes
Shared with the teamNoIf you host itYes
Opens in a desktop mail clientNosmtp4dev over IMAP, Mailpit over POP3IMAP and POP3
SPF, DKIM, DMARC resultsNoNoFor mail that arrives over MX
Works offlineYesYesNo

Switching from Mailpit or MailHog

You don't have to pick one. Keep the catcher on laptops and point CI and staging at the sandbox, with the host coming from the environment. It's usually the four settings your mailer already reads:

SMTP_HOST=send.smtp.dev
SMTP_PORT=587
SMTP_USER=dev@test.example.com
SMTP_PASSWORD=your-account-password

Three things behave differently from a local catcher:

  • Credentials. A catcher usually accepts mail without a login. The sandbox needs the account's address and password, over STARTTLS.
  • Recipients. A catcher keeps mail for any address. The sandbox only delivers to accounts that exist in it. Seed users with addresses on your sandbox domain, or with plus aliases of one account, like dev+alice@test.example.com. On Pro and Max, a catch-all account makes every address on the domain work.
  • The API. Tests that read Mailpit's /api/v1/messages, MailHog's /api/v2/messages, or MailCatcher's /messages read /accounts/{accountId}/mailboxes/{mailboxId}/messages instead, newest first. The API quickstart has the wait-and-assert loop.

SMTP Test has the settings for Node, Python, Laravel, Django, Rails, Go, and Spring Boot, plus commands to check the connection.