Resource library

QA How-To

MailHog vs Mailpit Email Testing (2026)

Compare mailhog vs mailpit email testing for 2026 with Docker setup, REST API checks, CI examples, migration risks, and a clear choice for QA teams today.

20 min read | 3,370 words

TL;DR

Mailpit is the better default for new email test environments in 2026. MailHog still captures SMTP reliably, but its old release line and narrower diagnostics make it a legacy choice unless compatibility requirements justify keeping it.

Key Takeaways

  • Choose Mailpit for new 2026 projects because it is actively released and has stronger automation and operations features.
  • Keep MailHog temporarily when an existing suite depends on its API response shape, Jim, MongoDB, or file storage.
  • Both tools accept SMTP on port 1025 and serve a web UI on port 8025 by default.
  • Hide vendor-specific REST payloads behind a small adapter so application tests can switch sinks safely.
  • Correlate every test email with a unique subject token instead of reading the latest shared message.
  • Use Mailpit readiness probes, SQLite storage, HTML checks, and API search to make CI failures easier to diagnose.
  • Treat either tool as non-production infrastructure and never expose an unauthenticated inbox publicly.

MailHog vs Mailpit email testing is a choice between a familiar SMTP catcher with an old release line and a maintained successor with richer search, diagnostics, health checks, and protocol controls. For a new 2026 test environment, choose Mailpit. Keep MailHog only when existing automation depends on its API payloads, Jim chaos behavior, MongoDB or file storage, or a migration cannot yet be justified.

Both tools prevent test messages from reaching real customers. Your application sends normal SMTP to port 1025, the sink stores the message, and a browser or test reads it through port 8025. This guide runs one Playwright assertion through both products, exposes the differences that affect QA teams, and gives you a controlled migration path. If Docker networking is new to you, read Docker Compose for test environments alongside the setup.

The comparison is based on the projects' published behavior and APIs. MailHog's latest tagged release is v1.0.1 from August 2020. Mailpit v1.30.0 was released in May 2026 and includes current security fixes, so the version pins below make the exercise reproducible as of the publication date.

TL;DR: mailhog vs mailpit email testing Verdict

Decision factor MailHog v1.0.1 Mailpit v1.30.0 2026 verdict
Default SMTP and web ports 1025 and 8025 1025 and 8025 Drop-in network configuration for basic capture
Release activity Latest tagged release is from 2020 Regular 2026 releases Mailpit
REST automation v1 detail/delete plus v2 list/search Documented v1 API for list, search, body views, tags, checks, send, and release Mailpit
Search From, to, or containing query Field filters, dates, tags, read state, and free text Mailpit
Email diagnostics Source and MIME inspection HTML compatibility, link check, screenshots, optional SpamAssassin Mailpit
Health and metrics Probe the HTTP root yourself /livez, /readyz, and Prometheus metrics Mailpit
Persistence Memory, file, or MongoDB Temporary or persistent SQLite, plus rqlite support Depends on an existing storage contract
Failure injection Jim chaos testing Configurable SMTP chaos triggers Both, but APIs and semantics differ
Best fit Stable legacy stack New local, CI, Compose, or Kubernetes stack Mailpit for new work

MailHog remains useful software. The issue is not whether it can receive a message. The issue is whether a team should build new infrastructure around a release that predates current container, security, and observability expectations. Mailpit wins that broader decision.

1. What mailhog vs mailpit email testing Actually Covers

An SMTP sink tests the handoff between your application and a mail server. It can prove the envelope recipient, visible headers, subject, text and HTML bodies, attachments, and generated links. It can also expose encoding defects that a mocked sendMail() call will never show, such as a malformed multipart boundary or a display name encoded incorrectly.

It does not prove internet delivery. Neither local product validates your DNS reputation, SPF alignment, DKIM signature, DMARC policy, provider throttling, mailbox placement, or rendering in every real client. Mailpit's HTML compatibility report is useful static analysis, but it is not a screenshot from Outlook or Gmail. Keep that boundary explicit in your test strategy.

Use three layers. Unit-test template variables and mail orchestration without a server. Run a smaller SMTP integration suite against Mailpit or MailHog. Reserve a controlled staging check with a real provider for DNS, relay, and inbox behavior. The API error handling and negative testing guide helps define rejected inputs and timeouts at the mail-service boundary.

