Contents
Introduction
This tutorial explains, in exhaustive detail, how to intercept wp_mail() in WordPress for logging, debugging and development purposes. It covers WordPress internals relevant to email delivery, the available hooks and actions, practical examples (lightweight logger, raw MIME capture, database storage, admin viewer), ways to safely short-circuit sending in development, and operational considerations (performance, security, GDPR, rotation).
Quick summary of the approaches
- Use the wp_mail filter to capture and modify the email parameters (to, subject, body, headers, attachments) just before WordPress builds and sends the message.
- Use the wp_mail_failed action to capture failures (WP_Error) when the PHPMailer send fails.
- Use the phpmailer_init action to inspect the PHPMailer object — you can capture the raw MIME message, SMTP debug output, or adjust the transport.
- Override the pluggable wp_mail() function in a plugin (advanced) to completely replace the sending logic — useful for dev environments where you want to suppress sending entirely.
How wp_mail() works (internals relevant to interception)
The wp_mail() function is a pluggable function that builds email parameters then uses an instance of PHPMailer to send. Internally it:
- Collects inputs (to, subject, message, headers, attachments) and normalizes them into an array.
- Applies the wp_mail filter to that array — this is the earliest clear interception point that receives all send parameters.
- Creates and configures a PHPMailer instance and then calls its send() method.
- If send() fails, WordPress triggers the wp_mail_failed action with a WP_Error object describing the failure.
Additionally, WordPress exposes a phpmailer_init action which hands you the PHPMailer instance and lets you modify it or inspect it before sending. PHPMailer itself can produce a raw MIME message with preSend() / getSentMIMEMessage() and has SMTP debug output.
Basic interception: logging with the wp_mail filter
This is the simplest, least-invasive approach. You receive the canonical parameters used by wp_mail() and can log them to debug.log, a custom file, the database, or an external service.
Example: simple logger plugin
lt?php / Plugin Name: WP Mail Intercept - Simple Logger Description: Logs wp_mail() parameters to the debug log for debugging. Version: 1.0 Author: Example / add_filter( wp_mail, wpmi_log_wp_mail, 10, 1 ) function wpmi_log_wp_mail( atts ) { // atts contains: to, subject, message, headers, attachments log = array( time =gt current_time( mysql ), to =gt atts[to], subject =gt atts[subject], message =gt atts[message], headers =gt atts[headers], attachments =gt atts[attachments], url =gt ( is_admin() ? admin : ( isset(_SERVER[REQUEST_URI]) ? _SERVER[REQUEST_URI] : ) ), ) // Convert to string for php error_log (or use error_log(print_r(log, true))) error_log( [wp_mail] . print_r( log, true ) ) // Always return atts back to WordPress return atts }
Notes on the simple logger
- The logger above writes to the PHP error log (WP_DEBUG_LOG = true writes to wp-content/debug.log if configured).
- This hook runs every time wp_mail() is called — be careful on high-traffic sites.
- You can serialize the atts and write to a custom file using file_put_contents() with file locking for reliability.
Capture failures with wp_mail_failed
When PHPMailer fails to send, WordPress triggers wp_mail_failed and passes a WP_Error object. Use it to record failure reasons and full stack or context.
add_action( wp_mail_failed, wpmi_log_wp_mail_failed, 10, 1 ) function wpmi_log_wp_mail_failed( wp_error ) { if ( is_wp_error( wp_error ) ) { msg = sprintf( [wp_mail_failed] %s, wp_error->get_error_message() ) // If there are error_data entries, include them if ( data = wp_error->get_error_data() ) { msg .= data: . print_r( data, true ) } error_log( msg ) } }
Capture the raw MIME message and PHPMailer debug output (phpmailer_init)
To get the full raw email (headers and body exactly as PHPMailer will send), intercept the PHPMailer object with phpmailer_init. The safest approach is to clone the PHPMailer instance and call preSend() on the clone so you do not alter the original object state.
Example: capture raw MIME message without changing send behavior
add_action( phpmailer_init, wpmi_capture_phpmailer_mime ) function wpmi_capture_phpmailer_mime( phpmailer ) { try { // Clone so we dont alter the original PHPMailer instance clone = clone phpmailer // Build the message structures (preSend prepares headers and body) if ( method_exists( clone, preSend ) ) { clone->preSend() // getSentMIMEMessage exists in newer PHPMailer versions if ( method_exists( clone, getSentMIMEMessage ) ) { mime = clone->getSentMIMEMessage() } else { // Fallback: construct raw message from headers body mime = clone->MIMEHeader . rnrn . clone->MIMEBody } // Write the MIME to the debug log (or a custom log) error_log( [wp_mail_mime] . substr( mime, 0, 5000 ) ) // limit size in logs // Optionally store the full MIME to a file: // file_put_contents( WP_CONTENT_DIR . /mail-mime- . time() . .eml, mime ) } } catch ( Exception e ) { error_log( [wp_mail_mime_error] . e->getMessage() ) } }
Capture SMTP debug output
If WordPress is configured to use SMTP (isSMTP() is true), PHPMailer can emit verbose debug output. You can capture that with Debugoutput set to a callable.
add_action( phpmailer_init, wpmi_capture_smtp_debug ) function wpmi_capture_smtp_debug( phpmailer ) { // Only for SMTP deliveries if ( method_exists( phpmailer, isSMTP ) phpmailer-gtMailer === smtp ) { // Capture debug output in a variable debug = phpmailer-gtDebugoutput = function( str, level ) use ( debug ) { debug .= [ . level . ] . trim( str ) . PHP_EOL } phpmailer-gtSMTPDebug = 2 // 1 = commands, 2 = commands data // You could log debug after send or when preSend() was called by cloning (similar approach as above) } }
Short-circuit sending in development
On development or staging, you often want to prevent any outgoing mail from being actually delivered. There are several safe strategies:
- Use the wp_mail filter to redirect recipients to a developer address or to add a header marking the message as intercepted.
- Override the pluggable wp_mail() function in your plugin to completely replace sending logic (this prevents the core wp_mail from being declared and used). This is powerful but must be used carefully.
- Use phpmailer_init and detect a header (e.g., X-Dev-Intercept: true) and then prevent sending by substituting the transport or by returning without calling send (requires overriding wp_mail or pluggable behavior if you want to short-circuit entirely inside PHPMailer lifecycle).
Example: developer interceptor — redirect all recipients to a single address
add_filter( wp_mail, wpmi_redirect_all_mail_to_dev, 10, 1 ) function wpmi_redirect_all_mail_to_dev( atts ) { // Replace dev@example.test with the address you want to receive all emails dev_address = dev@example.test // Keep original recipient info in a header for debugging orig_to = atts[to] header = X-Original-To: . ( is_array( orig_to ) ? implode( ,, orig_to ) : orig_to ) // Normalize headers to array headers = is_array( atts[headers] ) ? atts[headers] : explode( rn, (string) atts[headers] ) headers[] = header headers[] = X-Intercepted-By: dev-interceptor atts[headers] = headers atts[to] = dev_address // send everything to the dev box return atts }
Example: override the pluggable wp_mail() to suppress delivery (dev-only)
Pluggable functions are declared only if they are not already defined. Plugins load before pluggable functions, so a plugin can define wp_mail() to replace the core function. This completely prevents WordPress core from defining its version. Use this only in development and keep it guarded by an environment flag.
lt?php / Plugin Name: WP Mail Intercept - Suppress Sender (DEV ONLY) Description: Overrides wp_mail() to suppress sending and log messages. DO NOT USE IN PRODUCTION. Version: 1.0 Author: Example / if ( ! function_exists( wp_mail ) ) { function wp_mail( to, subject, message, headers = , attachments = array() ) { // Simple log of what would have been sent log = compact( to, subject, message, headers, attachments ) error_log( [wp_mail_override] . print_r( log, true ) ) // Simulate success (do not actually send) return true } }
Warning
Overriding wp_mail() is intrusive. Keep this in code that is only active in safe environments (do NOT activate on production). Use environment checks (WP_ENV constant, server IP checks) to avoid accidental deploys.
Store email logs in the database and build an admin viewer
If you need a persistent searchable log and want centralized access for support, store the email log to a custom DB table and create an admin page to browse entries. Use dbDelta for schema creation in activation hook, and sanitize/limit stored content to avoid storing large attachments inline.
Example: create a DB table and save basic logs on send
lt?php / Plugin Name: WP Mail Intercept - DB Logger Description: Stores wp_mail() logs in a custom table and exposes a basic admin page. Version: 1.0 Author: Example / register_activation_hook( __FILE__, wpmi_db_logger_activate ) function wpmi_db_logger_activate() { global wpdb table = wpdb-gtprefix . mail_logs charset_collate = wpdb-gtget_charset_collate() sql = CREATE TABLE table ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, sent_at datetime NOT NULL, to text NOT NULL, subject text NOT NULL, message longtext NOT NULL, headers longtext NOT NULL, attachments text NULL, PRIMARY KEY (id) ) charset_collate require_once( ABSPATH . wp-admin/includes/upgrade.php ) dbDelta( sql ) } add_filter( wp_mail, wpmi_db_log_mail ) function wpmi_db_log_mail( atts ) { global wpdb table = wpdb-gtprefix . mail_logs wpdb-gtinsert( table, array( sent_at =gt current_time( mysql ), to =gt maybe_serialize( atts[to] ), subject =gt atts[subject], message =gt atts[message], headers =gt maybe_serialize( atts[headers] ), attachments =gt maybe_serialize( atts[attachments] ), ), array( %s, %s, %s, %s, %s, %s ) ) return atts } // Example: add admin menu page to view logs (simplified) add_action( admin_menu, wpmi_mail_logs_menu ) function wpmi_mail_logs_menu() { add_menu_page( Mail Logs, Mail Logs, manage_options, wpmi-mail-logs, wpmi_render_mail_logs ) } function wpmi_render_mail_logs() { global wpdb table = wpdb-gtprefix . mail_logs rows = wpdb-gtget_results( SELECT FROM table ORDER BY id DESC LIMIT 100 ) echo ltdiv class=wrapgtlth1gtMail Logslt/h1gt echo lttable class=widefatgtlttheadgtlttrgtltthgtIDlt/thgtltthgtSentlt/thgtltthgtTolt/thgtltthgtSubjectlt/thgtlt/trgtlt/theadgtlttbodygt foreach ( rows as row ) { echo lttrgt echo lttdgt . esc_html( row-gtid ) . lt/tdgt echo lttdgt . esc_html( row-gtsent_at ) . lt/tdgt echo lttdgt . esc_html( maybe_unserialize( row-gtto ) ) . lt/tdgt echo lttdgt . esc_html( row-gtsubject ) . lt/tdgt echo lt/trgt } echo lt/tbodygtlt/tablegtlt/divgt }
Notes on DB logging
- Sanitize and possibly truncate large messages and attachments metadata to avoid huge DB rows.
- Consider storing attachments as file references, not raw file contents.
- Implement retention and rotation (e.g., delete logs older than X days) to avoid unbounded growth.
Best practices and operational considerations
- Avoid logging sensitive info: Emails can contain personal data, tokens, or passwords. Mask or avoid storing sensitive fields for GDPR / privacy compliance.
- Performance: Logging synchronously on every wp_mail call may slow page loads. Consider offloading to a background job (WP-Cron, Action Scheduler) or write to an append-only file quickly and process later.
- File permissions: If you write files, ensure the destination directory is writable by the web server user and not web-accessible (store in wp-content/uploads with .htaccess to block listing or outside web root).
- Rotation: Implement log rotation or retention rules. Old email logs can accumulate quickly.
- Debug verbosity: Limit the amount of data you output into logs (truncate raw MIME or do sample captures) to avoid logs exhausting disk.
- Compatibility: PHPMailer methods like getSentMIMEMessage() exist in certain versions. Use method_exists() guards as shown above.
- Testing: Use a staging environment and real SMTP dev mailboxes (MailHog, Mailtrap, MailCatcher) for safe testing without reaching real recipients.
Testing and verifying your intercepts
- Install or activate your interceptor plugin in a staging site (do not activate on production unless intended).
- Send a test email from code, theme, or a plugin:
// Simple test send wp_mail( recipient@example.test, Test subject, This is a test message. Sent at: . current_time( mysql ) )
- Verify your debug log, DB table, or saved file contains the expected entry. If using SMTP debug capture, review the captured SMTP trace to diagnose auth/connection issues.
- If using pluggable override to suppress sending, confirm no emails leave the system (use a network inspector or MailHog).
- Test failure handling: configure wrong SMTP credentials (in a non-production environment) and confirm wp_mail_failed logs errors correctly.
Security, privacy and compliance checklist
- Do not log full user passwords, API keys, or tokens.
- Limit access to saved logs — restrict to administrators only and secure admin pages.
- If storing personal data, document processing purposes and retention periods to comply with GDPR or other privacy regulations.
- When using third-party services for logs (Sentry, external logging), ensure encrypted transport and appropriate access controls.
Troubleshooting common issues
- No hook firing: Make sure the plugin is active. Plugins load before pluggable.php so plugin-defined wp_mail will replace core if you expect the default wp_mail hook and replaced it by accident, adjust your plugin.
- PHPMailer methods not available: Guard with method_exists() because different WP/PHPMailer versions may vary.
- Raw MIME missing attachments: If you capture MIME before attachments are processed, clone and preSend should include attachments. Verify cloning strategy and PHPMailer version.
- Performance hit: If logging noticeably slows requests, write logs to an async queue (Action Scheduler) or a lightweight append-only file and process later.
Useful links
- wp_mail() reference (WordPress Developer)
- phpmailer_init action reference
- PHPMailer upstream (documentation and methods)
Final notes
By combining the wp_mail filter, wp_mail_failed action and phpmailer_init action you can capture all relevant information needed to debug email delivery in WordPress: the normalized parameters, PHPMailer internals, raw MIME, and SMTP-level traces. For non-invasive logging use the wp_mail filter for raw MIME and transport details use phpmailer_init and clone preSend. For development-only suppression, overriding the pluggable wp_mail function is pragmatic but must be used with extreme caution.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |