MailHog alternative: API compatibility matrix and a working migration diff

MailHog has been unmaintained since 2022. Here's a MailHog API compatibility matrix, a real docker-compose migration diff, and an honest comparison with Mailpit.

• 12 min read •

If you're searching for a MailHog alternative, you've probably already settled the first question: yes, it's abandoned. What's left is the annoying part, which is swapping it out without breaking a docker-compose file, a test suite, and a CI job on the same afternoon. So this isn't a ranked list of ten tools. It's an endpoint-by-endpoint compatibility matrix, a real before/after migration diff, and a straight answer about when you should pick Mailpit instead of us.

MailHog is dead, and here's the receipt

MailHog's last tagged release is v1.0.1, published in 2020. The repository at github.com/mailhog/MailHog has had no commits to the main branch since August 2022, and the issue tracker has open reports going back years with no maintainer response. Ian Kent built something genuinely good and then moved on, which is his right, but the practical result is a Go binary sitting in your CI pipeline with a dependency tree nobody has touched in four years.

For local development on a laptop, an unmaintained mail catcher is a low-stakes risk. It gets less comfortable once the same image runs in a shared CI runner or a staging cluster, where it's an unpatched HTTP server with an open SMTP listener. That's usually the trigger for the migration, not any missing feature. Nothing broke. It's just that nobody wants to be the person who signed off on it.

Check before you migrate

Grep your codebase for port 8025 and for /api/v1 and /api/v2 paths. In most projects the actual coupling to MailHog is three or four lines: an SMTP host, a UI port, and one API call in a test helper. Knowing exactly which endpoints you hit makes the rest of this article a two-minute read.

The honest answer: Mailpit is the lowest-friction switch

We're not going to bury this. If your setup is one developer, one laptop, one docker-compose file, Mailpit is the easiest MailHog replacement available and it costs you nothing. It's actively maintained by Axllent, it ships a MailHog compatibility layer for parts of API v1 and v2, and the migration is genuinely a one-line image swap because it uses the same default ports.

services:
mailhog:
- image: mailhog/mailhog:v1.0.1
+ image: axllent/mailpit
ports:
- "1025:1025"
- "8025:8025"

That's the whole migration for a lot of teams. If you read this far, tried that, and it worked, we're happy for you. Close the tab. The rest of this article is for the case where a container bound to localhost stopped being enough.

Where a local-only mail catcher runs out of road

MailHog and Mailpit share a design assumption: the mailbox lives next to the process that sent the mail. That's fine until any of these show up. Your CI job sends a password reset and the assertion has to happen in a different container. A QA engineer wants to look at the same welcome email the backend developer just triggered, without cloning the repo. A product manager asks whether the new invoice template looks right in Outlook. A staging environment on a remote host sends mail and there's no browser on that host to open port 8025 with.

At that point people start doing awkward things: SSH tunnels to a staging MailHog, a shared box with the UI exposed behind basic auth, screenshots pasted into Slack. MailSandbox is built for that shape of problem instead. One SMTP endpoint your laptop, your staging deploy, and your CI runners all point at, with per-project inboxes, real accounts, and an API token you can put in a secrets store. Same job as MailHog, different topology.

  • Every environment sends to the same host, so there's no per-machine setup step
  • Inboxes are shared, so QA and support see what developers see
  • CI runners hit an API over HTTPS with a scoped token instead of a sidecar container
  • Messages persist past the end of a job, which matters when you're debugging a flaky test

MailHog API compatibility matrix

This is the part worth skimming. MailSandbox implements the MailHog API shape deliberately so your existing test helpers keep working, but it is not a byte-for-byte clone and we're not going to pretend the gaps don't exist. Everything marked as missing is missing on purpose, usually because the feature only makes sense for a single-process local binary.

Endpoint Method MailSandbox Notes
/api/v1/messages GET Yes Returns the same message envelope shape. Scoped to the inbox your token belongs to.
/api/v1/messages DELETE Yes Clears the inbox. Common in test teardown.
/api/v1/messages/{id} GET Yes Full MIME structure, headers, and both bodies.
/api/v1/messages/{id} DELETE Yes Single message delete.
/api/v1/messages/{id}/download GET Yes Raw .eml download.
/api/v1/messages/{id}/mime/part/{part}/download GET Yes Attachment and part download.
/api/v1/events GET No MailHog's SSE stream is not implemented. Use webhooks or short-interval polling of /api/v2/messages instead.
/api/v1/messages/{id}/release POST Partial Release to a real SMTP server works, but only to allow-listed domains configured on the project.
/api/v2/messages GET Yes Supports start and limit paging with the same response envelope (total, count, start, items).
/api/v2/search GET Yes kind=from, kind=to and kind=containing are supported. Regex search is not.
/api/v2/jim GET / POST / DELETE No The Jim chaos monkey has no equivalent. Failure injection belongs in your SMTP client's test doubles, not in a shared inbox.
/api/v2/outgoing-smtp GET No Outgoing SMTP profiles are configured per project in the dashboard, not through the API.

One difference that isn't an endpoint: authentication. MailHog either had no auth or used MH_AUTH_FILE with basic auth. MailSandbox uses a bearer token per project, so any helper that currently sends no Authorization header needs one line added. Full request and response examples live in the [API docs](https://mailsandbox.com/docs/api).

The docker-compose migration diff

