Wholesale rename to avoid clashes with generic 'site healthcheck' plugin names on a target site: - Plugin Name: 'Site Healthcheck' → 'ATT Site Healthcheck' - Main file: site-healthcheck.php → att-site-healthcheck.php - Plugin folder: site-healthcheck → att-site-healthcheck - Admin menu slug: site-healthcheck → att-site-healthcheck - Settings slug: site-healthcheck-settings → att-site-healthcheck-settings - PHP class prefix: WPH_ → ATT_HC_ - Function prefix: wph_ → att_hc_ - Option / transient: wph_* → att_hc_* - Action/filter: wph_* → att_hc_* - CSS class prefix: wph- → att-hc- - Constants: WPH_GITEA_* → ATT_HC_GITEA_* - Class file names: class-wph-*.php → class-att-hc-*.php - Dev folder: ~/dev/wp-healthcheck → ~/dev/att-site-healthcheck Existing in-progress sessions on installs that had the old wph_session option will not migrate — they were intended for dev use only and the user has confirmed this is OK for the rename window. Smoke-tested on testsite: classes load, 14 steps discovered, save/load round-trip works, admin page renders with new att-hc- CSS classes. Recovery plugin detection unchanged — that lives in wp-site-recovery and continues to be detected by Name + Author header.
193 lines
10 KiB
PHP
193 lines
10 KiB
PHP
<?php
|
|
if (!defined('ABSPATH')) exit;
|
|
|
|
return new class extends ATT_HC_Step {
|
|
|
|
public function id(): string { return 'email_test'; }
|
|
public function title(): string { return 'Step — Email Delivery Test'; }
|
|
public function blurb(): string {
|
|
return 'Send a test email via the site\'s default mailer (wp_mail) to verify outbound delivery is working. Useful for spotting hosts that block PHP mail, or SMTP plugins that have lost auth.';
|
|
}
|
|
public function sub_items(): array {
|
|
return [
|
|
'Enter a destination address (your own inbox is fine) and click Send',
|
|
'Confirm the email arrives — check spam folder if not',
|
|
'If using an SMTP plugin (WP Mail SMTP, FluentSMTP), verify its log shows the message',
|
|
];
|
|
}
|
|
|
|
/** Inform the user which mailer would be used, before they send. */
|
|
public function autocheck(array $session_state): array {
|
|
$f = [];
|
|
|
|
// Detect SMTP / mail-routing plugins.
|
|
$smtp_plugins = [
|
|
'wp-mail-smtp/wp_mail_smtp.php' => 'WP Mail SMTP',
|
|
'fluent-smtp/fluent-smtp.php' => 'FluentSMTP',
|
|
'post-smtp/postman-smtp.php' => 'Post SMTP',
|
|
'easy-wp-smtp/easy-wp-smtp.php' => 'Easy WP SMTP',
|
|
'gmail-smtp/main.php' => 'Gmail SMTP',
|
|
'wp-ses/wp-ses.php' => 'WP Offload SES',
|
|
'sendgrid-email-delivery-simplified/wpsendgrid.php' => 'SendGrid',
|
|
];
|
|
$found = [];
|
|
if (!function_exists('is_plugin_active')) require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
|
foreach ($smtp_plugins as $file => $label) {
|
|
if (is_plugin_active($file)) $found[] = $label;
|
|
}
|
|
$f[] = $this->finding(
|
|
'mailer',
|
|
$found ? 'ok' : 'info',
|
|
'Mailer',
|
|
$found ? implode(', ', $found) : 'PHP mail() (default)',
|
|
$found ? 'Test will route through this plugin.' : 'No SMTP plugin detected — wp_mail will use PHP\'s built-in mail() function, which is often blocked on shared hosts.'
|
|
);
|
|
|
|
// Default From address (filter into wp_mail_from)
|
|
$from = apply_filters('wp_mail_from', '');
|
|
if (!$from) {
|
|
$sitename = parse_url(get_site_url(), PHP_URL_HOST) ?: 'localhost';
|
|
$sitename = preg_replace('/^www\./', '', strtolower($sitename));
|
|
$from = 'wordpress@' . $sitename;
|
|
}
|
|
$f[] = $this->finding('from_address', 'info', 'Default From', $from, 'wp_mail_from filter value.');
|
|
|
|
return $f;
|
|
}
|
|
|
|
/** Render the send-test form inside the step card. */
|
|
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="att-hc-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')); ?>">
|
|
<?php wp_nonce_field('att_hc_step_action_email_test_send_test'); ?>
|
|
<input type="hidden" name="action" value="att_hc_step_action">
|
|
<input type="hidden" name="step" value="email_test">
|
|
<input type="hidden" name="step_action" value="send_test">
|
|
<p style="margin:.2rem 0 .5rem"><strong>Send test email</strong></p>
|
|
<label>
|
|
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. 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
|
|
}
|
|
|
|
/** Send the test mail and return a finding to record the result. */
|
|
public function handle_action(string $action_name, array $input): ?array {
|
|
if ($action_name !== 'send_test') return null;
|
|
$to = isset($input['to']) ? sanitize_email((string) $input['to']) : '';
|
|
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_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_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');
|
|
$body = "This is a test email sent from the Site Healthcheck plugin.\n\n"
|
|
. "Site: " . get_site_url() . "\n"
|
|
. "Sent: " . date('Y-m-d H:i:s') . "\n"
|
|
. "Technician: " . (wp_get_current_user()->user_login ?? '?') . "\n\n"
|
|
. "If you received this, wp_mail() is working from this site.";
|
|
|
|
$sent = wp_mail($to, $subject, $body);
|
|
|
|
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 = ATT_HC_Session::current();
|
|
if ($session && $debug_requested) {
|
|
$trace = array_slice($trace, 0, 200); // cap to avoid bloating the option
|
|
$data = $session->data();
|
|
$data['email_debug'] = [
|
|
'captured_at' => time(),
|
|
'transport' => $transport,
|
|
'trace' => $trace,
|
|
];
|
|
update_option(ATT_HC_OPT_SESSION, $data, false);
|
|
|
|
// Append the trace to the step's notes so the technician can edit
|
|
// it and so it lands in the downloadable report (notes are
|
|
// already serialised verbatim).
|
|
$current_state = $session->step_state($this->id());
|
|
$current_notes = (string) $current_state['notes'];
|
|
$stamp = date('Y-m-d H:i');
|
|
$block = "\n\n--- SMTP debug trace (" . $stamp . ", transport: " . $transport . ", to: " . $to . ") ---\n";
|
|
$block .= $trace ? implode("\n", $trace) : '(no trace lines captured — transport does not emit SMTP traffic)';
|
|
$block .= "\n--- end trace ---";
|
|
// Keep whatever status the technician had previously chosen.
|
|
$session->update_step($this->id(), (string) $current_state['status'], trim($current_notes . $block));
|
|
}
|
|
|
|
$time = date('Y-m-d H:i');
|
|
if ($sent) {
|
|
$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.')
|
|
. ($debug_requested && $trace ? ' (debug trace below)' : '')
|
|
);
|
|
}
|
|
};
|