Skip to main content

MailHog vs Mailpit: Testing Verification and Password Reset Emails with Casdoor

· 9 min read
Yang Luo
Casdoor Maintainer

MailHog and Mailpit are both fake SMTP servers for development: they accept every message, keep it, and show it in a web UI instead of delivering it. Both use the same default ports, SMTP on 1025 and the web UI and API on 8025, so the SMTP address is smtp://127.0.0.1:1025 and the inbox is at http://127.0.0.1:8025 for either one. The difference is maintenance: MailHog's last release, v1.0.1, came out in August 2020, while Mailpit released v1.33.0 on October 31, 2026. For a new project, use Mailpit. Casdoor sends to both through a plain SMTP provider, so you can test sign-up codes, password reset emails and magic links without a real mail account.

MailHog vs Mailpit at a glance​

MailHogMailpit
Latest releasev1.0.1, 2020-08-11v1.33.0, 2026-10-31
Last commit on the default branch2022-08-022026-10-31
LicenseMITMIT
Default SMTP port10251025
Default web UI and API port80258025
Docker imagemailhog/mailhog, amd64 onlyaxllent/mailpit, amd64, arm64 and 386
HTTP APIv1 and v2: list, get, delete, search by from, to or containingv1: list, get, delete, search with filters such as to: and subject:, latest message shortcut
Default storageIn memoryTemporary SQLite database, deleted on exit
Persistent storageMongoDB or MaildirSQLite file (MP_DATABASE) or rqlite
Message limitNone documentedKeeps the newest 500 by default (MP_MAX_MESSAGES)
HTML check, link check, spam checkNoYes
Webhook on new mailNoYes (MP_WEBHOOK_URL)
SMTP TLSNot offeredOptional STARTTLS or SSL/TLS
Failure injectionJimChaos
Release a message to a real SMTP serverYesYes
POP3 serverNoYes, optional
Basic auth for the web UIYesYes

Docker Hub counts show that many people still pull MailHog. That reflects old tutorials and CI files, not activity: an open issue on the MailHog repository titled "THIS PROJECT IS DEAD!" has a 2023 reply from the author saying he no longer has time to maintain it. Mailpit's README describes itself as inspired by MailHog and points to that same comment.

The image architecture matters more than it looks. On an Apple Silicon laptop or an ARM CI runner, mailhog/mailhog runs only under emulation, while axllent/mailpit has a native arm64 image.

Run either one with Docker​

MailHog:

docker run -d --name mailhog -p 1025:1025 -p 8025:8025 mailhog/mailhog

Mailpit:

docker run -d --name mailpit -p 1025:1025 -p 8025:8025 axllent/mailpit

Mailpit keeps mail in a temporary database that disappears when the container stops. To keep it across restarts, mount a volume and set MP_DATABASE:

docker run -d --name mailpit \
-v "$PWD/mailpit-data:/data" \
-e MP_DATABASE=/data/mailpit.db \
-p 1025:1025 -p 8025:8025 \
axllent/mailpit

Without Docker, brew install mailhog and brew install mailpit both work on macOS, and Mailpit ships static binaries for Windows, Linux and macOS on its releases page.

In GitHub Actions, run it as a service container next to your test job:

services:
mailpit:
image: axllent/mailpit
ports:
- 1025:1025
- 8025:8025

Point Casdoor at MailHog or Mailpit​

Casdoor has no separate provider type for MailHog or Mailpit, and it doesn't need one. The Default email type sends over SMTP, and both tools are SMTP servers.

  1. As a global administrator, add a provider in the admin or built-in organization.
  2. Set Category to Email and Type to Default.
  3. Set Host to where the tool runs and Port to 1025.
  4. Leave Username and Password empty.
  5. Click Test SMTP Connection, then Send Testing Email, and check the web UI on port 8025.

The step-by-step pages with screenshots are Test email with MailHog and Test email with Mailpit.

A few details decide whether this works on the first try:

Leave SSL mode on Auto, or set it to Disable. In Auto, Casdoor uses implicit TLS only on port 465 and upgrades with STARTTLS only if the server offers it. Neither MailHog nor a default Mailpit offers STARTTLS on port 1025, so the connection stays plain. Don't choose Enable: that forces implicit TLS, and the handshake fails against a plain-text server. The SSL mode table has the full behavior.

