Intercepting Outgoing WordPress Email
The filter is the supported way to rewrite mail before it is sent, and it has one blind spot: a plugin that redefines the mail function entirely never fires it. That is exactly what many delivery plugins do.
This filter has one blind spot, and it is the case you are most likely to be in. wp_mail lets you rewrite an outgoing message before it is handed to the mailer. But the underlying function is pluggable: a plugin can define its own version, and a version that does not apply the filter means your callback is never called. Many delivery and SMTP plugins do exactly that, which is why a correct snippet can produce no effect at all on a site that sends mail successfully.
The array
add_filter( 'wp_mail', 'ti_wp_mail' );
function ti_wp_mail( $args ) {
return $args;
}
One argument, an associative array, returned modified:
| Key | Type | Notes |
|---|---|---|
to |
string or array | One address, a comma-separated list, or an array |
subject |
string | Plain text |
message |
string | Body; HTML only if a content type header says so |
headers |
string or array | From, Reply-To, Content-Type |
attachments |
string or array | Server paths, not URLs |
Two of those are regularly got wrong. to may already be an array, so code that concatenates a string onto it produces a malformed recipient. And attachments takes filesystem paths — passing a URL produces a message with no attachment and no error.
Redirecting mail on a staging site
The most valuable use of this filter is making sure a copy of the production database cannot mail real customers:
add_filter( 'wp_mail', 'ti_redirect_mail' );
function ti_redirect_mail( $args ) {
if ( ! defined( 'TI_STAGING' ) || ! TI_STAGING ) {
return $args;
}
$original = is_array( $args['to'] ) ? implode( ', ', $args['to'] ) : (string) $args['to'];
$args['to'] = 'staging@example.com';
$args['subject'] = '[staging → ' . $original . '] ' . $args['subject'];
return $args;
}
The constant check is the whole safety mechanism. Without it, the same file deployed to production redirects every real message to one inbox, and nobody notices until a customer asks why they heard nothing. Define the constant in the staging site's configuration only, alongside the other environment settings covered in creating a WordPress staging site.
Keeping the original recipient in the subject line is what makes the redirected mail useful rather than merely harmless.
Headers, and the sender
Headers can be supplied as an array of complete header lines:
$args['headers'] = array(
'Content-Type: text/html; charset=UTF-8',
'Reply-To: support@example.com',
);
Assigning to headers replaces whatever was there, including a content type another plugin set. Appending is safer when the existing value is already an array, and normalising a string value to an array first avoids a callback that works for some messages and mangles others.
The sender address is not a key in this array. It comes from wp_mail_from and wp_mail_from_name, or from a From: header supplied here. Setting the sender to an address on a domain the site does not control is a reliable way to have mail rejected, which is the most common cause behind WooCommerce emails not sending.
When the filter is not the problem
If mail does not arrive at all, this filter is usually the wrong place to look. The order of investigation that actually resolves things:
- Confirm the site can send anything, with a plain test message.
- Confirm the sender domain matches the site and has the records that let a receiving server accept it.
- Confirm no delivery plugin has replaced the mail function — which is also the check that explains a filter doing nothing.
- Only then look at what the filter is doing to the arguments.
Steps two and three account for most cases. A filter that quietly sets an empty recipient is real but rare by comparison, and password reset mail failing almost always turns out to be step two.
Verify by logging rather than by guessing
Log the array before returning it, with logging enabled through debug mode, then trigger a password reset. If nothing is logged, the function has been replaced and the filter is not in the path — that is the finding, and no amount of editing the callback changes it.
If the array is logged and looks right but nothing arrives, the problem is downstream at delivery. If it is logged and the recipient is empty or malformed, the callback is the cause and the array shape is where to look.
Frequently asked
- Because wp_mail is a pluggable function and a plugin has replaced it outright. A replacement is free to skip the filter entirely, so mail leaves the site without your callback being consulted.
- Yes, by rewriting every recipient to one address you control. Do it only when a staging constant is set, so the same code cannot redirect production mail if it is deployed.
- Because the sender is set by its own filters, wp_mail_from and wp_mail_from_name, unless supplied as a header. Setting a From header in this array works; setting a from key does not, because there is no such key.