0). Sends a plain-text email to the configured * recipient(s) with a 24h throttle key (per-alert-fingerprint) so admins * don't get spammed. * * Default OFF — admin must explicitly enable via Settings tab. * * @package WP_Data_Optimizer */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Email notifier — stateless static API. */ class TMDO_Email_Notifier { public const OPT_ENABLED = 'wpdo_email_alerts_enabled'; public const OPT_RECIPIENT = 'wpdo_alert_email'; public const OPT_THROTTLE_HRS = 'wpdo_alert_throttle_hours'; /** Default throttle window. */ public const DEFAULT_THROTTLE_HOURS = 24; /** * Register subscriber. * * @return void */ public static function register(): void { add_action( 'wpdo/health_alert_critical', array( __CLASS__, 'maybe_send' ), 10, 1 ); } /** * Decide + send. * * @param array $summary Health summary from Health_Cron::run(). * @return bool true on send, false on skip / fail. */ public static function maybe_send( array $summary ): bool { if ( ! self::is_enabled() ) { return false; } $recipient = self::recipient(); if ( ! is_email( $recipient ) ) { return false; } $fingerprint = self::fingerprint( $summary ); if ( self::is_throttled( $fingerprint ) ) { return false; } $subject = self::build_subject( $summary ); $body = self::build_body( $summary ); $sent = wp_mail( $recipient, $subject, $body ); if ( $sent ) { self::mark_sent( $fingerprint ); if ( class_exists( 'TMDO_Logger' ) ) { TMDO_Logger::info( 'email_alert_sent', array( 'fingerprint' => $fingerprint, 'critical' => (int) ( $summary['critical_count'] ?? 0 ), 'recipient' => $recipient, ) ); } } return (bool) $sent; } // ─── settings accessors ───────────────────────────────────────────── /** * Whether email alerts are enabled. * * @return bool */ public static function is_enabled(): bool { return '1' === (string) get_option( self::OPT_ENABLED, '0' ); } /** * Alert recipient email address (falls back to admin_email). * * @return string */ public static function recipient(): string { $v = (string) get_option( self::OPT_RECIPIENT, '' ); if ( '' === $v ) { $v = (string) get_option( 'admin_email', '' ); } return $v; } /** * Throttle window in hours (clamped 1..168). * * @return int */ public static function throttle_hours(): int { $v = (int) get_option( self::OPT_THROTTLE_HRS, self::DEFAULT_THROTTLE_HOURS ); return max( 1, min( 168, $v ) ); // Clamp 1h..1week. } // ─── private helpers ────────────────────────────────────────────── /** * Stable fingerprint from the alert content (so identical incidents * dedupe within the throttle window). * * @param array $summary Summary array. * @return string md5 hash. */ private static function fingerprint( array $summary ): string { $relevant = array( 'critical_count' => (int) ( $summary['critical_count'] ?? 0 ), 'first_critical' => null, ); foreach ( (array) ( $summary['tests'] ?? array() ) as $slug => $t ) { if ( 'critical' === ( $t['status'] ?? '' ) ) { $relevant['first_critical'] = $slug; break; } } return md5( wp_json_encode( $relevant ) ); } /** * Check whether an alert with this fingerprint was recently sent. * * @param string $fingerprint Alert fingerprint (md5). * @return bool */ private static function is_throttled( string $fingerprint ): bool { $key = 'wpdo_alert_sent_' . $fingerprint; return false !== get_transient( $key ); } /** * Record that an alert was sent (sets throttle transient). * * @param string $fingerprint Alert fingerprint (md5). * @return void */ private static function mark_sent( string $fingerprint ): void { $key = 'wpdo_alert_sent_' . $fingerprint; set_transient( $key, time(), self::throttle_hours() * HOUR_IN_SECONDS ); } /** * Build subject. Site name + critical count + first slug. * * @param array $summary Summary array. * @return string */ private static function build_subject( array $summary ): string { $site = (string) get_option( 'blogname', 'WordPress' ); $crit = (int) ( $summary['critical_count'] ?? 0 ); $first = ''; foreach ( (array) ( $summary['tests'] ?? array() ) as $slug => $t ) { if ( 'critical' === ( $t['status'] ?? '' ) ) { $first = $slug; break; } } return sprintf( /* translators: 1: site name, 2: critical count, 3: first critical test slug */ __( '[%1$s] WPDO 警告:%2$d 項 critical (%3$s)', '2meet-data-optimizer' ), $site, $crit, $first ); } /** * Build plain-text body. * * @param array $summary Summary array. * @return string */ private static function build_body( array $summary ): string { $site_url = home_url( '/' ); $health_url = admin_url( 'site-health.php' ); $wpdo_url = admin_url( 'tools.php?page=wp-data-optimizer&tab=doctor' ); $lines = array(); $lines[] = __( 'WP Data Optimizer 自動健康檢查發現 critical 警告。', '2meet-data-optimizer' ); $lines[] = ''; $lines[] = sprintf( '站台:%s', $site_url ); $lines[] = sprintf( /* translators: %s: timestamp */ __( '檢查時間:%s UTC', '2meet-data-optimizer' ), (string) ( $summary['ran_at'] ?? '?' ) ); $lines[] = sprintf( /* translators: 1: critical count, 2: recommended count */ __( '結果:%1$d critical / %2$d recommended', '2meet-data-optimizer' ), (int) ( $summary['critical_count'] ?? 0 ), (int) ( $summary['recommended_count'] ?? 0 ) ); $lines[] = ''; $lines[] = __( '失敗的檢查:', '2meet-data-optimizer' ); foreach ( (array) ( $summary['tests'] ?? array() ) as $slug => $t ) { if ( in_array( ( $t['status'] ?? '' ), array( 'critical', 'recommended' ), true ) ) { $lines[] = sprintf( ' [%s] %s — %s', strtoupper( (string) $t['status'] ), $slug, (string) ( $t['description'] ?? '' ) ); } } $lines[] = ''; $lines[] = __( '建議行動:', '2meet-data-optimizer' ); $lines[] = ' · ' . sprintf( /* translators: %s: WPDO Doctor admin URL */ __( '立即查看 WPDO Doctor:%s', '2meet-data-optimizer' ), $wpdo_url ); $lines[] = ' · ' . sprintf( /* translators: %s: WP Site Health admin URL */ __( '或 WP Site Health:%s', '2meet-data-optimizer' ), $health_url ); $lines[] = ''; $lines[] = sprintf( /* translators: %d: hours */ __( '註:相同警告在 %d 小時內不會重發;前往設定可調整 throttle / 收件人 / 關閉。', '2meet-data-optimizer' ), self::throttle_hours() ); return implode( "\n", $lines ); } }