A good comparison therefore measures more than UI preference. Check the SMTP behavior your application uses, REST contracts your tests consume, readiness in orchestration, evidence available after failure, data cleanup, access control, and the cost of changing existing helpers.

What You Will Build

You will create a small, vendor-neutral email test harness that can run against either sink. By the end, you will have:

  • A Docker Compose file with mutually exclusive mailhog and mailpit profiles.
  • A Nodemailer client that sends the same multipart reset email to either container.
  • A REST adapter that normalizes the two incompatible message payloads.
  • A Playwright test that verifies recipient, subject, text, HTML, and a unique correlation token.
  • A Mailpit-only diagnostic test for attachments and HTML compatibility analysis.
  • A GitHub Actions service definition with a real readiness check.

The harness deliberately uses SMTP for the action and HTTP for observation. That preserves the production-like delivery boundary while giving the test deterministic access to captured mail.

Prerequisites

Install Docker Engine or Docker Desktop with Compose v2, Node.js 22 or newer, and curl. Node 22 provides the built-in fetch implementation used by the adapter. Run these commands before creating files:

docker --version
docker compose version
node --version
curl --version

Expected verification: all four commands exit with status 0. docker compose version must use the space-separated Compose v2 command, not the retired Python docker-compose binary.

Create an empty exercise directory, initialize npm, and install the two runtime dependencies. Playwright's API assertions do not require a browser download for this example.

mkdir email-sink-comparison
cd email-sink-comparison
npm init -y
npm install nodemailer
npm install --save-dev @playwright/test
npm pkg set 'scripts.test:email=playwright test tests/email.spec.mjs tests/mailpit-diagnostics.spec.mjs'

Verify the package graph before continuing:

npm ls nodemailer @playwright/test

Both packages should appear without UNMET DEPENDENCY. Commit package.json and package-lock.json if you move this exercise into a real repository.

Step 1: Start MailHog and Mailpit with Exclusive Compose Profiles

Put both products in compose.yaml, but do not start them together. They publish the same host ports, so profiles let you compare them without editing application configuration.

services:
  mailhog:
    image: mailhog/mailhog:v1.0.1
    profiles: [mailhog]
    ports:
      - '1025:1025'
      - '8025:8025'

  mailpit:
    image: axllent/mailpit:v1.30.0
    profiles: [mailpit]
    ports:
      - '1025:1025'
      - '8025:8025'
    environment:
      MP_DATABASE: /data/mailpit.db
      MP_MAX_MESSAGES: 500
    volumes:
      - mailpit-data:/data

volumes:
  mailpit-data:

Start MailHog first. Pinning images prevents an invisible version change from altering a CI run.

docker compose --profile mailhog up -d mailhog
curl -fsS 'http://127.0.0.1:8025/api/v2/messages?limit=1'

Expected verification: Compose reports the mailhog service as running and the API returns JSON with total, count, start, and items. Open http://127.0.0.1:8025 if you also want to confirm the inbox visually. An empty items array is correct before the first send.

Mailpit's official container includes a health check that runs /mailpit readyz. The Compose volume is intentionally attached only to Mailpit because its standard persistent store is SQLite. MailHog has different file and MongoDB storage options, so pretending the volume configuration is portable would hide a migration concern.

Step 2: Send One Real Multipart SMTP Message

Create test-support/email-client.mjs. The sender uses the same host, port, and message for both tools. The unique run ID appears in the subject, bodies, URL, and attachment, which makes a parallel test distinguish its own mail from another worker's output.

import nodemailer from 'nodemailer';

export async function sendResetEmail({ runId, to = 'buyer@example.test' }) {
  const transporter = nodemailer.createTransport({
    host: process.env.SMTP_HOST ?? '127.0.0.1',
    port: Number(process.env.SMTP_PORT ?? 1025),
    secure: false
  });

  const subject = `Password reset ${runId}`;
  const result = await transporter.sendMail({
    from: 'QA Store <no-reply@example.test>',
    to,
    subject,
    text: `Use reset token ${runId} within 15 minutes.`,
    html: `<h1>Reset password</h1><p>Run ${runId}</p><a href="https://example.test/reset?token=${encodeURIComponent(runId)}">Reset password</a>`,
    attachments: [
      { filename: 'run-evidence.txt', content: `run=${runId}\n` }
    ]
  });

  return { messageId: result.messageId, subject, to };
}

