🔔 'Email', 'rec' => 'Recommended', 'desc' => 'Toggle on to receive alerts by email for security scans, SSH brute-force, PHP errors, and threat events. Leave the address blank to use the WordPress admin email.', ), array( 'name' => 'Telegram', 'rec' => 'Recommended', 'desc' => 'Toggle on for instant push alerts to your phone. Create a bot via @BotFather, paste the token, click Fetch Chat ID after messaging your bot. Works even when SMTP is not configured.', ), array( 'name' => 'Both', 'rec' => 'Optional', 'desc' => 'Both channels can be active simultaneously. Email provides a paper trail; Telegram delivers instant mobile alerts for critical events.', ), ) ); ?>
📧

✈

✓
true ) ); } /** * Renders the Mail / SMTP settings panel. * * @since 1.9.4 * @return void */ public static function render_smtp_panel(): void { $enabled = CSDT_Options::get( 'csdt_devtools_smtp_enabled' ) === '1'; $host = get_option( 'csdt_devtools_smtp_host', '' ); $port = get_option( 'csdt_devtools_smtp_port', 587 ); $encryption = get_option( 'csdt_devtools_smtp_encryption', 'tls' ); $auth = get_option( 'csdt_devtools_smtp_auth', '1' ) === '1'; $user = get_option( 'csdt_devtools_smtp_user', '' ); $has_pass = '' !== get_option( 'csdt_devtools_smtp_pass', '' ); $from_email = get_option( 'csdt_devtools_smtp_from_email', '' ); $from_name = get_option( 'csdt_devtools_smtp_from_name', '' ); ?>
📧 SMTP CONFIGURATION 'Enable SMTP', 'rec' => 'Recommended', 'desc' => 'Routes all WordPress emails through your own SMTP server instead of the server\'s PHP mail() function. This dramatically improves deliverability and lets you use Gmail, Outlook, or any hosted mail service.', ), array( 'name' => 'App Passwords', 'rec' => 'Note', 'html' => 'Gmail and most modern providers require an App Password, a separate password generated specifically for third-party apps, rather than your regular account password. This is required when two-factor authentication (2FA) is enabled on the account.' . '

' . 'Generate an App Password from your provider\'s security settings and paste it into the Password field below:' . '

' . 'Gmail, support.google.com/accounts/answer/185833
' . 'Outlook / Microsoft 365, support.microsoft.com, App passwords
' . 'Yahoo Mail, help.yahoo.com, Generate app passwords
' . 'Zoho Mail, zoho.com/mail/help, Two-factor authentication', ), array( 'name' => 'Send Test Email', 'rec' => 'Note', 'desc' => 'Sends a test message to the address on your own WordPress account (not the site admin email) using your current saved settings. If it fails, check that your host, port, and encryption match your provider\'s requirements (port 587 + TLS is the safest default), and that you\'re using an App Password where required.', ), ) ); ?>
•••••••• 
✓
✉️ FROM ADDRESS 'From Name & Email', 'rec' => 'Recommended', 'desc' => 'Sets the sender name and email address that recipients see in their inbox. Leave blank to keep WordPress defaults (usually the site name and admin email).', ), array( 'name' => 'SMTP Authorisation', 'rec' => 'Note', 'desc' => 'The From Email must be authorised to send via your SMTP account. Using an address your SMTP provider doesn\'t recognise will cause emails to bounce or land in spam.', ), ) ); ?>
✓
📋 EMAIL ACTIVITY LOG 'How it works', 'rec' => 'Overview', 'html' => 'The log is off by default. Once you switch on Record outgoing emails, every email WordPress sends via 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.', ), ) ); ?>
' . esc_html( $empty ) . '

