Skip to main content

benecaster_email_queue_before_send

Action Free

Fires immediately before a queued email is handed to wp_mail(), carrying the message exactly as wp_mail() will receive it plus the row’s email type, show and recipient.

This is the only Benecaster hook that fires inside the request that actually sends an email, and that is what it is for. Benecaster emails are enqueued in one PHP request and sent later by WP-Cron in a different one. benecaster_email_headers fires at enqueue time, so anything it stashes in a static property, a global or any other request-scoped variable is gone by the time wp_mail() runs. If your code needs to act in the same request as the send — most commonly to bridge Benecaster’s per-email context into a mail plugin’s own outgoing-payload filter — this action is the seam to use.

Add SendGrid category tracking in API delivery mode

Premium Intermediate

Tags every outbound Benecaster email with a SendGrid category and custom arguments when WP Mail SMTP is configured with the SendGrid API mailer (Settings → WP Mail SMTP → Mailer → SendGrid → Mailer Type = API).

API mode only. When SendGrid delivers over SMTP relay, the X-SMTPAPI header approach works and this one is unnecessary — use the sibling recipe Add SendGrid category tracking to all emails. In API mode that header is treated as an ordinary RFC 5322 header, gets delivered to the recipient’s inbox, and never reaches SendGrid’s dashboard. The API path expects categories and custom_args as top-level fields on the JSON body posted to https://api.sendgrid.com/v3/mail/send, so the header trick cannot work at all. Hook both recipes in parallel if the operator may switch delivery modes.

Why an action and a filter, and why the bridge between them works. benecaster_email_queue_before_send fires immediately before that row’s wp_mail() call, carrying the message plus email_type, show_id and recipient_user_id. WP Mail SMTP’s wp_mail_smtp_providers_mailer_get_body filter fires later in the same wp_mail() call — after PHPMailer is built, after WP Mail SMTP intercepts via phpmailer_init, deep inside the SendGrid Mailer’s request assembly, with no access to Benecaster context of its own. Both run in one PHP request, which is what makes the static property a valid bridge between them.

Capture the context on benecaster_email_queue_before_send and nowhere earlier. Benecaster enqueues an email in one PHP request and sends it later from the benecaster_process_email_queue cron hook, in a different one. A filter that fires at enqueue time — benecaster_email_headers among them — cannot stash anything in a static property that survives to the send, so $pending_context would be null every time the body filter ran and no category or custom argument would ever be attached. Nothing errors when this goes wrong; it simply looks like a SendGrid configuration problem. benecaster_email_queue_before_send is the only Benecaster hook that fires inside the sending request, which is why this recipe uses it.

The action fires once per queued row, so do not treat $pending_context as request-global. One cron tick sends many emails, and each send re-populates the context immediately before its own wp_mail(). The one-shot null-ing in inject_into_body() is what stops row N’s category leaking onto row N+1 if the body filter ever fails to fire for a row.

Queue only. benecaster_mail() is a direct send that never touches the queue and does not fire this action, so ad-hoc add-on mail is not categorised by this recipe. Anything you want categorised must go through benecaster_send_email().

SMTP fallback. If the operator later flips WP Mail SMTP back to SMTP mode (or switches mailer entirely), the body filter simply won’t fire — $mailer !== 'sendgrid' on any non-SendGrid path, and the API filter doesn’t fire at all on SMTP delivery. The recipe becomes a no-op automatically.

<?php
/**
 * SendGrid API-mode category tracking.
 *
 * Requires WP Mail SMTP >= 3.0 with the SendGrid mailer set to "API" mode
 * (Settings → WP Mail SMTP → Advanced → SendGrid → Mailer Type = API).
 * In SMTP-relay mode, use the sibling recipe
 * `add-sendgrid-category-tracking-to-emails` instead.
 */
final class My_Addon_SendGrid_API_Categories {

    /** @var array<string, int|string>|null */
    private static ?array $pending_context = null;

