Type: Filter
Arguments: 3
Fired in: Processor::pre_send_filter() on the wp_mail filter at priority 5
Since: 1.0.0
Overview
Fires early in the wp_mail chain, before Logger captures the attempt and before PHPMailer is invoked. Returning true from any handler signals that the handler has accepted responsibility for the message — NimblyMail will then return true to wp_mail()‘s caller without delivering the email itself.
Caution
Silent-drop risk. Returning
truewithout actually delivering or persisting the message silently drops the email. Your handler MUST either deliver the message itself, persist it for later delivery, or explicitly log the deferral before returningtrue.
Signature
/**
* @param bool $short_circuit Default false — allow normal delivery.
* @param array $atts The wp_mail() $atts array:
* - to string|array
* - subject string
* - message string
* - headers string|array
* - attachments array
* @param AbstractMailer $mailer The currently-selected active mailer.
*/
$short_circuit = apply_filters( 'nimbly_mail_pre_send', false, $atts, $mailer );
Hook Ordering
This filter fires through a two-phase mechanism:
- Phase 1 —
Processor::pre_send_filter()is hooked onwp_mailat priority 5. It firesnimbly_mail_pre_sendand, if a handler returnstrue, sets an internal static flag. - Phase 2 —
Processor::maybe_short_circuit_send()is hooked onpre_wp_mailat priority 3. It reads the flag and returnstrueto WordPress, which causeswp_mail()to return early without invoking PHPMailer.
| Priority / Hook | Handler |
|---|---|
wp_mail — 5 |
Processor::pre_send_filter() — nimbly_mail_pre_send fires here |
pre_wp_mail — 1 |
Processor::maybe_block_email() — Do Not Send |
pre_wp_mail — 3 |
Processor::maybe_short_circuit_send() — short-circuit executes here |
wp_mail — PHP_INT_MAX |
Logger::capture_email() |
wp_mail() continues |
PHPMailer / phpmailer_init / configure_transport |
Note
Because
wp_mailruns beforepre_wp_mailin WordPress’swp_mail()function, Phase 1 (the evaluation) happens first, then Phase 2 (the stop) executes before PHPMailer is set up.
Behaviour
| Handler returns | Result |
|---|---|
false (default) |
Normal delivery proceeds |
true |
wp_mail() returns true; no PHPMailer send; no Logger row written |
Bypass Mechanism
When your handler defers a message and later re-submits it via wp_mail(), you must mark it for bypass so the second pass is not short-circuited again:
// Before re-submitting the queued message:
NimblyMailProcessor::mark_bypass( $atts );
wp_mail( $atts['to'], $atts['subject'], $atts['message'], $atts['headers'], $atts['attachments'] );
The bypass marker lives in a static, request-scoped property and is cleared automatically after the next pre_send_filter() pass for that message. The key is derived from an md5 of { to, subject, message }.
Example — Rate Limiting (Enqueue and Defer)
<?php
/**
* Plugin Name: My Rate Limiter (mu-plugin example)
*/
use NimblyMailProcessor;
add_filter(
'nimbly_mail_pre_send',
function ( bool $short_circuit, array $atts, $mailer ): bool {
if ( $short_circuit ) {
// Already intercepted by another handler — don't override.
return true;
}
$slug = $mailer->get_slug();
$key = 'my_rate_limit_' . $slug;
$count = (int) get_transient( $key );
// Allow max 10 sends per minute per mailer.
if ( $count >= 10 ) {
// Persist the message for later delivery.
$queue = get_option( 'my_queued_emails', array() );
$queue[] = $atts;
update_option( 'my_queued_emails', $queue, false );
// Log the deferral for observability.
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( '[my-rate-limiter] deferred email to ' . ( $atts['to'] ?? '' ) );
}
return true; // Signal: "I've taken it."
}
set_transient( $key, $count + 1, MINUTE_IN_SECONDS );
return false; // Normal delivery.
},
10,
3
);
/**
* Drain the queue — e.g. via a cron job.
*/
function my_drain_queue(): void {
$queue = get_option( 'my_queued_emails', array() );
if ( empty( $queue ) ) {
return;
}
// Take the first message.
$atts = array_shift( $queue );
update_option( 'my_queued_emails', $queue, false );
// Mark it for bypass so our rate-limit handler lets it through.
Processor::mark_bypass( $atts );
wp_mail(
$atts['to'],
$atts['subject'],
$atts['message'],
$atts['headers'] ?? array(),
$atts['attachments'] ?? array()
);
}
add_action( 'my_drain_queue_cron', 'my_drain_queue' );
Common Pitfalls
- Always return the first argument unchanged if you are not intercepting. Use
return $short_circuit;instead ofreturn false;when a handler earlier in the chain already returnedtrue. - The
$mailerinstance may differ from whatget_active_mailer()would return later — it was resolved at priority 5 and reflects anynimbly_mail_active_maileroverrides at that point. - No Logger row is written when you short-circuit. Your handler is fully responsible for persistence and observability.
mark_bypass()uses{ to, subject, message }as the key. If any of these fields changes between the original send and the re-submission, the bypass check will fail and the message will be deferred again.
Security Notes
- A handler that returns
truewithout delivering is a silent-drop vulnerability. Always log the deferral and schedule a delivery attempt. - Do not use this hook to block emails as a spam filter — the Logger captures an attempt before this filter runs (at
PHP_INT_MAX), but only if the message is not short-circuited first. - The bypass key is an
md5hash — not cryptographically strong. Do not rely on it for security-sensitive decisions.
See Also
nimbly_mail_active_mailer— override the mailer per-sendnimbly_mail_mail_sent— react after a successful send- Extensibility Overview