Do not set fake SMTP credentials merely because a production mailer uses them. Both default containers accept unencrypted local SMTP without authentication. Add TLS and authentication in a separate environment-specific test when those settings are part of your product's actual contract.

Verify that Node can load the module before involving either API:

node -e "import('./test-support/email-client.mjs').then(m => console.log(typeof m.sendResetEmail))"

Expected verification: the command prints function. A syntax or package-resolution error at this point belongs to the local harness, not to MailHog.

Step 3: Normalize the MailHog and Mailpit REST APIs

The SMTP ports are compatible, but the HTTP payloads are not. MailHog v2 returns messages in items, with headers below Content.Headers. Mailpit returns summaries in messages, then exposes full content at /api/v1/message/{ID}. Hide that difference in test-support/email-sink.mjs.

import assert from 'node:assert/strict';

const sink = process.env.EMAIL_SINK ?? 'mailpit';
const baseUrl = process.env.MAIL_API_URL ?? 'http://127.0.0.1:8025';

async function checkedJson(path) {
  const response = await fetch(`${baseUrl}${path}`);
  assert.equal(response.ok, true, `GET ${path} returned ${response.status}`);
  return response.json();
}

async function listMessages() {
  if (sink === 'mailhog') {
    const page = await checkedJson('/api/v2/messages?limit=50');
    return page.items;
  }
  const page = await checkedJson('/api/v1/messages?limit=50');
  return page.messages;
}

function getSubject(message) {
  return sink === 'mailhog'
    ? message.Content?.Headers?.Subject?.[0]
    : message.Subject;
}

async function normalize(message) {
  if (sink === 'mailhog') {
    return {
      id: message.ID,
      subject: getSubject(message),
      recipients: message.To.map(item => `${item.Mailbox}@${item.Domain}`),
      content: message.Content?.Body ?? ''
    };
  }

  const full = await checkedJson(`/api/v1/message/${message.ID}`);
  return {
    id: full.ID,
    subject: full.Subject,
    recipients: full.To.map(item => item.Address),
    content: `${full.Text}\n${full.HTML}`
  };
}

export async function clearMailbox() {
  const path = sink === 'mailhog' ? '/api/v1/messages' : '/api/v1/messages';
  const response = await fetch(`${baseUrl}${path}`, { method: 'DELETE' });
  assert.equal(response.ok, true, `DELETE ${path} returned ${response.status}`);
}

export async function waitForMessage(subject, timeoutMs = 10000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const match = (await listMessages()).find(item => getSubject(item) === subject);
    if (match) return normalize(match);
    await new Promise(resolve => setTimeout(resolve, 200));
  }
  throw new Error(`No ${sink} message found with subject: ${subject}`);
}

The poll is condition-based and bounded. A fixed five-second wait makes every successful test slow and still fails when a busy runner needs six seconds. The subject match is exact, so Password reset run-12 cannot accidentally select Password reset run-123.

Verify both JavaScript files parse:

node --check test-support/email-client.mjs
node --check test-support/email-sink.mjs

Expected verification: both commands exit silently with status 0. Note that the identical delete path is not an abstraction mistake: both products implement DELETE /api/v1/messages, while their list and detail contracts diverge.

Step 4: Run the Same Playwright Test Against MailHog

Create tests/email.spec.mjs. The test clears stale data, sends through SMTP, then observes the sink through HTTP. In a shared long-lived environment, replace global cleanup with per-run search and targeted deletion so one worker cannot erase another worker's evidence.

import { randomUUID } from 'node:crypto';
import { test, expect } from '@playwright/test';
import { sendResetEmail } from '../test-support/email-client.mjs';
import { clearMailbox, waitForMessage } from '../test-support/email-sink.mjs';

test('captures a complete password reset email', async () => {
  const runId = process.env.RUN_ID ?? `email-${randomUUID()}`;
  await clearMailbox();

  const sent = await sendResetEmail({ runId });
  const captured = await waitForMessage(sent.subject);

  expect(captured.subject).toBe(`Password reset ${runId}`);
  expect(captured.recipients).toContain('buyer@example.test');
  expect(captured.content).toContain(runId);
  expect(captured.content).toContain('Reset password');
});

Run only the portable test against MailHog:

EMAIL_SINK=mailhog RUN_ID=mh-2026 npx playwright test tests/email.spec.mjs

