PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.10.0
2.10.0 2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 All 51 releases
thinkrank / includes / seo / class-email-report-mailer.php

class-email-report-mailer.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.10.0, at includes/seo/class-email-report-mailer.php

223 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Email Report Mailer
5 *
6 * Thin wrapper around wp_mail() for the Email Reporting feature.
7 *
8 * Responsibilities:
9 * - Build From/Reply-To/Content-Type headers
10 * - Resolve subject template tokens
11 * - Drop invalid and duplicate recipients
12 * - Return a structured result with per-recipient outcome
13 *
14 * Not its job:
15 * - Building HTML (that's Email_Report_Renderer)
16 * - Deciding when to send (that's Email_Report_Scheduler)
17 * - Logging to the email_report_logs table (that's Email_Report_Generator)
18 *
19 * @package ThinkRank
20 * @subpackage SEO
21 * @since 1.9.0
22 */
23
24 declare(strict_types=1);
25
26 namespace ThinkRank\SEO;
27
28 if (!defined('ABSPATH')) {
29 exit;
30 }
31
32 /**
33 * Email_Report_Mailer
34 *
35 * @since 1.9.0
36 */
37 final class Email_Report_Mailer {
38
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 /**
49 * Send a rendered email report.
50 *
51 * @param array $config Resolved config (recipients).
52 * @param string $html Rendered HTML body.
53 * @param array $tokens Tokens to substitute in subject (date, period, …).
54 * @return array{success:bool,recipients:array<string,bool>,subject:string,error?:string}
55 */
56 public function send(array $config, string $html, array $tokens = []): array {
57 $recipients = $this->valid_recipients((array) ($config['recipients'] ?? []));
58
59 if (empty($recipients)) {
60 return [
61 'success' => false,
62 'recipients' => [],
63 'subject' => '',
64 'error' => __('No valid recipients configured.', 'thinkrank'),
65 ];
66 }
67
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);
79
80 $subject = $this->resolve_subject($template, $tokens);
81
82 /**
83 * Filter the rendered HTML one last time before send.
84 *
85 * Pro plugin uses this to insert tracking pixels, rewrite links,
86 * inject AI Highlights summary, etc. Free code never reads this
87 * filter — it just provides the seam.
88 *
89 * @since 1.9.0
90 *
91 * @param string $html Rendered HTML.
92 * @param array $config Per-site config.
93 * @param array $recipients Final recipient list.
94 */
95 $html = (string) apply_filters('thinkrank_email_report_html', $html, $config, $recipients);
96
97 $headers = $this->build_headers($config);
98
99 // wp_mail accepts an array of recipients but reports a single boolean.
100 // Send per-recipient so a single bad address doesn't sink the batch.
101 $results = [];
102 $any_failed = false;
103
104 foreach ($recipients as $address) {
105 $sent = wp_mail($address, $subject, $html, $headers);
106 $results[$address] = (bool) $sent;
107 if (!$sent) {
108 $any_failed = true;
109 }
110 }
111
112 return [
113 'success' => !$any_failed,
114 'recipients' => $results,
115 'subject' => $subject,
116 ];
117 }
118
119 /**
120 * Resolve %token% substitutions in the subject. Pro can extend the
121 * token set via the `thinkrank_email_report_tokens` filter.
122 */
123 private function resolve_subject(string $template, array $tokens): string {
124 if ($template === '') {
125 $template = self::DEFAULT_SUBJECT;
126 }
127
128 $defaults = [
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%' => '',
138 ];
139
140 $merged = array_merge($defaults, $tokens);
141
142 /**
143 * Filter the subject token map.
144 *
145 * @since 1.9.0
146 *
147 * @param array $merged Token => replacement.
148 * @param string $template Subject template, pre-substitution.
149 */
150 $merged = (array) apply_filters('thinkrank_email_report_subject_tokens', $merged, $template);
151
152 $subject = strtr($template, $merged);
153
154 /**
155 * Filter the final subject string after substitution.
156 *
157 * Pro can apply its own custom template that ignores the free token
158 * set entirely.
159 *
160 * @since 1.9.0
161 *
162 * @param string $subject Resolved subject.
163 * @param string $template Original template.
164 * @param array $merged Token map used.
165 */
166 return (string) apply_filters('thinkrank_email_report_subject', $subject, $template, $merged);
167 }
168
169 /**
170 * Build wp_mail headers. Uses the WP admin From identity to keep
171 * deliverability sane; agencies on Pro typically swap this via the
172 * `wp_mail_from`/`wp_mail_from_name` filters elsewhere in their stack.
173 *
174 * @return string[]
175 */
176 private function build_headers(array $config): array {
177 $headers = [
178 'Content-Type: text/html; charset=UTF-8',
179 ];
180
181 $reply_to = $this->first_recipient($config['recipients'] ?? []);
182 if ($reply_to !== '') {
183 $headers[] = 'Reply-To: ' . $reply_to;
184 }
185
186 /**
187 * Filter the email headers.
188 *
189 * @since 1.9.0
190 *
191 * @param string[] $headers Default headers.
192 * @param array $config Per-site config.
193 */
194 $headers = (array) apply_filters('thinkrank_email_report_headers', $headers, $config);
195
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));
212 }
213
214 private function first_recipient(array $recipients): string {
215 foreach ($recipients as $r) {
216 if (is_string($r) && is_email($r)) {
217 return $r;
218 }
219 }
220 return '';
221 }
222 }
223