PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.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 1.0.2 1.1.0 1.10.0 All 48 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.7.0, at includes/seo/class-email-report-mailer.php

214 lines 6.5 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 public const DEFAULT_SUBJECT = '%site_title% SEO performance report';
43
44 /**
45 * Send a rendered email report.
46 *
47 * @param array $config Resolved config (recipients).
48 * @param string $html Rendered HTML body.
49 * @param array $tokens Tokens to substitute in subject (date, period, …).
50 * @return array{success:bool,recipients:array<string,bool>,subject:string,error?:string}
51 */
52 public function send(array $config, string $html, array $tokens = []): array {
53 $recipients = $this->valid_recipients((array) ($config['recipients'] ?? []));
54
55 if (empty($recipients)) {
56 return [
57 'success' => false,
58 'recipients' => [],
59 'subject' => '',
60 'error' => __('No valid recipients configured.', 'thinkrank'),
61 ];
62 }
63
64 /**
65 * Filter the subject template before token substitution.
66 *
67 * ThinkRank Pro returns its custom subject line here.
68 *
69 * @since 2.6.0
70 *
71 * @param string $template Subject template.
72 * @param array $config Resolved config.
73 */
74 $template = (string) apply_filters('thinkrank_email_report_subject_template', self::DEFAULT_SUBJECT, $config);
75
76 $subject = $this->resolve_subject($template, $tokens);
77
78 /**
79 * Filter the rendered HTML one last time before send.
80 *
81 * Pro plugin uses this to insert tracking pixels, rewrite links,
82 * inject AI Highlights summary, etc. Free code never reads this
83 * filter — it just provides the seam.
84 *
85 * @since 1.9.0
86 *
87 * @param string $html Rendered HTML.
88 * @param array $config Per-site config.
89 * @param array $recipients Final recipient list.
90 */
91 $html = (string) apply_filters('thinkrank_email_report_html', $html, $config, $recipients);
92
93 $headers = $this->build_headers($config);
94
95 // wp_mail accepts an array of recipients but reports a single boolean.
96 // Send per-recipient so a single bad address doesn't sink the batch.
97 $results = [];
98 $any_failed = false;
99
100 foreach ($recipients as $address) {
101 $sent = wp_mail($address, $subject, $html, $headers);
102 $results[$address] = (bool) $sent;
103 if (!$sent) {
104 $any_failed = true;
105 }
106 }
107
108 return [
109 'success' => !$any_failed,
110 'recipients' => $results,
111 'subject' => $subject,
112 ];
113 }
114
115 /**
116 * Resolve %token% substitutions in the subject. Pro can extend the
117 * token set via the `thinkrank_email_report_tokens` filter.
118 */
119 private function resolve_subject(string $template, array $tokens): string {
120 if ($template === '') {
121 $template = self::DEFAULT_SUBJECT;
122 }
123
124 $defaults = [
125 '%site_title%' => (string) get_bloginfo('name'),
126 '%site_url%' => (string) home_url(),
127 '%date%' => wp_date(get_option('date_format', 'Y-m-d')),
128 '%period%' => (string) ($tokens['%period%'] ?? ''),
129 ];
130
131 $merged = array_merge($defaults, $tokens);
132
133 /**
134 * Filter the subject token map.
135 *
136 * @since 1.9.0
137 *
138 * @param array $merged Token => replacement.
139 * @param string $template Subject template, pre-substitution.
140 */
141 $merged = (array) apply_filters('thinkrank_email_report_subject_tokens', $merged, $template);
142
143 $subject = strtr($template, $merged);
144
145 /**
146 * Filter the final subject string after substitution.
147 *
148 * Pro can apply its own custom template that ignores the free token
149 * set entirely.
150 *
151 * @since 1.9.0
152 *
153 * @param string $subject Resolved subject.
154 * @param string $template Original template.
155 * @param array $merged Token map used.
156 */
157 return (string) apply_filters('thinkrank_email_report_subject', $subject, $template, $merged);
158 }
159
160 /**
161 * Build wp_mail headers. Uses the WP admin From identity to keep
162 * deliverability sane; agencies on Pro typically swap this via the
163 * `wp_mail_from`/`wp_mail_from_name` filters elsewhere in their stack.
164 *
165 * @return string[]
166 */
167 private function build_headers(array $config): array {
168 $headers = [
169 'Content-Type: text/html; charset=UTF-8',
170 ];
171
172 $reply_to = $this->first_recipient($config['recipients'] ?? []);
173 if ($reply_to !== '') {
174 $headers[] = 'Reply-To: ' . $reply_to;
175 }
176
177 /**
178 * Filter the email headers.
179 *
180 * @since 1.9.0
181 *
182 * @param string[] $headers Default headers.
183 * @param array $config Per-site config.
184 */
185 $headers = (array) apply_filters('thinkrank_email_report_headers', $headers, $config);
186
187 return array_values(array_filter($headers, 'is_string'));
188 }
189
190 /**
191 * Unique, valid addresses, in the order given.
192 *
193 * @param array $recipients Candidate addresses.
194 * @return string[]
195 */
196 private function valid_recipients(array $recipients): array {
197 $valid = array_filter(
198 array_map(static fn ($r) => is_string($r) ? trim($r) : '', $recipients),
199 static fn (string $r): bool => $r !== '' && (bool) is_email($r)
200 );
201
202 return array_values(array_unique($valid));
203 }
204
205 private function first_recipient(array $recipients): string {
206 foreach ($recipients as $r) {
207 if (is_string($r) && is_email($r)) {
208 return $r;
209 }
210 }
211 return '';
212 }
213 }
214