Catching WordPress Emails Locally with MailHog (No More Test Mail Hitting Real Inboxes)
Every WordPress site sends more mail than people think: password resets, contact form notifications, WooCommerce order confirmations, plugin alerts. That’s fine in production. It’s a liability in local development — the last thing you want is a bug in a test script firing a “your order has shipped” email at an actual customer, or a password-reset flood landing in a real inbox while you’re debugging a form.
MailHog solves this cleanly: it’s a local SMTP server that catches every outgoing email instead of delivering it, and gives you a web UI to inspect what WordPress actually sent — headers, body, attachments — without any of it leaving your machine.
Here’s how it’s set up on a Laravel Valet local WordPress install.
The Setup
| Component | Details |
|---|---|
| Web Server | Laravel Valet |
| MailHog | Installed via Homebrew, running as a service |
| SMTP Port | 1025 |
| Web UI Port | 8025 |
| WordPress Config | Must-use plugin in wp-content/mu-plugins/ |
MailHog was already available via Homebrew:
brew list | grep mailhog
ps aux | grep mailhog
It runs as a persistent background process — it survives Valet restarts, so there’s no per-session setup once it’s running.
Wiring WordPress to MailHog
WordPress sends mail through wp_mail(), which under the hood uses PHPMailer. Rather than touch PHP’s sendmail_path or reach for a full SMTP plugin, a small must-use plugin is enough to redirect PHPMailer at MailHog’s SMTP port:
<?php
/**
* Plugin Name: MailHog SMTP Override
* Description: Configures WordPress to send emails through MailHog SMTP (localhost:1025)
* Version: 1.0
*/
if (!defined('ABSPATH')) {
exit;
}
add_action('phpmailer_init', 'mailhog_configure_smtp');
function mailhog_configure_smtp($phpmailer) {
$phpmailer->isSMTP();
$phpmailer->Hostname = 'example.test';
$phpmailer->Host = 'localhost';
$phpmailer->Port = 1025;
$phpmailer->SMTPSecure = false;
$phpmailer->SMTPAutoTLS = false;
$phpmailer->SMTPAuth = false;
if (empty($phpmailer->From)) {
$phpmailer->From = 'noreply@example.test';
}
if (empty($phpmailer->FromName)) {
$phpmailer->FromName = 'Example Site';
}
}
Save that as wp-content/mu-plugins/mailhog-smtp.php and it loads automatically — must-use plugins don’t need activating, and they can’t accidentally get deactivated mid-debug session either, which matters here since the whole point is that this stays on for every local request.
No authentication, no TLS — MailHog doesn’t need either since nothing actually leaves localhost.
Verifying It Works
wp eval-file /dev/stdin <<'PHP'
<?php
$to = 'test@example.com';
$subject = 'MailHog Test Email';
$message = 'This is a test email sent through MailHog SMTP';
$result = wp_mail($to, $subject, $message);
echo "Email sent: " . ($result ? "SUCCESS" : "FAILED") . "\n";
PHP
Then open http://localhost:8025 — the email shows up in the inbox immediately, with full headers and body, and it never touched test@example.com for real.
Day-to-Day Use
View emails: http://localhost:8025 — click any message for headers, body, and attachments.
Clear the inbox via the UI, or via the API:
curl -X DELETE http://localhost:8025/api/v1/messages
Check message count (useful in a script or CI-style check after a test run):
curl -s http://localhost:8025/api/v2/messages | jq '.total'
Multiple recipients and HTML email both work exactly as they would against a real SMTP server — MailHog doesn’t need special-casing for either:
wp_mail('user1@example.com, user2@example.com', 'Subject', 'Message');
wp_mail('test@example.com', 'HTML Email', '<h1>Hello</h1><p>This is HTML</p>');
If Emails Don’t Show Up
Work through these in order — in practice it’s almost always the first one:
- Is MailHog actually running?
ps aux | grep mailhog - Is something listening on 1025?
lsof -i :1025 - Manual SMTP smoke test:
telnet localhost 1025Type
HELO test, thenQUIT— if that hangs or refuses, the problem is MailHog itself, not WordPress. - Check the WordPress debug log for a PHPMailer error the mu-plugin didn’t catch:
tail -f wp-content/debug.log
If MailHog isn’t running at all, mailhog & starts it manually, or brew services start mailhog if it’s registered as a service. Port conflicts on 1025/8025 are the other common failure — lsof -i :1025 / lsof -i :8025 to find the PID, kill -9 it, then restart MailHog.
Alternatives
If a full SMTP plugin fits the project better than a bare mu-plugin, WP Mail SMTP points at the same MailHog port through its own settings screen (Mailer: Other SMTP, Host: localhost, Port: 1025, Encryption: None, Authentication: Off) — same destination, more UI for anyone who’d rather not touch PHP.
The one case a phpmailer_init hook can’t catch: code that bypasses wp_mail() entirely and calls PHP’s mail() directly. That needs sendmail_path overridden in php.ini instead, which is more invasive and worth avoiding unless something in your stack actually forces it.
Summary
- MailHog catches outgoing WordPress mail locally instead of delivering it — no risk of test runs emailing real people.
- A single
phpmailer_initmu-plugin is enough to redirectwp_mail()at MailHog’s SMTP port; no plugin activation, no config file changes. - The web UI (
localhost:8025) and its REST API cover everything you need day to day: inspecting, clearing, and counting captured mail. - Keep MailHog running as a background service so it survives Valet restarts and there’s nothing to remember to start.
This is the same kind of local-environment hygiene we set up as part of managed WordPress hosting work at Imagewize — getting a dev environment that behaves like production without any of the production risk. If you’re setting up a WordPress local dev workflow and want a hand, get in touch.
Find me on Mastodon at @jfrumau@mastodon.social if you’ve got a MailHog or local-mail setup worth comparing notes on.