Expected verification: Playwright reports one passed test, and the MailHog UI shows a message with subject Password reset mh-2026. If the SMTP send succeeds but the API assertion fails, save the API response before changing selectors. MailHog's v2 Swagger describes a simplified payload, while the running v1.0.1 API uses the established items, ID, and Content fields consumed above.

This is the main reason not to scatter sink calls across test cases. A change to one adapter is reviewable; dozens of direct JSON paths create a costly and fragile migration. Apply the same boundary principle used in a larger JavaScript API automation framework.

Step 5: Switch to Mailpit Without Changing the Product Test

Stop MailHog before publishing the same host ports. Then start the Mailpit profile and wait on the product's explicit readiness endpoint.

docker compose --profile mailhog stop mailhog
docker compose --profile mailpit up -d mailpit
curl -fsS http://127.0.0.1:8025/readyz
curl -fsS http://127.0.0.1:8025/api/v1/info

Expected verification: /readyz returns HTTP 200, and /api/v1/info returns runtime information including the Mailpit version and message totals. If Compose reports a port conflict, use docker compose ps to find the service still bound to 1025 or 8025.

Run the unchanged Playwright scenario with only the adapter selection changed:

EMAIL_SINK=mailpit RUN_ID=mp-2026 npx playwright test tests/email.spec.mjs

Expected verification: one test passes and Mailpit displays Password reset mp-2026. This proves practical SMTP compatibility, not total product equivalence. Your production application still needs a smoke test for any features it actually uses, such as SMTP AUTH, STARTTLS, a sendmail replacement, relay release, or deliberate 4xx/5xx responses.

Mailpit also supports GET /api/v1/search?query=... for richer server-side selection. Prefer it when the mailbox contains many messages, but keep a unique run ID even with good search. The identifier makes the test result, logs, and captured mail traceable as one execution.

Step 6: Add Mailpit-Specific HTML and Attachment Checks

Portability should cover common business assertions, not suppress valuable product capabilities. Create tests/mailpit-diagnostics.spec.mjs for checks that have no direct MailHog equivalent. The test uses the normalized ID, then calls documented Mailpit endpoints.

import { randomUUID } from 'node:crypto';
import { test, expect } from '@playwright/test';
import { sendResetEmail } from '../test-support/email-client.mjs';
import { clearMailbox, waitForMessage } from '../test-support/email-sink.mjs';

test('inspects Mailpit HTML and attachment metadata', async ({ request }) => {
  test.skip(process.env.EMAIL_SINK !== 'mailpit', 'Mailpit API test');
  const runId = `diagnostic-${randomUUID()}`;
  await clearMailbox();

  const sent = await sendResetEmail({ runId });
  const captured = await waitForMessage(sent.subject);

  const messageResponse = await request.get(
    `${process.env.MAIL_API_URL ?? 'http://127.0.0.1:8025'}/api/v1/message/${captured.id}`
  );
  expect(messageResponse.ok()).toBeTruthy();
  const message = await messageResponse.json();
  expect(message.Attachments.map(item => item.FileName)).toContain('run-evidence.txt');

  const checkResponse = await request.get(
    `${process.env.MAIL_API_URL ?? 'http://127.0.0.1:8025'}/api/v1/message/${captured.id}/html-check`
  );
  expect(checkResponse.ok()).toBeTruthy();
  const report = await checkResponse.json();
  expect(report.Warnings).toEqual(expect.any(Array));
  expect(report.Total.Tests).toBeGreaterThan(0);
});

Run the diagnostic file explicitly:

EMAIL_SINK=mailpit npx playwright test tests/mailpit-diagnostics.spec.mjs

Expected verification: one test passes. Mailpit analyzes the HTML against more than 175 HTML and CSS checks backed by caniemail.com compatibility data. Do not set a universal minimum compatibility score without reviewing the targeted clients and each warning. A marketing template and a plain transactional receipt have different acceptable risks.

Mailpit can also check links, generate HTML screenshots, tag messages, and integrate with SpamAssassin. Link checking makes outbound HTTP requests, so keep current Mailpit security defaults and never enable internal-network access merely to make a test green.

Step 7: Run Email Testing in GitHub Actions

Use Mailpit as a service container. The health command prevents tests from racing the SMTP process, while the API URL remains on the runner's published port. Save the following as .github/workflows/email-test.yml in the exercise project.

name: email integration

on:
  pull_request:

