| @@ -7,9 +7,9 @@ | ||
| 7 | 7 | * |
| 8 | 8 | * Responsibilities: |
| 9 | 9 | * - Build From/Reply-To/Content-Type headers |
| 10 | 10 | * - Resolve subject template tokens |
| 11 | - * - Truncate recipients to plan-allowed maximum (defense-in-depth) | |
| 11 | + * - Drop invalid and duplicate recipients | |
| 12 | 12 | * - Return a structured result with per-recipient outcome |
| 13 | 13 | * |
| 14 | 14 | * Not its job: |
| 15 | 15 | * - Building HTML (that's Email_Report_Renderer) |
| @@ -24,10 +24,8 @@ | ||
| 24 | 24 | declare(strict_types=1); |
| 25 | 25 | |
| 26 | 26 | namespace ThinkRank\SEO; |
| 27 | 27 | |
| 28 | -use ThinkRank\Core\Plan_Config; | |
| 29 | - | |
| 30 | 28 | if (!defined('ABSPATH')) { |
| 31 | 29 | exit; |
| 32 | 30 | } |
| 33 | 31 | |
| @@ -38,19 +36,26 @@ | ||
| 38 | 36 | */ |
| 39 | 37 | final class Email_Report_Mailer { |
| 40 | 38 | |
| 41 | 39 | /** |
| 40 | + * Subject line when nothing filters it. | |
| 41 | + * | |
| 42 | + * `%headline%` is the report's own summary — "12,480 Google clicks | |
| 43 | + * (+12.4%) in the last 30 days" — supplied by the generator (#742). A | |
| 44 | + * subject that carries the number gets opened; a label does not. | |
| 45 | + */ | |
| 46 | + public const DEFAULT_SUBJECT = '%site_title%: %headline%'; | |
| 47 | + | |
| 48 | + /** | |
| 42 | 49 | * Send a rendered email report. |
| 43 | 50 | * |
| 44 | - * @param array $config Per-site config (recipients, subject_template). | |
| 51 | + * @param array $config Resolved config (recipients). | |
| 45 | 52 | * @param string $html Rendered HTML body. |
| 46 | 53 | * @param array $tokens Tokens to substitute in subject (date, period, …). |
| 47 | 54 | * @return array{success:bool,recipients:array<string,bool>,subject:string,error?:string} |
| 48 | 55 | */ |
| 49 | 56 | public function send(array $config, string $html, array $tokens = []): array { |
| 50 | - $recipients = Plan_Config::clamp_email_report_recipients( | |
| 51 | - (array) ($config['recipients'] ?? []) | |
| 52 | - ); | |
| 57 | + $recipients = $this->valid_recipients((array) ($config['recipients'] ?? [])); | |
| 53 | 58 | |
| 54 | 59 | if (empty($recipients)) { |
| 55 | 60 | return [ |
| 56 | 61 | 'success' => false, |
| @@ -59,10 +64,22 @@ | ||
| 59 | 64 | 'error' => __('No valid recipients configured.', 'thinkrank'), |
| 60 | 65 | ]; |
| 61 | 66 | } |
| 62 | 67 | |
| 63 | - $subject = $this->resolve_subject((string) ($config['subject_template'] ?? ''), $tokens); | |
| 68 | + /** | |
| 69 | + * Filter the subject template before token substitution. | |
| 70 | + * | |
| 71 | + * ThinkRank Pro returns its custom subject line here. | |
| 72 | + * | |
| 73 | + * @since 2.6.0 | |
| 74 | + * | |
| 75 | + * @param string $template Subject template. | |
| 76 | + * @param array $config Resolved config. | |
| 77 | + */ | |
| 78 | + $template = (string) apply_filters('thinkrank_email_report_subject_template', self::DEFAULT_SUBJECT, $config); | |
| 64 | 79 | |
| 80 | + $subject = $this->resolve_subject($template, $tokens); | |
| 81 | + | |
| 65 | 82 | /** |
| 66 | 83 | * Filter the rendered HTML one last time before send. |
| 67 | 84 | * |
| 68 | 85 | * Pro plugin uses this to insert tracking pixels, rewrite links, |
| @@ -104,16 +121,21 @@ | ||
| 104 | 121 | * token set via the `thinkrank_email_report_tokens` filter. |
| 105 | 122 | */ |
| 106 | 123 | private function resolve_subject(string $template, array $tokens): string { |
| 107 | 124 | if ($template === '') { |
| 108 | - $template = '%site_title% SEO performance report'; | |
| 125 | + $template = self::DEFAULT_SUBJECT; | |
| 109 | 126 | } |
| 110 | 127 | |
| 111 | 128 | $defaults = [ |
| 112 | - '%site_title%' => (string) get_bloginfo('name'), | |
| 113 | - '%site_url%' => (string) home_url(), | |
| 114 | - '%date%' => wp_date(get_option('date_format', 'Y-m-d')), | |
| 115 | - '%period%' => (string) ($tokens['%period%'] ?? ''), | |
| 129 | + '%site_title%' => (string) get_bloginfo('name'), | |
| 130 | + '%site_url%' => (string) home_url(), | |
| 131 | + '%date%' => wp_date(get_option('date_format', 'Y-m-d')), | |
| 132 | + '%period%' => (string) ($tokens['%period%'] ?? ''), | |
| 133 | + '%headline%' => __('SEO performance report', 'thinkrank'), | |
| 134 | + '%clicks%' => '', | |
| 135 | + '%clicks_change%' => '', | |
| 136 | + '%impressions%' => '', | |
| 137 | + '%period_days%' => '', | |
| 116 | 138 | ]; |
| 117 | 139 | |
| 118 | 140 | $merged = array_merge($defaults, $tokens); |
| 119 | 141 | |
| @@ -171,8 +193,23 @@ | ||
| 171 | 193 | */ |
| 172 | 194 | $headers = (array) apply_filters('thinkrank_email_report_headers', $headers, $config); |
| 173 | 195 | |
| 174 | 196 | return array_values(array_filter($headers, 'is_string')); |
| 197 | + } | |
| 198 | + | |
| 199 | + /** | |
| 200 | + * Unique, valid addresses, in the order given. | |
| 201 | + * | |
| 202 | + * @param array $recipients Candidate addresses. | |
| 203 | + * @return string[] | |
| 204 | + */ | |
| 205 | + private function valid_recipients(array $recipients): array { | |
| 206 | + $valid = array_filter( | |
| 207 | + array_map(static fn ($r) => is_string($r) ? trim($r) : '', $recipients), | |
| 208 | + static fn (string $r): bool => $r !== '' && (bool) is_email($r) | |
| 209 | + ); | |
| 210 | + | |
| 211 | + return array_values(array_unique($valid)); | |
| 175 | 212 | } |
| 176 | 213 | |
| 177 | 214 | private function first_recipient(array $recipients): string { |
| 178 | 215 | foreach ($recipients as $r) { |