Email step: optional PHPMailer SMTPDebug capture
Checkbox on the send-test form turns on PHPMailer SMTPDebug=2 for one send only. A Debugoutput callback captures the wire conversation; hook is removed immediately after wp_mail returns. AUTH credentials redacted: lines containing 'AUTH' or 16+ char base64 strings get replaced. Trace capped at 200 lines and stashed on the session. Rendered as a collapsible <details> block on the step card. Transport type (mail/smtp/etc., read from PHPMailer->Mailer) surfaced in the finding detail so the technician knows whether a trace will contain useful SMTP traffic — only the smtp transport emits any.
This commit is contained in:
@@ -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;
|
||||
?>
|
||||
<div class="wph-card" style="background:#fafafb;border-radius:6px;padding:.75rem 1rem;margin-top:.6rem">
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||||
@@ -71,9 +72,22 @@ return new class extends WPH_Step {
|
||||
To:
|
||||
<input type="email" name="to" required value="<?php echo esc_attr($default_to); ?>" style="min-width:18em">
|
||||
</label>
|
||||
<label style="margin-left:1em">
|
||||
<input type="checkbox" name="debug" value="1">
|
||||
Capture SMTP debug trace
|
||||
</label>
|
||||
<button class="button button-primary">Send</button>
|
||||
<p class="description" style="margin:.4rem 0 0">Uses <code>wp_mail()</code> with the default From address. Result is appended to the findings above.</p>
|
||||
<p class="description" style="margin:.4rem 0 0">Uses <code>wp_mail()</code> with the default From address. The debug option turns on PHPMailer <code>SMTPDebug=2</code> for this one send — useful when an SMTP plugin is routing the mail; no effect when transport is plain PHP <code>mail()</code>. Credentials in AUTH lines are redacted.</p>
|
||||
</form>
|
||||
|
||||
<?php if (is_array($debug_data) && !empty($debug_data['trace'])): ?>
|
||||
<details style="margin-top:.6rem" open>
|
||||
<summary><strong>SMTP debug trace</strong> — transport: <code><?php echo esc_html($debug_data['transport']); ?></code> · captured <?php echo esc_html(human_time_diff((int) $debug_data['captured_at'], time())); ?> ago</summary>
|
||||
<pre style="background:#0d1117;color:#c9d1d9;padding:.6rem .9rem;border-radius:6px;max-height:24em;overflow:auto;font-size:12px"><?php echo esc_html(implode("\n", $debug_data['trace'])); ?></pre>
|
||||
</details>
|
||||
<?php elseif (is_array($debug_data)): ?>
|
||||
<p class="description" style="margin-top:.6rem">SMTP debug was requested but no trace lines were captured — transport was <code><?php echo esc_html($debug_data['transport']); ?></code>. PHPMailer only emits trace output for the <code>smtp</code> transport.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
@@ -85,15 +99,38 @@ return new class extends WPH_Step {
|
||||
if (!is_email($to)) {
|
||||
return $this->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)' : '')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user