jobs:
  test-email:
    runs-on: ubuntu-latest
    services:
      mailpit:
        image: axllent/mailpit:v1.30.0
        ports:
          - 1025:1025
          - 8025:8025
        options: >-
          --health-cmd "/mailpit readyz"
          --health-interval 2s
          --health-timeout 2s
          --health-retries 20

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run test:email
        env:
          EMAIL_SINK: mailpit
          SMTP_HOST: 127.0.0.1
          SMTP_PORT: 1025
          MAIL_API_URL: http://127.0.0.1:8025

Verification happens twice. GitHub waits for the container health command, then Playwright proves end-to-end SMTP capture and REST inspection. The Mailpit diagnostic test runs because EMAIL_SINK is mailpit. If your job itself runs inside a container, service networking changes: use the service hostname mailpit and container ports instead of 127.0.0.1.

Upload Playwright output on failure and correlate it with the run ID in application logs. The broader test automation CI/CD guide explains artifact retention, failure ownership, and pipeline stages; reusable GitHub Actions workflows for QA shows how to share this service definition without copying it into every repository.

How the Tools Differ Beyond the Happy Path

MailHog is compact and understandable. Its UI displays HTML, text, source, and MIME parts; its API lists, retrieves, deletes, searches, and releases messages; EventSource supports live updates; and Jim can inject SMTP problems. Existing users may rely on file or MongoDB persistence. Those capabilities are enough for many mature suites, which is why an immediate replacement is not always the highest-priority engineering work.

Mailpit covers the basic capture loop and extends it for current automation. Its API exposes message summaries, parsed bodies, rendered HTML and text views, raw source, attachments, tags, search, release, and an HTTP send endpoint. Operations teams get liveness, readiness, optional Prometheus metrics, SQLite persistence, TLS and authentication controls, POP3, and current container images. QA teams get client compatibility warnings, link inspection, HTML screenshots, optional SpamAssassin results, and chaos triggers.

The maintenance difference affects risk. A frozen dependency can remain stable, but known assumptions around TLS libraries, image bases, browser code, and network protections do not freeze with it. Mailpit's 2026 release history includes explicit security work around message-size limits, HTML handling, path traversal, and server-side request protections. That does not make any deployment safe by default. It does mean the project is responding to newly reported threats.

API design also changes test quality. MailHog's containing search is convenient but coarse. Mailpit's structured filters can narrow by sender, recipient, subject, tag, date, attachment state, and read state. Better selection reduces cross-test collisions, though unique test data remains mandatory. For more on isolation, use the API test data management guide.

Which Should You Choose

Choose Mailpit for a greenfield repository, a new shared QA environment, Kubernetes, or a CI service added in 2026. You gain a current release line, richer REST surfaces, explicit readiness, persistent SQLite, diagnostics, and clearer operational controls without changing the default SMTP host or ports. It is the pragmatic default, not merely the newer interface.

Keep MailHog for now if its current behavior is stable and a verified dependency is expensive to replace. Examples include helpers that deserialize Content.Headers, tests that configure Jim through its endpoints, infrastructure built around MongoDB persistence, or a release process that forwards selected captured messages through MailHog. Put a date and owner on that exception. An indefinite legacy choice made by silence is different from an accepted compatibility decision.

Do not choose either as a public, multi-tenant hosted inbox or a production mail delivery service. A captured message can contain reset links, one-time passwords, invoices, personal data, and internal URLs. Run the sink on an isolated test network, enable authentication when people or untrusted workloads can reach it, cap retention, and use synthetic recipients.

When a team needs provider-level delivery, cross-client screenshots, deliverability analytics, or managed tenancy, evaluate a hosted email testing platform in addition to the local integration sink. That is a different comparison from deciding which container should receive developer SMTP.

MailHog to Mailpit Migration Checklist

Start by inventorying behavior, not configuration names. Search the repository for mailhog, ports 1025 and 8025, /api/v1, /api/v2, Content.Headers, Jim settings, container health probes, and persistence mounts. Classify each reference as application SMTP, test observation, operations, or manual documentation.