    public static function register(): void {
        add_action( 'benecaster_email_queue_before_send', [ self::class, 'capture_context' ], 10, 4 );
        add_filter( 'wp_mail_smtp_providers_mailer_get_body', [ self::class, 'inject_into_body' ], 10, 2 );
    }

    /**
     * Capture Benecaster's per-email context immediately before the send.
     *
     * The body filter fires deep inside WP Mail SMTP's SendGrid Mailer with
     * no access to the email type / user / show, so stash it here and read
     * it back in inject_into_body().
     *
     * @param array{to: string, subject: string, body_html: string, headers: string[]} $message
     */
    public static function capture_context( array $message, string $email_type, int $show_id, int $user_id ): void {
        self::$pending_context = [
            'type'    => $email_type,
            'user_id' => $user_id,
            'show_id' => $show_id,
        ];
    }

    /**
     * Merge Benecaster's context onto the outbound SendGrid API body as
     * `categories` (array of strings) and `custom_args` (string=>string map)
     * per SendGrid's /v3/mail/send spec.
     */
    public static function inject_into_body( array $body, string $mailer ): array {
        if ( 'sendgrid' !== $mailer || null === self::$pending_context ) {
            return $body;
        }

        $ctx                   = self::$pending_context;
        self::$pending_context = null; // one-shot; the next email starts fresh

        $body['categories'] = array_values( array_unique( array_merge(
            (array) ( $body['categories'] ?? [] ),
            [ 'benecaster', 'benecaster_' . $ctx['type'] ]
        ) ) );

        // custom_args values must be strings per SendGrid's schema.
        $body['custom_args'] = array_merge(
            (array) ( $body['custom_args'] ?? [] ),
            [
                'benecaster_type' => $ctx['type'],
                'user_id'         => (string) $ctx['user_id'],
                'show_id'         => (string) $ctx['show_id'],
            ]
        );

        return $body;
    }
}

add_action( 'init', [ My_Addon_SendGrid_API_Categories::class, 'register' ] );

View on GitHub →

Parameters

Name Type Default Description
$message array The message as `wp_mail()` will receive it: `to`, `subject`, `body_html`, `headers`.
$email_type string Email type slug for the queued row (e.g. 'welcome', 'broadcast').
$show_id int ID of the show the email belongs to.
$recipient_user_id int WordPress user ID of the recipient.

Examples

Log every outgoing queued email

add_action(
    'benecaster_email_queue_before_send',
    function ( array $message, string $email_type, int $show_id, int $recipient_user_id ): void {
        error_log( sprintf(
            '[my-addon] sending %s to user %d for show %d: %s',
            $email_type,
            $recipient_user_id,
            $show_id,
            $message['subject']
        ) );
    },
    10,
    4
);

Add a header to one email type at send time

// The action cannot change the message. Register a wp_mail filter from
// inside it, and unhook it again so it applies to this email only.
add_action(
    'benecaster_email_queue_before_send',
    function ( array $message, string $email_type ): void {
        if ( 'broadcast' !== $email_type ) {
            return;
        }

        $add_header = static function ( array $args ) use ( &$add_header ): array {
            $args['headers']   = (array) ( $args['headers'] ?? [] );
            $args['headers'][] = 'X-My-Addon-Broadcast: 1';
            remove_filter( 'wp_mail', $add_header );
            return $args;
        };

        add_filter( 'wp_mail', $add_header );
    },
    10,
    2
);

Notes

It fires once per row, and one cron tick sends many rows. Context captured here is per-email, not request-global — each send re-populates it immediately before its own wp_mail() call. Anything you stash must be consumed and cleared within that one send, or row N's data will leak onto row N+1.

It is an action, not a filter. Returning a modified $message does nothing. To change the outgoing mail, register a wp_mail filter from inside the action, as in the second example above.

It is queue-only. benecaster_mail() is a direct send that never touches the queue and does not fire this action. Mail you want covered by a listener here has to go through benecaster_send_email().

The worked example embedded above carries an email's type, show and recipient into WP Mail SMTP's SendGrid API body filter — something benecaster_email_headers cannot do, because it runs in the wrong request.

Need this built rather than just documented? See our services →