Full Email Log With Search, Viewer and Resend
Records every outgoing email in its own database table and adds Tools > Mail Log, where you can search, read the full message, resend it, and clear old entries automatically.
Opens WPIDE on
Code
$wpide_mlog_days = 30; // Days to keep entries.
$wpide_mlog_state = new stdClass();
$wpide_mlog_state->id = 0;
add_action( 'admin_init', function () {
global $wpdb;
if ( '1' === get_option( 'wpide_mail_log_db' ) || ! current_user_can( 'manage_options' ) ) {
return;
}
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( "CREATE TABLE {$wpdb->prefix}wpide_mail_log (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
sent_at datetime NOT NULL,
to_addr text NOT NULL,
subject text NOT NULL,
headers text NOT NULL,
message longtext NOT NULL,
status varchar(20) NOT NULL DEFAULT 'queued',
error text NOT NULL,
PRIMARY KEY (id),
KEY sent_at (sent_at)
) {$wpdb->get_charset_collate()};" );
update_option( 'wpide_mail_log_db', '1', true );
} );
add_filter( 'wp_mail', function ( $atts ) use ( $wpide_mlog_state ) {
global $wpdb;
if ( '1' !== get_option( 'wpide_mail_log_db' ) ) {
return $atts;
}
$wpdb->insert( $wpdb->prefix . 'wpide_mail_log', array(
'sent_at' => current_time( 'mysql', true ),
'to_addr' => implode( ', ', (array) $atts['to'] ),
'subject' => (string) $atts['subject'],
'headers' => is_array( $atts['headers'] ) ? implode( "\n", $atts['headers'] ) : (string) $atts['headers'],
'message' => (string) $atts['message'],
'status' => 'queued',
'error' => '',
) );
$wpide_mlog_state->id = (int) $wpdb->insert_id;
return $atts;
}, PHP_INT_MAX );
add_action( 'wp_mail_succeeded', function () use ( $wpide_mlog_state ) {
global $wpdb;
if ( $wpide_mlog_state->id ) {
$wpdb->update( $wpdb->prefix . 'wpide_mail_log', array( 'status' => 'sent' ), array( 'id' => $wpide_mlog_state->id ) );
$wpide_mlog_state->id = 0;
}
} );
add_action( 'wp_mail_failed', function ( $error ) use ( $wpide_mlog_state ) {
global $wpdb;
if ( $wpide_mlog_state->id ) {
$wpdb->update( $wpdb->prefix . 'wpide_mail_log', array( 'status' => 'failed', 'error' => $error->get_error_message() ), array( 'id' => $wpide_mlog_state->id ) );
$wpide_mlog_state->id = 0;
}
} );
add_action( 'init', function () {
if ( ! wp_next_scheduled( 'wpide_mail_log_purge' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'wpide_mail_log_purge' );
}
} );
add_action( 'wpide_mail_log_purge', function () use ( $wpide_mlog_days ) {
global $wpdb;
if ( '1' === get_option( 'wpide_mail_log_db' ) ) {
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}wpide_mail_log WHERE sent_at < %s", gmdate( 'Y-m-d H:i:s', time() - $wpide_mlog_days * DAY_IN_SECONDS ) ) );
}
} );
add_action( 'admin_menu', function () {
add_management_page( 'Mail Log', 'Mail Log', 'manage_options', 'wpide-mail-log', function () {
global $wpdb;
$table = $wpdb->prefix . 'wpide_mail_log';
$base = admin_url( 'tools.php?page=wpide-mail-log' );
echo '<div class="wrap"><h1>Mail Log</h1>';
if ( '1' !== get_option( 'wpide_mail_log_db' ) ) {
echo '<p>The log table is being created. Reload this page.</p></div>';
return;
}
// Actions.
if ( isset( $_GET['resend'] ) && check_admin_referer( 'wpide_mlog_resend' ) ) {
$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", absint( $_GET['resend'] ) ) );
if ( $row ) {
$ok = wp_mail( array_map( 'trim', explode( ',', $row->to_addr ) ), $row->subject, $row->message, array_filter( explode( "\n", $row->headers ) ) );
printf( '<div class="notice notice-%s"><p>%s</p></div>', $ok ? 'success' : 'error', $ok ? 'Email resent.' : 'The email could not be resent.' );
}
}
if ( isset( $_POST['wpide_mlog_clear'] ) && check_admin_referer( 'wpide_mlog_clear' ) ) {
$wpdb->query( "DELETE FROM {$table}" ); // phpcs:ignore
echo '<div class="notice notice-success"><p>Log cleared.</p></div>';
}
// Single message view.
if ( isset( $_GET['view'] ) ) {
$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", absint( $_GET['view'] ) ) );
if ( $row ) {
printf( '<p><a href="%s">← Back to the log</a></p>', esc_url( $base ) );
printf( '<table class="widefat striped"><tbody><tr><th>To</th><td>%s</td></tr><tr><th>Subject</th><td>%s</td></tr><tr><th>Date (UTC)</th><td>%s</td></tr><tr><th>Status</th><td>%s %s</td></tr><tr><th>Headers</th><td><pre style="white-space:pre-wrap">%s</pre></td></tr></tbody></table>', esc_html( $row->to_addr ), esc_html( $row->subject ), esc_html( $row->sent_at ), esc_html( $row->status ), esc_html( $row->error ), esc_html( $row->headers ) );
echo '<h2>Message</h2><iframe sandbox="" style="width:100%;min-height:420px;background:#fff;border:1px solid #c3c4c7" srcdoc="' . esc_attr( false !== stripos( $row->message, '<html' ) || false !== stripos( $row->headers, 'text/html' ) ? $row->message : '<pre style="white-space:pre-wrap;font:14px/1.5 sans-serif">' . esc_html( $row->message ) . '</pre>' ) . '"></iframe>';
printf( '<p><a class="button" href="%s">Resend this email</a></p></div>', esc_url( wp_nonce_url( add_query_arg( 'resend', (int) $row->id, $base ), 'wpide_mlog_resend' ) ) );
return;
}
}
$search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
$paged = max( 1, isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1 );
$where = $search ? $wpdb->prepare( 'WHERE to_addr LIKE %s OR subject LIKE %s', '%' . $wpdb->esc_like( $search ) . '%', '%' . $wpdb->esc_like( $search ) . '%' ) : '';
$rows = $wpdb->get_results( "SELECT id, sent_at, to_addr, subject, status FROM {$table} {$where} ORDER BY id DESC LIMIT 50 OFFSET " . ( ( $paged - 1 ) * 50 ) ); // phpcs:ignore
printf( '<form method="get"><input type="hidden" name="page" value="wpide-mail-log"><p class="search-box"><input type="search" name="s" value="%s" placeholder="Recipient or subject"> <button class="button">Search</button></p></form>', esc_attr( $search ) );
echo '<table class="widefat striped"><thead><tr><th>Date (UTC)</th><th>To</th><th>Subject</th><th>Status</th><th></th></tr></thead><tbody>';
if ( ! $rows ) {
echo '<tr><td colspan="5">No emails logged yet.</td></tr>';
}
foreach ( $rows as $row ) {
printf( '<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td><a href="%s">View</a></td></tr>', esc_html( $row->sent_at ), esc_html( $row->to_addr ), esc_html( $row->subject ), esc_html( $row->status ), esc_url( add_query_arg( 'view', (int) $row->id, $base ) ) );
}
echo '</tbody></table>';
if ( 50 === count( $rows ) ) {
printf( '<p><a class="button" href="%s">Older emails →</a></p>', esc_url( add_query_arg( array( 'paged' => $paged + 1, 's' => $search ), $base ) ) );
}
echo '<form method="post" style="margin-top:1em">';
wp_nonce_field( 'wpide_mlog_clear' );
echo '<button class="button button-link-delete" name="wpide_mlog_clear" value="1" onclick="return confirm(\'Delete all log entries?\')">Clear the log</button></form></div>';
} );
} );
Notes
Creates the {prefix}wpide_mail_log table the first time an administrator opens the admin and purges old entries daily; message bodies can hold personal data, so keep the retention short.
How to add this snippet
- Install WPIDE on your WordPress site (it is free) and open Code Snippets.
- Use snippet above adds it from the library in one click. Or copy the code into a new snippet with the same code type.
- It is added inactive: review the code and settings, then switch it on.