Then migrate in a controlled order:

  1. Add the vendor-neutral adapter while MailHog still runs. Its tests become your baseline.
  2. Start Mailpit with pinned versions and the same internal service alias used by the application.
  3. Map MailHog v2 items to Mailpit messages, and fetch full Mailpit content by ID.
  4. Replace root-page readiness probes with /readyz; use /livez only to decide whether a process should restart.
  5. Convert persistence deliberately. MailHog files or MongoDB records are not a Mailpit SQLite database. Most test teams should discard old messages rather than transform them.
  6. Recreate authentication, TLS, relaying, maximum-message, and retention rules from requirements instead of translating variable names blindly.
  7. Run a parity suite for text, HTML, Unicode headers, multiple recipients, attachments, and the failure responses your mailer handles.
  8. Remove MailHog only after CI evidence and rollback instructions are reviewed.

Do not dual-deliver real reset or verification messages during migration. Send synthetic cases with non-routable example domains and compare captured fields. If API consumers outside the test repository exist, publish the adapter contract and deprecation date before changing the endpoint.

Troubleshooting

bind: address already in use -> Only one profile can publish ports 1025 and 8025. Run docker compose ps, stop the active sink, and verify no unrelated local process owns either port.

SMTP reports wrong version number or a TLS handshake failure -> The default listeners are plaintext. Set Nodemailer secure: false; configure certificates and STARTTLS explicitly only when the scenario requires encryption. Port 1025 is not implicit TLS merely because the production provider uses port 465.

The send succeeds but no matching message appears -> Confirm the application is using the container hostname from inside Compose and 127.0.0.1 only from the host runner. Log the SMTP envelope, run ID, sink selection, and API base URL. Poll by exact correlation value rather than opening the latest message.

Mailpit diagnostic assertions return 404 -> Fetch the message summary first and use its database ID. Do not substitute the RFC Message-ID header. Keep the same Mailpit database between the list and detail requests.

Tests pass alone but fail in parallel -> Remove global mailbox deletion from shared environments. Give each test a unique subject token, search for that token, and delete only its matched ID. Separate mailboxes or sink instances per worker provide even stronger isolation.

The HTML or link check cannot access an internal URL -> Current Mailpit blocks internal HTTP requests by default to reduce SSRF exposure. Prefer public synthetic assets or test the application link separately. Enabling internal access expands the sink's network reach and requires a security review.

Interview Questions and Answers

Use the structured interview Q&A attached to this guide to practice explaining the decision. A strong answer should distinguish SMTP capture from deliverability, name the incompatible API shapes, describe unique correlation IDs, and explain why readiness and maintenance status affect CI reliability.

Be ready to sketch the boundary used here: the application owns SMTP behavior, an adapter owns sink-specific HTTP, and the test owns business assertions. Interviewers usually care more about isolation, diagnosis, and trade-offs than whether you remember a particular JSON property.

Common Mistakes

  • Calling Mailpit a perfect drop-in replacement because the default ports match. SMTP setup is similar, but API payloads, persistence, chaos controls, and advanced configuration differ.
  • Reading /message/latest in parallel tests. Another worker can deliver between the user action and the lookup.
  • Deleting the entire inbox from every test. Global cleanup creates race conditions in a shared sink and destroys failure evidence.
  • Mocking the mailer in every layer. A mock cannot expose SMTP envelope, MIME, encoding, attachment, or network-configuration defects.
  • Treating a captured message as proof of external delivery. The local sink does not validate DNS, reputation, provider acceptance, or placement.
  • Exposing port 8025 publicly without authentication. Test mail often contains credentials, tokens, personal data, and internal links.
  • Using latest image tags in CI. Pin a reviewed release and update it through dependency maintenance.
  • Failing a build on an arbitrary HTML score. Review compatibility warnings against the clients and template risk that matter to the product.
  • Ignoring negative SMTP behavior. Exercise rejection, connection failure, timeout, and retry paths without turning chaos settings on for unrelated tests.
  • Migrating old captured messages automatically. Test inbox history is usually disposable, while a flawed conversion can preserve sensitive content longer than intended.

Where To Go Next

First, run the portable test against both profiles and record the API differences observed by your team. Next, add one product-owned workflow, such as password reset or invoice delivery, and assert only the fields that define its business contract. Practice the surrounding browser flow with Playwright API testing in TypeScript, or strengthen environment construction with Docker basics for testers.

Then add one negative case: make the SMTP endpoint unavailable and verify the application reports or retries the failure according to policy. Keep the fault limited to that case, restore the service afterward, and preserve the relevant application logs. This turns the sink from a convenient visual inbox into reliable test infrastructure.

Conclusion

