wpbridge/includes/Notification/EmailHandler.php
feibisi 7bb18002f7 fix: 修复 Codex v0.3.0 代码评审发现的问题
HIGH 级别修复:
- WebhookHandler: 添加 SSRF 防护 (Validator::is_valid_url)
- AIGateway: 自定义端点添加 SSRF 验证
- AIGateway: 白名单域名匹配改为大小写不敏感
- CommercialManager: 文件读取添加路径遍历防护 (realpath)
- GroupModel: from_array() 添加输入类型验证和清理
- GroupManager: add/remove_source_to_group 添加权限检查

MEDIUM 级别修复:
- NotificationManager: 添加 5 分钟速率限制防止通知轰炸
- NotificationManager: 允许第三方扩展通知处理器
- EmailHandler: 添加收件人邮箱格式验证
- EmailHandler: 移除自定义 From 头避免 SPF/DKIM 问题
- CommercialManager: lock/unlock_version 添加权限检查
- CommercialManager: 商业插件列表支持过滤器扩展
- GroupManager: toggle 方法改进原子性,先更新分组再更新源
- AbstractAdapter: 正则匹配添加错误处理

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:14:16 +08:00

181 lines
5.1 KiB
PHP

<?php
/**
* 邮件通知处理器
*
* @package WPBridge
*/
namespace WPBridge\Notification;
use WPBridge\Core\Settings;
// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* 邮件通知处理器类
*/
class EmailHandler implements HandlerInterface {
/**
* 设置实例
*
* @var Settings
*/
private Settings $settings;
/**
* 支持的通知类型
*
* @var array
*/
private array $supported_types = [ 'update', 'error', 'recovery' ];
/**
* 构造函数
*
* @param Settings $settings 设置实例
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
}
/**
* 获取处理器名称
*
* @return string
*/
public function get_name(): string {
return 'email';
}
/**
* 是否启用
*
* @return bool
*/
public function is_enabled(): bool {
$notification_settings = $this->settings->get( 'notifications', [] );
return ! empty( $notification_settings['email']['enabled'] );
}
/**
* 是否支持该通知类型
*
* @param string $type 通知类型
* @return bool
*/
public function supports_type( string $type ): bool {
$notification_settings = $this->settings->get( 'notifications', [] );
$enabled_types = $notification_settings['email']['types'] ?? $this->supported_types;
return in_array( $type, $enabled_types, true );
}
/**
* 发送通知
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @throws \Exception 发送失败时抛出异常
*/
public function send( string $subject, string $message, array $data = [] ): void {
$notification_settings = $this->settings->get( 'notifications', [] );
$recipients = $notification_settings['email']['recipients'] ?? [];
if ( empty( $recipients ) ) {
// 默认发送给管理员
$recipients = [ get_option( 'admin_email' ) ];
}
// 验证收件人邮箱格式
$valid_recipients = array_filter( $recipients, function ( $email ) {
return is_email( $email );
} );
if ( empty( $valid_recipients ) ) {
throw new \Exception( __( '没有有效的收件人邮箱', 'wpbridge' ) );
}
// 构建 HTML 邮件
$html_message = $this->build_html_message( $subject, $message, $data );
// 使用 WordPress 默认发件人,避免 SPF/DKIM 问题
$headers = [
'Content-Type: text/html; charset=UTF-8',
];
$sent = wp_mail( $valid_recipients, $subject, $html_message, $headers );
if ( ! $sent ) {
throw new \Exception( __( '邮件发送失败', 'wpbridge' ) );
}
}
/**
* 构建 HTML 邮件内容
*
* @param string $subject 主题
* @param string $message 消息
* @param array $data 附加数据
* @return string
*/
private function build_html_message( string $subject, string $message, array $data ): string {
$site_name = get_bloginfo( 'name' );
$site_url = get_site_url();
$html = '<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>' . esc_html( $subject ) . '</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #0073aa; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; background: #f9f9f9; }
.footer { padding: 20px; text-align: center; font-size: 12px; color: #666; }
.data-table { width: 100%; border-collapse: collapse; margin-top: 15px; }
.data-table th, .data-table td { padding: 8px; border: 1px solid #ddd; text-align: left; }
.data-table th { background: #f0f0f0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>WPBridge</h1>
</div>
<div class="content">
<p>' . nl2br( esc_html( $message ) ) . '</p>';
// 添加数据表格
if ( ! empty( $data ) && ! isset( $data['test'] ) ) {
$html .= '<table class="data-table">';
foreach ( $data as $key => $value ) {
if ( is_scalar( $value ) ) {
$html .= '<tr><th>' . esc_html( $key ) . '</th><td>' . esc_html( $value ) . '</td></tr>';
}
}
$html .= '</table>';
}
$html .= '
</div>
<div class="footer">
<p>' . sprintf(
/* translators: %s: site name */
esc_html__( '此邮件由 %s 的 WPBridge 插件发送', 'wpbridge' ),
esc_html( $site_name )
) . '</p>
<p><a href="' . esc_url( $site_url ) . '">' . esc_html( $site_url ) . '</a></p>
</div>
</div>
</body>
</html>';
return $html;
}
}