Leave the credentials empty. Casdoor logs in to the SMTP server only when Username is set. If you fill it in, Go's PLAIN authentication refuses to send the password over an unencrypted connection unless the host is localhost, so a MailHog at 192.168.x.x fails with an authentication error even though MailHog would accept any login. Mailpit by default doesn't ask for authentication at all.

Use the right host. 127.0.0.1 works only when Casdoor and the mail catcher run on the same machine. If both run in Docker Compose, use the service name, such as mailpit, as the host.

Use a global provider. A provider owned by an ordinary organization may only reach Internet addresses, so it can't connect to 127.0.0.1 or a private IP. Providers in admin or built-in aren't restricted. See Which addresses a provider can reach.

Set origin for links. Verification codes work right away. Password reset links and magic links need Casdoor to know its public URL, through origin in conf/app.conf or a request on a loopback host. Otherwise the reset link is dropped from the email and magic links fail. See Templates.

Finally, add the provider to your application in Add providers to an application so sign-up and password reset actually use it.

Read the verification code in an automated test​

Casdoor's verification codes are six digits, and the default template reads "Here is your code: " followed by the code. An end-to-end test signs up a user, asks Casdoor to send the code, then reads it from the mail catcher's API.

With Mailpit, search by recipient and open the newest match. GET /api/v1/search returns matches newest first, and GET /api/v1/message/{ID} returns the parsed Text and HTML bodies:

import { request } from "@playwright/test";

const MAILPIT = process.env.MAILPIT_URL ?? "http://127.0.0.1:8025";

export async function latestCode(to: string): Promise<string> {
const api = await request.newContext({ baseURL: MAILPIT });
for (let i = 0; i < 20; i++) {
const res = await api.get("/api/v1/search", {
params: { query: `to:"${to}"`, limit: "1" },
});
const { messages } = await res.json();
if (messages.length > 0) {
const msg = await (await api.get(`/api/v1/message/${messages[0].ID}`)).json();
const match = (msg.Text || msg.HTML).match(/code: (\d{6})/);
if (match) return match[1];
}
await new Promise((r) => setTimeout(r, 500));
}
throw new Error(`no verification email for ${to}`);
}

If only one test sends mail at a time, GET /api/v1/message/latest skips the search step. Before each test, DELETE /api/v1/messages with no body clears the inbox.

With MailHog, the closest call is GET /api/v2/search?kind=to&query=<address>. Messages come back newest first in items, and the body is in items[0].Content.Body:

const res = await api.get("/api/v2/search", {
params: { kind: "to", query: to, limit: "1" },
});
const { items } = await res.json();
const body: string = items[0]?.Content.Body ?? "";

Content.Body is the raw MIME body. Casdoor sends quoted-printable, so = becomes =3D and long lines get soft breaks ending in =. Decode it first, or keep the code near the start of a short template. Clear MailHog with DELETE /api/v1/messages.

Migrating from MailHog to Mailpit​

Because the ports match, most migrations are a one-line change in docker-compose.yml: replace mailhog/mailhog with axllent/mailpit. Casdoor's provider keeps the same host and port. What does change:

  • Settings. MailHog reads MH_* variables; Mailpit reads MP_*. If you used MH_STORAGE=maildir or MongoDB, move to MP_DATABASE.
  • Test helpers. Mailpit has no /api/v2 endpoints. Rewrite calls to /api/v2/messages and /api/v2/search for /api/v1/messages and /api/v1/search, and read messages[].ID plus Text or HTML instead of items[].Content.Body. The quoted-printable decoding goes away.
  • Search syntax. kind=to&query=a@example.com becomes query=to:"a@example.com".
  • Failure injection. Jim has no flags in Mailpit; use Chaos.
  • Authentication. If an app insists on logging in over plain SMTP, start Mailpit with MP_SMTP_AUTH_ACCEPT_ANY=1 and MP_SMTP_AUTH_ALLOW_INSECURE=1. For Casdoor, leaving the credentials empty is simpler.
  • Retention. Mailpit prunes to the newest 500 messages by default. Raise MP_MAX_MESSAGES if a long test run needs more.

Going to production​

MailHog and Mailpit never deliver mail to real inboxes unless you release messages by hand, so production needs a real provider. In Casdoor you swap the provider, not the templates. The email templates, the %s code placeholder and the <reset-link> block stay the same:

A common setup keeps Mailpit in development and CI and the real provider in production. Each environment points its application at a different provider, so the sign-up flow you test is the one that ships.

References​