nimbly_mail_pre_send — Short-Circuit Before Delivery

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 true without 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 returning true.


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:

  1. Phase 1 — Processor::pre_send_filter() is hooked on wp_mail at priority 5. It fires nimbly_mail_pre_send and, if a handler returns true, sets an internal static flag.
  2. Phase 2 — Processor::maybe_short_circuit_send() is hooked on pre_wp_mail at priority 3. It reads the flag and returns true to WordPress, which causes wp_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_mail runs before pre_wp_mail in WordPress’s wp_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 of return false; when a handler earlier in the chain already returned true.
  • The $mailer instance may differ from what get_active_mailer() would return later — it was resolved at priority 5 and reflects any nimbly_mail_active_mailer overrides 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 true without 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 md5 hash — not cryptographically strong. Do not rely on it for security-sensitive decisions.

See Also

Was this article helpful?