Here's a typical MailHog setup before the change. One app service, one mail catcher, ports published to the host, no credentials because there aren't any.

services:
app:
build: .
environment:
MAIL_HOST: mailhog
MAIL_PORT: 1025
MAIL_USERNAME: ""
MAIL_PASSWORD: ""
MAIL_ENCRYPTION: ""
depends_on:
- mailhog
 
mailhog:
image: mailhog/mailhog:v1.0.1
ports:
- "1025:1025"
- "8025:8025"

And after. The container disappears entirely, which also means one less image to pull on every CI run and one less service in your dependency graph.

services:
app:
build: .
environment:
- MAIL_HOST: mailhog
- MAIL_PORT: 1025
- MAIL_USERNAME: ""
- MAIL_PASSWORD: ""
- MAIL_ENCRYPTION: ""
- depends_on:
- - mailhog
-
- mailhog:
- image: mailhog/mailhog:v1.0.1
- ports:
- - "1025:1025"
- - "8025:8025"
+ MAIL_HOST: smtp.mailsandbox.com
+ MAIL_PORT: 2525
+ MAIL_USERNAME: ${MAILSANDBOX_USER}
+ MAIL_PASSWORD: ${MAILSANDBOX_PASS}
+ MAIL_ENCRYPTION: tls

Port 2525 is there because plenty of hosting providers and CI networks block 587 and 25 outbound. If yours doesn't, 587 works the same way. The credentials come from a .env file locally and from your CI secrets store in the pipeline, so nothing sensitive lands in the compose file itself.

Asserting on email in CI

This is where the shared endpoint earns its keep. With MailHog you either ran a service container in the job and queried localhost:8025, or you skipped email assertions altogether. With a remote inbox the test just talks to the API, and the message is still there afterwards when you're trying to work out why the job went red.

# wait for the signup mail, then assert it arrived
for i in $(seq 1 10); do
TOTAL=$(curl -sf \
-H "Authorization: Bearer $MAILSANDBOX_TOKEN" \
"https://api.mailsandbox.com/api/v2/search?kind=to&query=signup@example.test" \
| jq '.total')
[ "$TOTAL" -gt 0 ] && break
sleep 1
done
 
[ "$TOTAL" -gt 0 ] || { echo "no signup mail received"; exit 1; }

If your old helper called /api/v2/search against MailHog, the only edits are the base URL and the Authorization header. Give each pipeline its own inbox so parallel jobs don't read each other's mail, and clear it in teardown with DELETE /api/v1/messages. There are ready-made examples for GitHub Actions and GitLab CI in the [CI guide](https://mailsandbox.com/docs/ci).

When you should pick Mailpit instead

Some MailHog users genuinely shouldn't migrate to a hosted sandbox, and pretending otherwise wastes everyone's time. Go with Mailpit if any of these describe you.

  • You work solo, or nobody outside the dev team ever needs to look at a test email
  • Your development machines are offline or air-gapped, so an external SMTP host is a non-starter
  • You depend on MailHog's Jim chaos monkey for failure injection, since we don't replicate it
  • You need the /api/v1/events SSE stream and rewriting that consumer isn't worth the effort
  • Policy says test data never leaves your own infrastructure, and you'd rather run your own container than review a vendor

Plenty of teams end up running both, which is a perfectly reasonable outcome. Mailpit locally for the tight inner loop where speed matters and there's nothing to share, MailSandbox for CI and staging where the whole team and the pipeline need to see the same inbox. The two configurations differ by a handful of environment variables, so switching is a matter of which .env file you load.

Is MailHog really abandoned, or just stable?

Abandoned. The last release, v1.0.1, is from 2020, and there have been no commits since August 2022. Issues and pull requests sit unanswered. It still runs, but nobody is patching it.

Will my existing MailHog API calls work against MailSandbox?

Most of them. The v1 message endpoints and the v2 messages and search endpoints are implemented with the same paths and response shapes. The SSE event stream, the Jim chaos monkey, and the outgoing-smtp endpoint are not. Check the matrix above before you migrate.

Do I have to change my application code?

No. It's an SMTP host, port, and credentials change in your configuration. Application code that sends mail through your framework's mailer stays exactly as it is. Only test helpers that call the MailHog API need a base URL and an auth header.

Can I keep using MailHog or Mailpit locally and MailSandbox in CI?

Yes, and it's a common pattern. Load different mail settings per environment. Local development points at a container, CI and staging point at the shared endpoint.

What happened to the chaos monkey?

We deliberately don't implement it. Randomly rejecting connections on an endpoint that other people's test runs depend on causes more confusion than it prevents. Simulate SMTP failures in your own test doubles instead, where the failure is deterministic.

What about port 8025 in my scripts?

It goes away. The UI is a web app you log into and the API is on HTTPS, so there's no local port to publish and no tunnel to open on a staging host.

Picking your replacement

The decision isn't really MailHog versus a specific tool. It's whether your test inboxes should stay on individual machines or move somewhere everyone can reach. If the answer is the first one, swap the image for Mailpit and get on with your day. If your CI pipeline can't assert on email, or if QA keeps asking a developer to screenshot an inbox, the topology is the thing that needs changing and the endpoint compatibility above means it costs you an afternoon at most.

Migrating off MailHog?
Create a project, get SMTP credentials and an API token, and point your compose file at it. The MailHog-compatible endpoints mean your existing test helpers keep working.
Get started