wp_mail() is logged: recipient, subject, body, status (sent / failed), and timestamp. The log holds the last 100 entries and is stored as a WordPress option. Switching it off stops recording and deletes the stored entries.',
),
array(
'name' => 'Failed emails',
'rec' => 'Important',
'html' => 'A Failed status means wp_mail() returned false. The most common cause is an unconfigured SMTP server, WordPress falls back to PHP mail() which many hosts block. Configure SMTP in the SMTP Configuration panel above.',
),
array(
'name' => 'View',
'rec' => 'Optional',
'html' => 'Click View on any logged email to open the stored copy, recipient, subject, delivery route and full body, in a modal. There is no resend: use Send Test Email in the SMTP panel above to re-test delivery after changing settings. Useful for testing SMTP changes without waiting for a real event.',
),
array(
'name' => 'Privacy',
'rec' => 'Info',
'html' => 'While the log is on, email body content is stored (up to 100 KB per email) so you can view it later. That includes password reset links and login codes, which is why the log starts off: switch it on while you diagnose delivery, then switch it off again. The log is visible to administrators only and is deleted when you switch it off, click Clear Log, or uninstall the plugin.',
),
)
);
?>
This is a test email from CloudScale Cyber and Devtools.
' . 'Your SMTP configuration is working correctly.
'; $mail->send(); wp_send_json_success( array( 'to' => $to ) ); } catch ( PHPMailer\PHPMailer\Exception $e ) { // Surface the PHPMailer error plus the last few relevant SMTP conversation lines. $filtered = array_values( array_filter( $debug_log, static function ( string $line ): bool { // Skip lines that are just raw email body content. return ! preg_match( '/^(Date:|From:|To:|Subject:|MIME|Content-|Message-ID:|X-Mailer:|--[a-zA-Z0-9]+|)/i', $line ); } ) ); wp_send_json_error( array( 'type' => 'smtp', 'message' => $e->getMessage(), 'debug' => array_slice( $filtered, -12 ), ) ); } } /** * Configures PHPMailer to use SMTP with saved settings. * Hooked onto phpmailer_init when SMTP is enabled. * * @since 1.9.4 * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer PHPMailer instance (passed by reference). * @return void */ public static function phpmailer_configure( $phpmailer ): void { $phpmailer->isSMTP(); $phpmailer->Host = (string) get_option( 'csdt_devtools_smtp_host', '' ); $port = (int) get_option( 'csdt_devtools_smtp_port', 587 ); $phpmailer->Port = $port > 0 ? $port : 587; $encryption = (string) get_option( 'csdt_devtools_smtp_encryption', 'tls' ); $encryption = in_array( $encryption, array( 'tls', 'ssl', 'none' ), true ) ? $encryption : 'tls'; $phpmailer->SMTPSecure = $encryption === 'none' ? '' : $encryption; // Default auth to ON, empty/missing option means "never explicitly turned off". $auth_val = get_option( 'csdt_devtools_smtp_auth', '1' ); $phpmailer->SMTPAuth = $auth_val !== '0'; $phpmailer->Username = (string) get_option( 'csdt_devtools_smtp_user', '' ); $phpmailer->Password = (string) get_option( 'csdt_devtools_smtp_pass', '' ); $phpmailer->SMTPDebug = 0; } /** * Filter: overrides wp_mail_from with configured from email. * * @since 1.9.4 * @param string $email Default from email. * @return string */ public static function smtp_from_email( string $email ): string { $configured = get_option( 'csdt_devtools_smtp_from_email', '' ); return $configured ?: $email; } /** * Filter: overrides wp_mail_from_name with configured from name. * * @since 1.9.4 * @param string $name Default from name. * @return string */ public static function smtp_from_name( string $name ): string { $configured = get_option( 'csdt_devtools_smtp_from_name', '' ); return $configured ?: $name; } /* ================================================================== EMAIL LOG ================================================================== */ const EMAIL_LOG_OPTION = 'csdt_devtools_email_log'; const EMAIL_LOG_ENABLED_OPTION = 'csdt_devtools_email_log_enabled'; const EMAIL_LOG_MAX = 100; /** * Whether the email log records outgoing mail. Off unless an administrator * switches it on: the log keeps message bodies, and those include password * reset links and login codes, so no site should hold them without choosing to. * * @return bool */ public static function email_log_enabled(): bool { return '1' === get_option( self::EMAIL_LOG_ENABLED_OPTION, '0' ); } /** * wp_mail filter, captures outgoing email details before send. * * @since 1.9.4 * @param array $args wp_mail arguments. * @return array Unchanged. */ public static function smtp_log_capture( array $args ): array { $to = $args['to'] ?? ''; if ( is_array( $to ) ) { $to = implode( ', ', $to ); } $hdrs = $args['headers'] ?? array(); if ( is_string( $hdrs ) ) { $hdrs = array( $hdrs ); } $is_html = false; foreach ( $hdrs as $h ) { if ( stripos( $h, 'content-type' ) !== false && stripos( $h, 'text/html' ) !== false ) { $is_html = true; break; } } $body = (string) ( $args['message'] ?? '' ); if ( ! $is_html && ( strpos( $body, ' time(), 'to' => (string) $to, 'subject' => (string) ( $args['subject'] ?? '' ), 'body' => mb_substr( $body, 0, 102400 ), 'is_html' => $is_html, 'status' => 'pending', 'error' => '', 'via' => ( CSDT_Options::get( 'csdt_devtools_smtp_enabled' ) === '1' && '' !== trim( (string) get_option( 'csdt_devtools_smtp_host', '' ) ) ) ? 'smtp' : 'phpmail', ); return $args; } /** * phpmailer_init (priority 5), sets the PHPMailer action_function callback * so we receive a reliable success/failure signal after every send attempt. * * @since 1.9.4 * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer PHPMailer instance. * @return void */ public static function smtp_log_set_callback( $phpmailer ): void { $phpmailer->action_function = array( __CLASS__, 'smtp_log_on_send' ); } /** * PHPMailer action_function callback, fires after every send attempt. * * @since 1.9.4 * @param bool $is_sent Whether the send succeeded. * @param array $to Recipient addresses. * @param array $cc CC addresses (unused). * @param array $bcc BCC addresses (unused). * @param string $subject Subject line (unused, already captured). * @param string $body Message body (unused). * @param string $from Sender address (unused). * @return void */ public static function smtp_log_on_send( bool $is_sent, array $to, array $cc, array $bcc, string $subject, string $body, string $from ): void { if ( self::$smtp_log_pending === null ) { return; } $entry = self::$smtp_log_pending; $entry['status'] = $is_sent ? 'sent' : 'failed'; self::smtp_log_write( $entry ); self::$smtp_log_pending = null; } /** * wp_mail_failed action, fires when wp_mail() returns false (PHPMailer threw). * * @since 1.9.4 * @param \WP_Error $error WP_Error with PHPMailer error message. * @return void */ public static function smtp_log_on_failure( \WP_Error $error ): void { $entry = self::$smtp_log_pending ?? array( 'ts' => time(), 'to' => '', 'subject' => '(unknown)', 'status' => 'pending', 'error' => '', 'via' => 'unknown', ); $entry['status'] = 'failed'; $entry['error'] = CSDT_Error_Text::in_seconds( $error->get_error_message() ); self::smtp_log_write( $entry ); self::$smtp_log_pending = null; } /** * Prepends a log entry to the stored email log (newest-first, capped at 100). * * @since 1.9.4 * @param array $entry Log entry array. * @return void */ private static function smtp_log_write( array $entry ): void { $log = get_option( self::EMAIL_LOG_OPTION, array() ); if ( ! is_array( $log ) ) { $log = array(); } array_unshift( $log, $entry ); update_option( self::EMAIL_LOG_OPTION, array_slice( $log, 0, self::EMAIL_LOG_MAX ), false ); } /** * AJAX: clears the email log. * * @since 1.9.4 * @return void */ public static function ajax_smtp_log_clear(): void { check_ajax_referer( CSDT_DevTools::SMTP_NONCE, 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Unauthorized', 403 ); } delete_option( self::EMAIL_LOG_OPTION ); wp_send_json_success(); } /** * AJAX: switches the email log on or off. Switching it off also deletes * the stored entries, so turning the log off leaves no email content behind. * * @return void */ public static function ajax_smtp_log_toggle(): void { check_ajax_referer( CSDT_DevTools::SMTP_NONCE, 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Unauthorized', 403 ); } $enabled = isset( $_POST['enabled'] ) && '1' === sanitize_text_field( wp_unslash( $_POST['enabled'] ) ); update_option( self::EMAIL_LOG_ENABLED_OPTION, $enabled ? '1' : '0', false ); if ( ! $enabled ) { delete_option( self::EMAIL_LOG_OPTION ); } wp_send_json_success( array( 'enabled' => $enabled ) ); } /** * AJAX: returns the email log as JSON for client-side refresh. * * @since 1.9.4 * @return void */ public static function ajax_smtp_log_fetch(): void { check_ajax_referer( CSDT_DevTools::SMTP_NONCE, 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Unauthorized', 403 ); } $log = get_option( self::EMAIL_LOG_OPTION, array() ); if ( ! is_array( $log ) ) { $log = array(); } // Strip body from table-refresh payload to keep it lightweight $slim = array_map( static function ( $e ) { unset( $e['body'] ); return $e; }, $log ); wp_send_json_success( $slim ); } public static function ajax_smtp_log_view(): void { check_ajax_referer( CSDT_DevTools::SMTP_NONCE, 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Unauthorized', 403 ); } $idx = isset( $_POST['idx'] ) ? (int) $_POST['idx'] : -1; $log = get_option( self::EMAIL_LOG_OPTION, array() ); if ( ! is_array( $log ) || ! isset( $log[ $idx ] ) ) { wp_send_json_error( 'Not found' ); } wp_send_json_success( $log[ $idx ] ); } // ── Prefix migration (cs_ → csdt_devtools_) ─────────────────────────────── /** * One-time migration: renames options and user meta from the old cs_ prefix * to csdt_devtools_. Runs on every load but exits immediately after the first * successful run (guarded by a flag option). */ public static function maybe_migrate_prefix(): void { if ( get_option( 'csdt_devtools_prefix_migrated' ) ) { return; } // ── Options ────────────────────────────────────────────────────────── $option_map = array( 'cs_hide_login' => 'csdt_devtools_hide_login', 'cs_login_slug' => 'csdt_devtools_login_slug', 'cs_2fa_method' => 'csdt_devtools_2fa_method', 'cs_2fa_force_admins' => 'csdt_devtools_2fa_force_admins', 'cs_code_default_theme' => 'csdt_devtools_code_default_theme', 'cs_code_theme_pair' => 'csdt_devtools_code_theme_pair', 'cs_perf_monitor_enabled' => 'csdt_devtools_perf_monitor_enabled', 'cs_perf_debug_logging' => 'csdt_devtools_perf_debug_logging', ); foreach ( $option_map as $old => $new ) { $val = get_option( $old ); if ( $val !== false ) { update_option( $new, $val ); delete_option( $old ); } } // ── User meta (all users) ───────────────────────────────────────────── global $wpdb; $meta_map = array( 'cs_passkeys' => 'csdt_devtools_passkeys', 'cs_totp_enabled' => 'csdt_devtools_totp_enabled', 'cs_totp_secret' => 'csdt_devtools_totp_secret', 'cs_totp_secret_pending' => 'csdt_devtools_totp_secret_pending', 'cs_2fa_email_enabled' => 'csdt_devtools_2fa_email_enabled', 'cs_email_verify_pending' => 'csdt_devtools_email_verify_pending', ); foreach ( $meta_map as $old => $new ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- meta_key lookup required for SMTP log retrieval $wpdb->update( $wpdb->usermeta, array( 'meta_key' => $new ), array( 'meta_key' => $old ) ); } update_option( 'csdt_devtools_prefix_migrated', '1' ); } /** * One-time migration: renames SMTP options from the cs_devtools_ prefix * (missed by the first migration) to csdt_devtools_. */ public static function maybe_migrate_smtp_prefix(): void { if ( get_option( 'csdt_devtools_smtp_prefix_migrated' ) ) { return; } $smtp_map = array( 'cs_devtools_smtp_enabled' => 'csdt_devtools_smtp_enabled', 'cs_devtools_smtp_host' => 'csdt_devtools_smtp_host', 'cs_devtools_smtp_port' => 'csdt_devtools_smtp_port', 'cs_devtools_smtp_encryption' => 'csdt_devtools_smtp_encryption', 'cs_devtools_smtp_auth' => 'csdt_devtools_smtp_auth', 'cs_devtools_smtp_user' => 'csdt_devtools_smtp_user', 'cs_devtools_smtp_pass' => 'csdt_devtools_smtp_pass', 'cs_devtools_smtp_from_email' => 'csdt_devtools_smtp_from_email', 'cs_devtools_smtp_from_name' => 'csdt_devtools_smtp_from_name', ); foreach ( $smtp_map as $old => $new ) { $val = get_option( $old ); if ( $val !== false ) { update_option( $new, $val ); delete_option( $old ); } } update_option( 'csdt_devtools_smtp_prefix_migrated', '1' ); } /** * One-time migration: renames TOTP/2FA user meta from cs_devtools_ prefix * (missed by the first migration which used incorrect short keys) to csdt_devtools_. */ public static function maybe_migrate_usermeta_prefix(): void { if ( get_option( 'csdt_devtools_usermeta_prefix_migrated' ) ) { return; } global $wpdb; $meta_map = array( 'cs_devtools_totp_enabled' => 'csdt_devtools_totp_enabled', 'cs_devtools_totp_secret' => 'csdt_devtools_totp_secret', 'cs_devtools_totp_secret_pending' => 'csdt_devtools_totp_secret_pending', 'cs_devtools_2fa_email_enabled' => 'csdt_devtools_2fa_email_enabled', 'cs_devtools_email_verify_pending' => 'csdt_devtools_email_verify_pending', 'cs_devtools_passkeys' => 'csdt_devtools_passkeys', ); foreach ( $meta_map as $old => $new ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- meta_key lookup required for SMTP log retrieval $wpdb->update( $wpdb->usermeta, array( 'meta_key' => $new ), array( 'meta_key' => $old ) ); } update_option( 'csdt_devtools_usermeta_prefix_migrated', '1' ); } }