'; return; } ?>
$entry ) : $bg = $i % 2 === 0 ? '#fff' : '#fafafa'; $status = $entry['status'] ?? 'unknown'; if ( $status === 'sent' ) { $badge = '✓ Sent'; } elseif ( $status === 'failed' ) { $err = ! empty( $entry['error'] ) ? ', ' . esc_html( $entry['error'] ) : ''; $badge = '✗ Failed' . esc_html( $err ) . ''; } else { $badge = ', Unknown'; } $via = $entry['via'] ?? 'phpmail'; $via_label = $via === 'smtp' ? 'SMTP' : 'PHP mail'; ?>
65535 ) { $port = 587; } // Validate: if enabling SMTP, require a host and (if auth on) credentials. if ( $enabled === '1' ) { $errors = array(); if ( $host === '' ) { $errors[] = __( 'SMTP Host is required when SMTP is enabled.', 'cloudscale-cyber-devtools' ); } if ( $auth === '1' && $user === '' ) { $errors[] = __( 'Username is required when SMTP Authentication is enabled.', 'cloudscale-cyber-devtools' ); } $existing_pass = get_option( 'csdt_devtools_smtp_pass', '' ); if ( $auth === '1' && $new_pass === '' && $existing_pass === '' ) { $errors[] = __( 'Password is required when SMTP Authentication is enabled.', 'cloudscale-cyber-devtools' ); } if ( ! empty( $errors ) ) { wp_send_json_error( implode( ' ', $errors ) ); } } update_option( 'csdt_devtools_smtp_enabled', $enabled ); update_option( 'csdt_devtools_smtp_host', $host ); update_option( 'csdt_devtools_smtp_port', $port ); update_option( 'csdt_devtools_smtp_encryption', $encryption ); update_option( 'csdt_devtools_smtp_auth', $auth ); update_option( 'csdt_devtools_smtp_user', $user ); update_option( 'csdt_devtools_smtp_from_email', $from_email ); update_option( 'csdt_devtools_smtp_from_name', $from_name ); // Only update password if the user explicitly provided one. // autoload=false keeps the SMTP password out of every request's option load. if ( $new_pass !== '' ) { update_option( 'csdt_devtools_smtp_pass', $new_pass, false ); } wp_send_json_success(); } /** * AJAX: sends a test email using current SMTP settings. * * @since 1.9.4 * @return void */ public static function ajax_smtp_test(): void { check_ajax_referer( CSDT_DevTools::SMTP_NONCE, 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'type' => 'auth' ), 403 ); } $enabled = CSDT_Options::get( 'csdt_devtools_smtp_enabled' ); $host = trim( (string) get_option( 'csdt_devtools_smtp_host', '' ) ); $port = (int) get_option( 'csdt_devtools_smtp_port', 587 ); $encryption = (string) get_option( 'csdt_devtools_smtp_encryption', 'tls' ); $auth = get_option( 'csdt_devtools_smtp_auth', '1' ) === '1'; $user = trim( (string) get_option( 'csdt_devtools_smtp_user', '' ) ); $pass = (string) get_option( 'csdt_devtools_smtp_pass', '' ); // ── Pre-flight checks ───────────────────────────────────────────── $issues = array(); if ( $enabled !== '1' ) { $issues[] = 'SMTP is not enabled, toggle it on and save first.'; } if ( $host === '' ) { $issues[] = 'SMTP Host is empty, enter your server hostname (e.g. smtp.gmail.com).'; } if ( $port <= 0 || $port > 65535 ) { $issues[] = 'Port is invalid, use 587 (TLS), 465 (SSL), or 25 (none).'; } if ( $auth && $user === '' ) { $issues[] = 'Authentication is on but Username is empty.'; } if ( $auth && $pass === '' ) { $issues[] = 'Authentication is on but no Password is saved.'; } if ( ! empty( $issues ) ) { wp_send_json_error( array( 'type' => 'preflight', 'issues' => $issues, ) ); } // ── Use PHPMailer directly so we capture real SMTP debug output ─── require_once ABSPATH . WPINC . '/PHPMailer/PHPMailer.php'; require_once ABSPATH . WPINC . '/PHPMailer/SMTP.php'; require_once ABSPATH . WPINC . '/PHPMailer/Exception.php'; $debug_log = array(); $to = wp_get_current_user()->user_email; $site = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ); try { $mail = new PHPMailer\PHPMailer\PHPMailer( true ); $mail->isSMTP(); $mail->Host = $host; $mail->Port = $port; $mail->SMTPSecure = $encryption === 'none' ? '' : $encryption; $mail->SMTPAuth = $auth; $mail->Username = $user; $mail->Password = $pass; $mail->SMTPDebug = 2; $mail->Debugoutput = static function ( string $str ) use ( &$debug_log ): void { $clean = trim( $str ); if ( $clean !== '' ) { $debug_log[] = $clean; } }; $from_email = get_option( 'csdt_devtools_smtp_from_email', '' ) ?: get_bloginfo( 'admin_email' ); $from_name = get_option( 'csdt_devtools_smtp_from_name', '' ) ?: $site; $mail->setFrom( $from_email, $from_name ); $mail->addAddress( $to ); $mail->isHTML( true ); $mail->CharSet = 'UTF-8'; $mail->Subject = sprintf( '[%s] CloudScale Cyber and Devtools, SMTP Test', $site ); $mail->Body = '

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' ); } }