PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.1.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.1.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.1.0, at includes/seo/class-email-report-mailer.php

186 lines 5.7 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 * - Truncate recipients to plan-allowed maximum (defense-in-depth)
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 use ThinkRank\Core\Plan_Config;
29
30 if (!defined('ABSPATH')) {
31 exit;
32 }
33
34 /**
35 * Email_Report_Mailer
36 *
37 * @since 1.9.0
38 */
39 final class Email_Report_Mailer {
40
41 /**
42 * Send a rendered email report.
43 *
44 * @param array $config Per-site config (recipients, subject_template).
45 * @param string $html Rendered HTML body.
46 * @param array $tokens Tokens to substitute in subject (date, period, …).
47 * @return array{success:bool,recipients:array<string,bool>,subject:string,error?:string}
48 */
49 public function send(array $config, string $html, array $tokens = []): array {
50 $recipients = Plan_Config::clamp_email_report_recipients(
51 (array) ($config['recipients'] ?? [])
52 );
53
54 if (empty($recipients)) {
55 return [
56 'success' => false,
57 'recipients' => [],
58 'subject' => '',
59 'error' => __('No valid recipients configured.', 'thinkrank'),
60 ];
61 }
62
63 $subject = $this->resolve_subject((string) ($config['subject_template'] ?? ''), $tokens);
64
65 /**
66 * Filter the rendered HTML one last time before send.
67 *
68 * Pro plugin uses this to insert tracking pixels, rewrite links,
69 * inject AI Highlights summary, etc. Free code never reads this
70 * filter — it just provides the seam.
71 *
72 * @since 1.9.0
73 *
74 * @param string $html Rendered HTML.
75 * @param array $config Per-site config.
76 * @param array $recipients Final recipient list.
77 */
78 $html = (string) apply_filters('thinkrank_email_report_html', $html, $config, $recipients);
79
80 $headers = $this->build_headers($config);
81
82 // wp_mail accepts an array of recipients but reports a single boolean.
83 // Send per-recipient so a single bad address doesn't sink the batch.
84 $results = [];
85 $any_failed = false;
86
87 foreach ($recipients as $address) {
88 $sent = wp_mail($address, $subject, $html, $headers);
89 $results[$address] = (bool) $sent;
90 if (!$sent) {
91 $any_failed = true;
92 }
93 }
94
95 return [
96 'success' => !$any_failed,
97 'recipients' => $results,
98 'subject' => $subject,
99 ];
100 }
101
102 /**
103 * Resolve %token% substitutions in the subject. Pro can extend the
104 * token set via the `thinkrank_email_report_tokens` filter.
105 */
106 private function resolve_subject(string $template, array $tokens): string {
107 if ($template === '') {
108 $template = '%site_title% SEO performance report';
109 }
110
111 $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%'] ?? ''),
116 ];
117
118 $merged = array_merge($defaults, $tokens);
119
120 /**
121 * Filter the subject token map.
122 *
123 * @since 1.9.0
124 *
125 * @param array $merged Token => replacement.
126 * @param string $template Subject template, pre-substitution.
127 */
128 $merged = (array) apply_filters('thinkrank_email_report_subject_tokens', $merged, $template);
129
130 $subject = strtr($template, $merged);
131
132 /**
133 * Filter the final subject string after substitution.
134 *
135 * Pro can apply its own custom template that ignores the free token
136 * set entirely.
137 *
138 * @since 1.9.0
139 *
140 * @param string $subject Resolved subject.
141 * @param string $template Original template.
142 * @param array $merged Token map used.
143 */
144 return (string) apply_filters('thinkrank_email_report_subject', $subject, $template, $merged);
145 }
146
147 /**
148 * Build wp_mail headers. Uses the WP admin From identity to keep
149 * deliverability sane; agencies on Pro typically swap this via the
150 * `wp_mail_from`/`wp_mail_from_name` filters elsewhere in their stack.
151 *
152 * @return string[]
153 */
154 private function build_headers(array $config): array {
155 $headers = [
156 'Content-Type: text/html; charset=UTF-8',
157 ];
158
159 $reply_to = $this->first_recipient($config['recipients'] ?? []);
160 if ($reply_to !== '') {
161 $headers[] = 'Reply-To: ' . $reply_to;
162 }
163
164 /**
165 * Filter the email headers.
166 *
167 * @since 1.9.0
168 *
169 * @param string[] $headers Default headers.
170 * @param array $config Per-site config.
171 */
172 $headers = (array) apply_filters('thinkrank_email_report_headers', $headers, $config);
173
174 return array_values(array_filter($headers, 'is_string'));
175 }
176
177 private function first_recipient(array $recipients): string {
178 foreach ($recipients as $r) {
179 if (is_string($r) && is_email($r)) {
180 return $r;
181 }
182 }
183 return '';
184 }
185 }
186