diff --git a/includes/steps/115-email.php b/includes/steps/115-email.php index 75d4b82..ea57a8a 100644 --- a/includes/steps/115-email.php +++ b/includes/steps/115-email.php @@ -59,6 +59,7 @@ return new class extends WPH_Step { public function render_extra(array $session_state): void { $current = wp_get_current_user(); $default_to = $current && $current->user_email ? $current->user_email : get_option('admin_email'); + $debug_data = $session_state['email_debug'] ?? null; ?>
@@ -71,9 +72,22 @@ return new class extends WPH_Step { To: + -

Uses wp_mail() with the default From address. Result is appended to the findings above.

+

Uses wp_mail() with the default From address. The debug option turns on PHPMailer SMTPDebug=2 for this one send — useful when an SMTP plugin is routing the mail; no effect when transport is plain PHP mail(). Credentials in AUTH lines are redacted.

+ + +
+ SMTP debug trace — transport: · captured ago +
+
+ +

SMTP debug was requested but no trace lines were captured — transport was . PHPMailer only emits trace output for the smtp transport.

+
finding('last_send', 'bad', 'Last test send', 'invalid address', 'The destination address was not a valid email.'); } + $debug_requested = !empty($input['debug']); // Capture any wp_mail_failed error so we can surface the underlying reason. $error_msg = ''; - $capture = function ($wp_error) use (&$error_msg) { + $capture_error = function ($wp_error) use (&$error_msg) { if (is_object($wp_error) && method_exists($wp_error, 'get_error_message')) { $error_msg = (string) $wp_error->get_error_message(); } }; - add_action('wp_mail_failed', $capture); + add_action('wp_mail_failed', $capture_error); + + // Optional: turn on PHPMailer SMTP debug for this one send. Only useful + // when transport is 'smtp' — for plain PHP mail() there is no wire + // conversation to capture. + $trace = []; + $transport = 'mail'; + $debug_hook = null; + if ($debug_requested) { + $debug_hook = function ($phpmailer) use (&$trace, &$transport) { + $transport = isset($phpmailer->Mailer) ? (string) $phpmailer->Mailer : 'mail'; + $phpmailer->SMTPDebug = 2; // 0=off, 1=client, 2=client+server, 3=connection, 4=low-level + $phpmailer->Debugoutput = function ($line, $level) use (&$trace) { + // Redact AUTH credentials — PHPMailer emits the base64'd username/password + // when SMTPDebug captures the AUTH LOGIN handshake. + if (stripos($line, 'AUTH') !== false || preg_match('/^[A-Za-z0-9+\/=]{16,}$/', trim($line))) { + $line = '[REDACTED — credential or response containing it]'; + } + $trace[] = trim($line); + }; + }; + add_action('phpmailer_init', $debug_hook, 1000); + } $host = parse_url(get_site_url(), PHP_URL_HOST) ?: 'site'; $subject = '[Site Healthcheck] Test from ' . $host . ' — ' . date('Y-m-d H:i'); @@ -104,24 +141,39 @@ return new class extends WPH_Step { . "If you received this, wp_mail() is working from this site."; $sent = wp_mail($to, $subject, $body); - remove_action('wp_mail_failed', $capture); + + remove_action('wp_mail_failed', $capture_error); + if ($debug_hook) remove_action('phpmailer_init', $debug_hook, 1000); + + // Stash the debug trace on the session so the UI can render it as a + // collapsible block. Returning it inside the finding's detail would + // mangle the formatting. + $session = WPH_Session::current(); + if ($session && $debug_requested) { + $data = $session->data(); + $data['email_debug'] = [ + 'captured_at' => time(), + 'transport' => $transport, + 'trace' => array_slice($trace, 0, 200), // cap to avoid bloating the option + ]; + update_option(WPH_OPT_SESSION, $data, false); + } $time = date('Y-m-d H:i'); if ($sent) { - return $this->finding( - 'last_send', - 'ok', - 'Last test send', - 'sent to ' . $to . ' at ' . $time, - 'wp_mail() returned true. Confirm receipt in the inbox (check spam too).' - ); + $detail = 'wp_mail() returned true — the transport accepted handoff. This does not guarantee inbox delivery; confirm receipt (check spam too).'; + if ($debug_requested) { + $detail .= ' SMTP debug ' . ($transport === 'smtp' ? 'captured below — ' . count($trace) . ' line(s).' : 'requested but transport is "' . $transport . '" (no SMTP conversation to capture).'); + } + return $this->finding('last_send', 'ok', 'Last test send', 'sent to ' . $to . ' at ' . $time, $detail); } return $this->finding( 'last_send', 'bad', 'Last test send', 'failed at ' . $time, - $error_msg ?: 'wp_mail() returned false but no error was captured. Check the host\'s mail logs.' + ($error_msg ?: 'wp_mail() returned false but no error was captured. Check the host\'s mail logs.') + . ($debug_requested && $trace ? ' (debug trace below)' : '') ); } };