mailhog-testing
Captures and asserts SMTP email in tests with MailHog, the Go-based dev mailbox (SMTP sink on `1025`, web UI + JSON API on `8025`, single Go binary or Docker), reading captured mail via APIv2 (`/api/v2/messages`, `/api/v2/search`) and injecting failures with the Jim chaos monkey. Use when a project already runs MailHog to test password-reset, verification, or notification emails; for new projects prefer Mailpit (richer API, active maintenance) - migration path in references.
Install with skills.sh (any agent)
npx skills add testland/qa --skill mailhog-testingmailhog-testing
Overview
Per github.com/mailhog/MailHog (opens in new window):
"MailHog is an email testing tool for developers" that allows you to "Configure your application to use MailHog for SMTP delivery" and "View messages in the web UI, or retrieve them with the JSON API."
MailHog is legacy as of the mid-2020s - Mailpit (per mailpit-testing) succeeded it with richer API + active maintenance. This skill covers MailHog for existing deployments + provides migration guidance to Mailpit.
When to use
For new projects, use mailpit-testing.
How to use
Install
Per mh-gh (opens in new window):
# Go install
go install github.com/mailhog/MailHog@latestDocker:
docker run -d \
--name mailhog \
-p 1025:1025 \
-p 8025:8025 \
mailhog/mailhogDefault ports
Per mh-gh (opens in new window):
| Port | Service |
|---|---|
| 1025 | SMTP server |
| 8025 | HTTP server (UI + APIv1 + APIv2) |
Configure your app's SMTP
Same pattern as Mailpit (since both expose unauthenticated SMTP on 1025 by default):
smtp:
host: localhost
port: 1025
auth: noneAssert via APIv2
Per mh-gh (opens in new window) MailHog has both APIv1 + APIv2; APIv2 is the modern one. Endpoints:
| Endpoint | Use |
|---|---|
GET /api/v2/messages | List captured messages (paginated) |
GET /api/v2/messages?limit=N&start=M | Pagination |
GET /api/v2/search?kind=to&query=alice@x.com | Search by recipient / subject / containing |
DELETE /api/v1/messages | Clear all messages (uses APIv1; APIv2 has no delete) |
The MailHog message structure is more nested than Mailpit's: Content.Headers.Subject is an array, not the flat Subject field Mailpit exposes. Walk the nested path or the assertion silently fails.
Worked example
Capture and assert a single password-reset email end to end. Clear the mailbox first so a stale message can't satisfy the assertion, trigger the app action, poll APIv2 until the message lands, then assert on its subject and body:
import requests
BASE = "http://localhost:8025"
def test_password_reset_via_mailhog():
requests.delete(f"{BASE}/api/v1/messages") # clear (APIv1 - no APIv2 delete)
trigger_password_reset("alice@example.com") # app under test sends the email
msg = poll_for_message(BASE, to="alice@example.com") # GET /api/v2/search?kind=to&query=...
assert msg["Content"]["Headers"]["Subject"][0] == "Reset your password"
assert "/reset?token=" in msg["Content"]["Body"]poll_for_message retries GET /api/v2/search?kind=to&query=alice@example.com until a message appears, then returns it. Sub-second SMTP delivery isn't guaranteed, so never assert immediately after triggering.
Jim chaos monkey
Per mh-gh (opens in new window): "Chaos Monkey for failure testing" via the Jim component. Jim is configurable via CLI flags or environment:
mailhog -invite-jim # enables the chaos monkeyOnce Jim is invited, MailHog injects failures (random connection drops, slow responses) per the configured probabilities. Configuration flags are documented in mailhog -h; the README references "Introduction to Jim" for details.
For new test work needing chaos, prefer Mailpit's Chaos mode (per mailpit-testing Step 5) - it has richer per-recipient configuration.
CI integration
services:
mailhog:
image: mailhog/mailhog
ports: [1025:1025, 8025:8025]
steps:
- run: pytest tests/integration/email/ -vMigrating to Mailpit
For new projects, and for teams ready to leave MailHog, migrate to Mailpit (mailpit-testing) - the actively maintained successor with a flatter API and richer chaos configuration. The feature-mapping table, the schema-rewrite notes, and the step-by-step cutover live in references/migrating-to-mailpit.md.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Start new project on MailHog | Legacy; loses richer Mailpit features | Use Mailpit (Migrating to Mailpit + cross-skill) |
| Use APIv1 for new test code | Deprecated by APIv2 (within MailHog) | APIv2 endpoints (Assert via APIv2) |
| Assume MailHog flat schema | MailHog's Content.Headers.Subject is an array | Walk the nested structure (Worked example) |
| Skip per-test message clear | Same problem as Mailpit Step 4; stale messages | DELETE /api/v1/messages in setup |
| Skip Jim coverage | Same as Mailpit Step 5; misses resilience | Enable Jim or migrate to Mailpit Chaos |
Limitations
References
Migrating from MailHog to Mailpit
View source (opens in new window)Migrating from MailHog to Mailpit
Deep reference for the mailhog-testing SKILL.md. Consult when moving an existing MailHog deployment to Mailpit (per mailpit-testing), the actively maintained successor with a flatter API.
Why migrate
MailHog has had no significant release since ~2020 and is effectively unmaintained. Mailpit is the actively maintained successor with a flatter JSON schema, a single REST API for both reads and deletes, and richer per-recipient chaos configuration.
Feature mapping
| MailHog | Mailpit equivalent |
|---|---|
SMTP on 1025 | SMTP on 1025 (same) |
HTTP UI on 8025 | Web UI on 8025 (same) |
GET /api/v2/messages | GET /api/v1/messages (path differs; flat schema) |
GET /api/v2/search?kind=to&query=... | GET /api/v1/search?query=to:... (Lucene-ish syntax) |
DELETE /api/v1/messages | DELETE /api/v1/messages (same path) |
mailhog -invite-jim | Chaos mode (richer per-recipient config) |
Schema rewrite
The largest test-code change is the message schema. MailHog nests the subject as Content.Headers.Subject[0] (an array) and the body as Content.Body; Mailpit exposes a flat Subject string and moves the body to Text. Every assertion that walks MailHog's nested structure must be rewritten against Mailpit's flatter shape.
Cutover steps
Chaos parity
MailHog's Jim chaos monkey (mailhog -invite-jim) injects random connection drops and slow responses at globally configured probabilities. Mailpit's Chaos mode replaces it with per-recipient configuration, so migrate resilience tests to Mailpit Chaos rather than porting Jim's flags.
Related skills
email-flow-test-author
Build-an-X for end-to-end email-flow tests - trigger → SMTP capture (via Mailpit / MailHog) → header assertions (DKIM/SPF/DMARC when relayed via real MTA) → body assertions (HTML + plain-text alternative) → link-rewrite + tracking-pixel handling → unsubscribe-link verification → bounce + complaint testing in non-prod (via Mailtrap-style services). Use when authoring tests for any transactional or marketing email flow regardless of the SMTP capture tool.
in-app-notification-test-author
Build-an-X workflow for testing real-time in-app notifications delivered over WebSocket (RFC 6455) or Server-Sent Events (WHATWG SSE spec), Firebase Realtime Database / Firestore listeners, and notification center read/unread state - covers fan-out to multiple sessions, offline-then-reconnect delivery, and ordering guarantees. Distinct from email, SMS, push, and webhook channels. Use when authoring tests for any notification that appears inside a connected web or mobile app UI without leaving the application.
mailpit-testing
Configures and runs Mailpit - modern dev-mailbox server for SMTP testing with built-in REST API for assertions; default SMTP `1025` + Web UI `8025`; ships single static binary or multi-architecture Docker images; features Chaos mode (configurable SMTP errors for resilience testing), message tagging (manual + auto via filters and plus-addressing), search filters. Use when the user develops email-sending code locally / in CI and needs SMTP capture with programmatic test assertions, or when migrating from MailHog (which Mailpit succeeds).
push-notification-test-author
Build-an-X for push notification tests (push notifications, web push, FCM / APNs push messages) across Web Push (RFC 8030 / VAPID), Apple Push Notification Service (APNs), and Firebase Cloud Messaging (FCM) - covers subscription handshake, payload encryption, badge / sound / click-action assertions, expired-subscription cleanup, silent-vs-alert, and topic-vs-targeted routing. Use when authoring tests for any push notification flow.
sms-test-author
Build-an-X for SMS-flow tests - uses Twilio Magic Numbers (`+15005550006` valid recipient, `+15005550001` invalid number, `+15005550002` cannot route, `+15005550003` international restriction, etc.) and Test Credentials for safe assertion-only Twilio interactions; covers segment-counting (GSM-7 vs UCS-2 encoding); rate-limit + opt-out keyword (STOP / HELP / UNSUBSCRIBE) handling; alphanumeric sender vs short-code vs 10DLC differences. Use when authoring tests for any Twilio-backed SMS flow.
webhook-delivery-tester
Build-an-X for webhook delivery + receiver tests per Standard Webhooks (standardwebhooks.com) - HMAC-SHA256 signature verification, retry semantics with exponential backoff + jitter, replay-window check via timestamp tolerance, ordering guarantees, dead-letter handling for permanent failures, content-type + body-encoding fidelity. Use when authoring tests for webhook senders OR receivers in any system (Stripe / Twilio / SendGrid / GitHub / GitLab outbound webhooks; SaaS app inbound webhooks).