For mailhog vs mailpit email testing in 2026, Mailpit is the stronger default. It keeps the familiar SMTP capture workflow while adding an active release line, a broader automation API, explicit probes, current security work, persistent SQLite, and useful message diagnostics.

Retain MailHog only behind a documented compatibility decision, then isolate its HTTP contract so migration remains possible. Start with the two-profile Compose file, run the same correlated email assertion against both products, and choose from evidence produced by your own mail path rather than UI familiarity.

Interview Questions and Answers

How would you compare MailHog and Mailpit for a CI test environment?

I would compare SMTP features first, then the observation API, readiness, persistence, security controls, diagnostics, and maintenance activity. Mailpit is my default for new CI because it supplies `/readyz`, current images, broader API coverage, and active fixes. I would keep MailHog only when a measured compatibility dependency makes migration more expensive than its current risk.

Why should an email test use SMTP for sending and HTTP for assertions?

SMTP exercises the same application boundary used to hand mail to a real server. HTTP gives the test deterministic, parsed access to the captured result without automating the inbox UI. Combining them detects transport and MIME problems while keeping assertions fast and diagnosable.

How do you prevent parallel email tests from selecting each other's messages?

I generate a collision-resistant run ID and place it in a searchable subject or header. Each worker polls for an exact match, records the matched database ID, and deletes only that record when cleanup is required. A dedicated sink per worker is appropriate when the service or test API cannot provide strong selection.

What breaks when migrating automated tests from MailHog to Mailpit?

The common SMTP host and ports may remain unchanged, but HTTP clients usually break. MailHog v2 uses `items` and nested `Content.Headers`; Mailpit returns `messages` summaries and a separate detail resource. Storage, health probes, search expressions, relaying, and chaos settings also need explicit review.

What can an SMTP sink prove, and what remains untested?

It proves that the application connected, supplied an envelope, and produced inspectable headers, MIME parts, bodies, links, and attachments. It does not establish public DNS authentication, sender reputation, provider delivery, spam-folder placement, or exact rendering by external clients. I cover those risks with narrower provider and client tests.

Why is polling preferable to a fixed sleep in captured-email tests?

Bounded polling completes as soon as the matching message exists and tolerates normal scheduling variance. A fixed delay wastes time on fast runs yet still fails when delivery exceeds the guessed duration. The poll must have an explicit deadline and an error that includes the correlation value and sink name.

How would you secure Mailpit or MailHog in a shared QA environment?

I would place it on a private network, restrict ingress, authenticate the UI and API, use TLS where traffic crosses an untrusted boundary, and keep SMTP access limited to approved workloads. I would cap message count and retention, prohibit real customer addresses, and avoid enabling features that let link checks reach internal services without a reviewed need.

Frequently Asked Questions

Is Mailpit a drop-in replacement for MailHog?

Mailpit is close to a drop-in replacement for basic SMTP capture because both default to SMTP port 1025 and web port 8025. It is not API-compatible: message lists, detail payloads, search syntax, persistence, and chaos configuration must be migrated or hidden behind an adapter.

Which is better, MailHog or Mailpit, for a new project in 2026?

Mailpit is the better starting point for a new project. It has active releases, explicit health endpoints, richer REST automation, SQLite persistence, and message diagnostics that reduce custom test infrastructure.

Can MailHog and Mailpit test real email delivery?

They can prove that an application formed a message and handed it to an SMTP server. They cannot prove public-provider acceptance, SPF, DKIM, DMARC, reputation, mailbox placement, or rendering in every real email client.

Can the same automated test run against MailHog and Mailpit?

Yes, if the test sends through their common SMTP contract and reads messages through a small product-specific API adapter. Keep recipient, subject, body, and correlation assertions in the test while the adapter translates JSON fields.

How should email tests find the correct captured message?

Generate a unique run ID and include it in the subject or a dedicated header. Poll for an exact match with a timeout; do not read the newest message, depend on inbox order, or erase a shared mailbox before every parallel case.

Should Mailpit or MailHog be exposed on the public internet?

No unauthenticated test inbox should be public. Captured messages can contain reset tokens, one-time codes, personal data, and internal URLs, so isolate the service, require authentication where necessary, limit retention, and use synthetic addresses.

Does Mailpit replace real email client compatibility testing?

No. Mailpit's HTML check is valuable static analysis and its screenshots help inspect generated markup, but neither reproduces every client engine or provider transformation. Use it as an early quality gate, then test critical templates in the actual clients you support.

Related Guides