Email PHP Intermediate

Log All wp_mail() Calls for Debugging

Last updated: May 6, 2026

When email deliverability problems occur on a WordPress site, the first question is almost always the same: did the email actually get sent? Without any logging in place, the answer requires digging through server mail logs, if you even have access to them, or asking the recipient to check their spam folder while hoping the issue reproduces. A simple mail log built directly into WordPress answers the question immediately: you can see every outgoing email, who it was addressed to, what the subject was, and whether WordPress reported it as sent or failed.

The Code

Add this to your functions.php or a site-specific plugin. It creates a lightweight database table and hooks into three points in the mail pipeline: the outgoing filter to capture the recipient and subject, the wp_mail_succeeded action to log successful sends, and the wp_mail_failed action to log failures.

wp_mail_succeeded and wp_mail_failed

These two action hooks were added in WordPress 5.9 specifically to support mail logging. wp_mail_succeeded fires after PHPMailer successfully hands the email off to the mail server, receiving the full mail data array. wp_mail_failed fires when PHPMailer encounters an error, receiving a WP_Error object. Between them they cover the two outcomes of every send attempt.

The one complication is that wp_mail_failed doesn’t receive the original to and subject values, only the error. The snippet uses a short-lived transient to bridge this gap, storing the pending mail details before the send attempt and reading them back in the failure handler.

What Gets Logged

The log table stores four fields per entry: the recipient address or addresses (comma-separated for multiple recipients), the subject line, the status (sent or failed), and the timestamp. This is intentionally minimal, logging full email bodies raises privacy concerns and creates a large database footprint on busy sites. The recipient and subject are enough to diagnose most deliverability issues and confirm that a specific email was dispatched.

Querying the Log

View recent entries directly with a database query: SELECT * FROM wp_mail_log ORDER BY sent_at DESC LIMIT 50;. To check whether a specific user received their password reset: SELECT * FROM wp_mail_log WHERE recipient LIKE '%user@example.com%' ORDER BY sent_at DESC;. Failed emails stand out immediately with their failed status and help pinpoint whether the problem is at the WordPress layer or further down the delivery chain.

Log Retention

On active sites this table grows continuously. Add a scheduled cleanup using wp_schedule_event() to delete records older than 30 or 60 days on a weekly basis. This keeps the table size manageable while retaining enough history to be useful for debugging and auditing recent activity.

functions.php
// Create the log table
function nsl_create_mail_log_table() {
    global $wpdb;
    $table   = $wpdb->prefix . 'mail_log';
    $charset = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE IF NOT EXISTS {$table} (
        id        BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        recipient VARCHAR(320)        NOT NULL DEFAULT '',
        subject   VARCHAR(998)        NOT NULL DEFAULT '',
        status    VARCHAR(10)         NOT NULL DEFAULT 'sent',
        sent_at   DATETIME            NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY sent_at (sent_at)
    ) {$charset};";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta( $sql );
}
add_action( 'after_setup_theme', 'nsl_create_mail_log_table' );

// Log before sending
add_filter( 'wp_mail', function( $args ) {
    // Store args in a transient so the sent/failed hook can access them
    set_transient( 'nsl_pending_mail', [
        'to'      => is_array( $args['to'] ) ? implode( ', ', $args['to'] ) : $args['to'],
        'subject' => $args['subject'],
    ], 30 );
    return $args;
} );

// Log result after sending
add_action( 'wp_mail_succeeded', function( $mail_data ) {
    nsl_write_mail_log(
        is_array( $mail_data['to'] ) ? implode( ', ', $mail_data['to'] ) : $mail_data['to'],
        $mail_data['subject'],
        'sent'
    );
} );

add_action( 'wp_mail_failed', function( $wp_error ) {
    $pending = get_transient( 'nsl_pending_mail' );
    nsl_write_mail_log(
        $pending['to']      ?? 'unknown',
        $pending['subject'] ?? 'unknown',
        'failed'
    );
    delete_transient( 'nsl_pending_mail' );
} );

function nsl_write_mail_log( $recipient, $subject, $status ) {
    global $wpdb;
    $wpdb->insert(
        $wpdb->prefix . 'mail_log',
        [
            'recipient' => sanitize_text_field( $recipient ),
            'subject'   => sanitize_text_field( $subject ),
            'status'    => sanitize_text_field( $status ),
            'sent_at'   => current_time( 'mysql' ),
        ],
        [ '%s', '%s', '%s', '%s' ]
    );
}

Built by Nahnu Plugins

Need something more powerful than a snippet?

Our commercial plugins go further, built for serious WordPress sites with full support, updates, and documentation included.

Browse All Plugins →

This website uses cookies to enhance your browsing experience and ensure the site functions properly. By continuing to use this site, you acknowledge and accept our use of cookies.

Accept All Accept Required Only