diff --git a/src/Admin/GlobalController.php b/src/Admin/GlobalController.php index d9a2d412..fc759f2c 100644 --- a/src/Admin/GlobalController.php +++ b/src/Admin/GlobalController.php @@ -176,13 +176,11 @@ class GlobalController extends ControllerBase { return $this->error(__('请输入有效的邮箱地址', 'aksrt')); } - $sent = class_exists('\\AKSRT\\Settings\\Mail') - ? \AKSRT\Settings\Mail::send_test_email($to) - : false; - if ($sent) { - return $this->success(['message' => '测试邮件已发送']); + $result = \AKSRT\Settings\Mail::send_test_email($to); + if (is_wp_error($result)) { + return $this->error(__('邮件发送失败,详细原因已写入服务器错误日志', 'aksrt')); } - return $this->error(__('邮件发送失败,请检查 SMTP 配置', 'aksrt')); + return $this->success(['message' => '测试邮件已发送']); } public function rest_homepage_tab_posts(\WP_REST_Request $request): \WP_REST_Response { diff --git a/src/Settings/Mail.php b/src/Settings/Mail.php index 6be372bf..27bfb740 100644 --- a/src/Settings/Mail.php +++ b/src/Settings/Mail.php @@ -10,24 +10,21 @@ namespace AKSRT\Settings; defined('ABSPATH') || exit; final class Mail { - /** @return list */ - public static function events(): array { - return array_keys(MailTemplates::definitions()); - } - /** @return array */ public static function get_smtp(): array { $s = Settings::get_all(); + $username = trim((string) ($s['mail_smtp_name'] ?? '')) ?: sanitize_email((string) ($s['mail_name'] ?? '')); + $password = (string) ($s['mail_passwd'] ?? ''); return [ 'enabled' => !empty($s['mail_smtps']), 'host' => trim((string) ($s['mail_host'] ?? '')), 'port' => max(1, min(65535, (int) ($s['mail_port'] ?? 465))), - 'username' => trim((string) ($s['mail_smtp_name'] ?? '')) ?: sanitize_email((string) ($s['mail_name'] ?? '')), - 'password' => (string) ($s['mail_passwd'] ?? ''), + 'username' => $username, + 'password' => $password, 'encryption' => in_array(($s['mail_smtpsecure'] ?? 'ssl'), ['ssl', 'tls', ''], true) ? (string) ($s['mail_smtpsecure'] ?? 'ssl') : 'ssl', 'from_name' => trim((string) ($s['mail_showname'] ?? '')) ?: get_bloginfo('name'), 'from_address' => sanitize_email((string) ($s['mail_name'] ?? '')), - 'auth' => !empty($s['mail_smtpauth']), + 'auth' => $username !== '' && $password !== '', ]; } @@ -88,16 +85,25 @@ final class Mail { $definition = MailTemplates::definitions()[$event]; $subject = (string) ($cfg['subject'] ?? $default_subject ?: $definition['title']); $content = (string) ($cfg['content'] ?? $default_content ?: $definition['content']); - $subject = self::replace_placeholders($subject, $placeholders, false); - $content = self::replace_placeholders($content, $placeholders, false); + $subject = self::replace_placeholders($subject, $placeholders); + $content = self::replace_placeholders($content, $placeholders); $content = self::wrap_html($content, $cfg['global'] ?? []); $headers = ['Content-Type: text/html; charset=UTF-8']; return (bool) wp_mail($to, $subject, $content, $headers); } - public static function send_test_email(string $to): bool { + public static function send_test_email(string $to): bool|\WP_Error { $to = sanitize_email($to); - if ($to === '' || !is_email($to)) return false; + if ($to === '' || !is_email($to)) { + return new \WP_Error('aksrt_invalid_test_email', __('请输入有效的邮箱地址', 'aksrt')); + } + + $failure = null; + $capture_failure = static function (\WP_Error $error) use (&$failure): void { + $failure = $error; + }; + add_action('wp_mail_failed', $capture_failure); + $site = esc_html((string) get_bloginfo('name')); $subject = '邮件配置测试'; $content = "您好,\n\n这是一封来自 {$site} 的邮件配置测试。"; @@ -105,7 +111,22 @@ final class Mail { 'description' => '此信为系统邮件,请不要直接回复。', 'more_content' => '', ]); - return (bool) wp_mail($to, $subject, $content, ['Content-Type: text/html; charset=UTF-8']); + + try { + $sent = (bool) wp_mail($to, $subject, $content, ['Content-Type: text/html; charset=UTF-8']); + } finally { + remove_action('wp_mail_failed', $capture_failure); + } + + if ($sent) { + return true; + } + + $error = $failure instanceof \WP_Error + ? $failure + : new \WP_Error('aksrt_mail_failed', __('wp_mail 返回失败,但未提供详细错误。', 'aksrt')); + self::log_test_failure($error); + return $error; } /** @return array */ @@ -121,20 +142,23 @@ final class Mail { ]; } - private static function replace_placeholders(string $value, array $placeholders, bool $html): string { + private static function replace_placeholders(string $value, array $placeholders): string { $replace = []; foreach ($placeholders as $key => $replacement) { - $replacement = (string) $replacement; - if ($html) { - $replacement = str_contains($key, 'url') ? esc_url($replacement) : esc_html($replacement); - } else { - $replacement = wp_strip_all_tags($replacement); - } - $replace[$key] = $replacement; + $replace[$key] = wp_strip_all_tags((string) $replacement); } return strtr($value, $replace); } + private static function log_test_failure(\WP_Error $error): void { + $message = sanitize_text_field($error->get_error_message()); + $message = preg_replace('/[A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,}/i', '[email]', $message); + $message = preg_replace('/\b(?:[a-z0-9-]+\.)+[a-z]{2,}\b/i', '[host]', (string) $message); + $message = preg_replace('/\b(?:\d{1,3}\.){3}\d{1,3}\b/', '[ip]', (string) $message); + $message = preg_replace('/\b(?:password|passwd|username|user|login|credential)\s*[:=]\s*[^\s,;]+/i', '$1=[redacted]', (string) $message); + error_log('[AKSRT] SMTP 测试失败:' . substr((string) $message, 0, 500)); + } + private static function wrap_html(string $content, array $global): string { $logo = method_exists(Settings::class, 'logo_url') ? Settings::logo_url() : ''; $background = esc_url(AKSRT_URI . '/assets/img/mail-bg.png'); diff --git a/src/Settings/MailTemplates.php b/src/Settings/MailTemplates.php index 3c666f4a..90279b2a 100644 --- a/src/Settings/MailTemplates.php +++ b/src/Settings/MailTemplates.php @@ -62,7 +62,6 @@ final class MailTemplates { 'mail_smtp_name' => '', 'mail_host' => '', 'mail_port' => 465, - 'mail_smtpauth' => true, 'mail_smtpsecure' => 'ssl', 'mail_showname' => $site, 'mail_title_prefix' => '[' . $site . ']', @@ -89,7 +88,6 @@ final class MailTemplates { ['key' => 'mail_smtp_name', 'label' => 'SMTP 用户名', 'type' => 'input', 'default' => $defaults['mail_smtp_name'], 'dependsOn' => ['key' => 'mail_smtps', 'value' => true]], ['key' => 'mail_host', 'label' => '邮件服务器地址', 'type' => 'input', 'default' => $defaults['mail_host'], 'dependsOn' => ['key' => 'mail_smtps', 'value' => true]], ['key' => 'mail_port', 'label' => 'SMTP 服务器端口', 'type' => 'number', 'default' => $defaults['mail_port'], 'min' => 1, 'max' => 65535, 'dependsOn' => ['key' => 'mail_smtps', 'value' => true]], - ['key' => 'mail_smtpauth', 'label' => '启用 SMTPAuth', 'type' => 'switch', 'default' => $defaults['mail_smtpauth'], 'dependsOn' => ['key' => 'mail_smtps', 'value' => true]], ['key' => 'mail_smtpsecure', 'label' => '加密方式', 'type' => 'select', 'default' => $defaults['mail_smtpsecure'], 'options' => [['label' => 'SSL', 'value' => 'ssl'], ['label' => 'TLS', 'value' => 'tls'], ['label' => '不加密', 'value' => '']], 'dependsOn' => ['key' => 'mail_smtps', 'value' => true]], ]], ['key' => 'branding', 'title' => '邮件品牌', 'fields' => [ diff --git a/tests/widget-system/email-system-contract.test.cjs b/tests/widget-system/email-system-contract.test.cjs index 124033f6..571c5144 100644 --- a/tests/widget-system/email-system-contract.test.cjs +++ b/tests/widget-system/email-system-contract.test.cjs @@ -9,6 +9,7 @@ const read = (file) => fs.readFileSync(path.join(root, file), 'utf8') const templates = read('src/Settings/MailTemplates.php') const mail = read('src/Settings/Mail.php') const notifications = read('src/Settings/MailNotifications.php') +const coreHooks = read('inc/core-hooks/part-2.php') const globalController = read('src/Admin/GlobalController.php') const userController = read('src/Admin/UserController.php') const settings = read('src/Settings/Settings.php') @@ -37,11 +38,28 @@ test('email templates and SMTP use one aksrt_settings schema', () => { assert.match(templates, /'option_key'\s*=>\s*'aksrt_settings'/) assert.match(templates, /'mail_passwd'.*'secret'\s*=>\s*true/) assert.match(mail, /Settings::get_all\(\)/) + assert.match(mail, /'auth'\s*=>\s*\$username\s*!==\s*''\s*&&\s*\$password\s*!==\s*''/) + assert.doesNotMatch(templates, /mail_smtpauth/) + assert.doesNotMatch(mail, /mail_smtpauth/) assert.doesNotMatch(settings, /aksrt_mail_smtp/) assert.doesNotMatch(globalController, /aksrt_mail_config|\/settings\/mail'/) assert.doesNotMatch(userController, /aksrt_mail_templates|mail-templates/) }) +test('SMTP authentication and failures use the canonical mail service', () => { + assert.match(coreHooks, /\$phpmailer->SMTPAuth\s*=\s*!empty\(\$mail\['auth'\]\)\s*&&\s*!empty\(\$mail\['username'\]\)/) + assert.match(mail, /add_action\('wp_mail_failed',\s*\$capture_failure\)/) + assert.match(mail, /remove_action\('wp_mail_failed',\s*\$capture_failure\)/) + assert.match(mail, /error_log\('\[AKSRT\] SMTP 测试失败:'/) + assert.match(globalController, /详细原因已写入服务器错误日志/) +}) + +test('canonical mail service contains no obsolete catalog or placeholder branches', () => { + assert.doesNotMatch(mail, /public static function events\s*\(/) + assert.doesNotMatch(mail, /replace_placeholders\([^\n]+,\s*false\)/) + assert.doesNotMatch(mail, /replace_placeholders\(string \$value, array \$placeholders, bool \$html\)/) +}) + test('only the canonical mail service calls wp_mail', () => { const phpFiles = [] const walk = (dir) => {