PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 11.1.0
WooCommerce v11.1.0
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / includes / emails / class-wc-email.php
woocommerce / includes / emails Last commit date
class-wc-email-admin-payment-gateway-enabled.php 2 months ago class-wc-email-cancelled-order.php 1 month ago class-wc-email-customer-abandoned-cart-recovery.php 1 month ago class-wc-email-customer-cancelled-order.php 2 months ago class-wc-email-customer-completed-order.php 2 months ago class-wc-email-customer-failed-order.php 2 months ago class-wc-email-customer-fulfillment-created.php 2 months ago class-wc-email-customer-fulfillment-deleted.php 2 months ago class-wc-email-customer-fulfillment-updated.php 2 months ago class-wc-email-customer-invoice.php 2 weeks ago class-wc-email-customer-new-account.php 2 months ago class-wc-email-customer-note.php 2 months ago class-wc-email-customer-on-hold-order.php 2 weeks ago class-wc-email-customer-partially-refunded-order.php 2 weeks ago class-wc-email-customer-pos-completed-order.php 2 months ago class-wc-email-customer-pos-refunded-order.php 2 months ago class-wc-email-customer-processing-order.php 2 months ago class-wc-email-customer-refunded-order.php 2 weeks ago class-wc-email-customer-reset-password.php 2 months ago class-wc-email-customer-review-request.php 2 months ago class-wc-email-failed-order.php 2 months ago class-wc-email-new-order.php 2 months ago class-wc-email.php 2 weeks ago
class-wc-email.php
1847 lines
1 <?php
2 /**
3 * Class WC_Email file.
4 *
5 * @package WooCommerce\Emails
6 */
7
8 use Automattic\WooCommerce\EmailEditor\Engine\Personalizer;
9 use Automattic\WooCommerce\Internal\EmailEditor\BlockEmailRenderer;
10 use Automattic\WooCommerce\Internal\EmailEditor\TransactionalEmailPersonalizer;
11 use Automattic\WooCommerce\Utilities\FeaturesUtil;
12 use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\CssInliner;
13 use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter;
14 use Automattic\WooCommerce\Vendor\Pelago\Emogrifier\HtmlProcessor\HtmlPruner;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 if ( class_exists( 'WC_Email', false ) ) {
21 return;
22 }
23
24 /**
25 * Email Class
26 *
27 * WooCommerce Email Class which is extended by specific email template classes to add emails to WooCommerce
28 *
29 * @class WC_Email
30 * @version 2.5.0
31 * @package WooCommerce\Classes\Emails
32 * @extends WC_Settings_API
33 */
34 class WC_Email extends WC_Settings_API {
35
36 /**
37 * Skip-reason identifier used when the email has no recipient address.
38 *
39 * @since 10.9.0
40 */
41 public const SKIP_REASON_NO_RECIPIENT = 'no_recipient';
42
43 /**
44 * Email method ID.
45 *
46 * @var string
47 */
48 public $id;
49
50 /**
51 * Email method title.
52 *
53 * @var string
54 */
55 public $title;
56
57 /**
58 * 'yes' if the method is enabled.
59 *
60 * @var string yes, no
61 */
62 public $enabled;
63
64 /**
65 * Description for the email.
66 *
67 * @var string
68 */
69 public $description;
70
71 /**
72 * Default heading.
73 *
74 * Supported for backwards compatibility but we recommend overloading the
75 * get_default_x methods instead so localization can be done when needed.
76 *
77 * @var string
78 */
79 public $heading = '';
80
81 /**
82 * Default subject.
83 *
84 * Supported for backwards compatibility but we recommend overloading the
85 * get_default_x methods instead so localization can be done when needed.
86 *
87 * @var string
88 */
89 public $subject = '';
90
91 /**
92 * Plain text template path.
93 *
94 * @var string
95 */
96 public $template_plain;
97
98 /**
99 * HTML template path.
100 *
101 * @var string
102 */
103 public $template_html;
104
105 /**
106 * Initial email block template path.
107 *
108 * @var string
109 */
110 public $template_block;
111
112 /**
113 * Template path.
114 *
115 * @var string
116 */
117 public $template_base;
118
119 /**
120 * Recipients for the email.
121 *
122 * @var string
123 */
124 public $recipient;
125
126 /**
127 * Cc recipients for the email.
128 *
129 * @var string
130 */
131 public $cc;
132
133 /**
134 * Bcc recipients for the email.
135 *
136 * @var string
137 */
138 public $bcc;
139
140 /**
141 * Object this email is for, for example a customer, product, or email.
142 *
143 * @var object|bool
144 */
145 public $object;
146
147 /**
148 * Mime boundary (for multipart emails).
149 *
150 * @var string
151 */
152 public $mime_boundary;
153
154 /**
155 * Mime boundary header (for multipart emails).
156 *
157 * @var string
158 */
159 public $mime_boundary_header;
160
161 /**
162 * True when email is being sent.
163 *
164 * @var bool
165 */
166 public $sending;
167
168 /**
169 * True when the email notification is sent manually only.
170 *
171 * @var bool
172 */
173 protected $manual = false;
174
175 /**
176 * True when the email notification is sent to customers.
177 *
178 * @var bool
179 */
180 protected $customer_email = false;
181
182 /**
183 * Email group slug.
184 *
185 * @var string
186 */
187 public $email_group = '';
188
189 /**
190 * List of preg* regular expression patterns to search for,
191 * used in conjunction with $plain_replace.
192 * https://raw.github.com/ushahidi/wp-silcc/master/class.html2text.inc
193 *
194 * @var array $plain_search
195 * @see $plain_replace
196 */
197 public $plain_search = array(
198 "/\r/", // Non-legal carriage return.
199 '/&(nbsp|#0*160);/i', // Non-breaking space.
200 '/&(quot|rdquo|ldquo|#0*8220|#0*8221|#0*147|#0*148);/i', // Double quotes.
201 '/&(apos|rsquo|lsquo|#0*8216|#0*8217);/i', // Single quotes.
202 '/&gt;/i', // Greater-than.
203 '/&lt;/i', // Less-than.
204 '/&#0*38;/i', // Ampersand.
205 '/&amp;/i', // Ampersand.
206 '/&(copy|#0*169);/i', // Copyright.
207 '/&(trade|#0*8482|#0*153);/i', // Trademark.
208 '/&(reg|#0*174);/i', // Registered.
209 '/&(mdash|#0*151|#0*8212);/i', // mdash.
210 '/&(ndash|minus|#0*8211|#0*8722);/i', // ndash.
211 '/&(bull|#0*149|#0*8226);/i', // Bullet.
212 '/&(pound|#0*163);/i', // Pound sign.
213 '/&(euro|#0*8364);/i', // Euro sign.
214 '/&(dollar|#0*36);/i', // Dollar sign.
215 '/&[^&\s;]+;/i', // Unknown/unhandled entities.
216 '/[ ]{2,}/', // Runs of spaces, post-handling.
217 );
218
219 /**
220 * List of pattern replacements corresponding to patterns searched.
221 *
222 * @var array $plain_replace
223 * @see $plain_search
224 */
225 public $plain_replace = array(
226 '', // Non-legal carriage return.
227 ' ', // Non-breaking space.
228 '"', // Double quotes.
229 "'", // Single quotes.
230 '>', // Greater-than.
231 '<', // Less-than.
232 '&', // Ampersand.
233 '&', // Ampersand.
234 '(c)', // Copyright.
235 '(tm)', // Trademark.
236 '(R)', // Registered.
237 '--', // mdash.
238 '-', // ndash.
239 '*', // Bullet.
240 '£', // Pound sign.
241 'EUR', // Euro sign. € ?.
242 '$', // Dollar sign.
243 '', // Unknown/unhandled entities.
244 ' ', // Runs of spaces, post-handling.
245 );
246
247 /**
248 * Strings to find/replace in subjects/headings.
249 *
250 * @var array
251 */
252 public $placeholders = array();
253
254 /**
255 * Strings to find in subjects/headings.
256 *
257 * @deprecated 3.2.0 in favour of placeholders
258 * @var array
259 */
260 public $find = array();
261
262 /**
263 * Strings to replace in subjects/headings.
264 *
265 * @deprecated 3.2.0 in favour of placeholders
266 * @var array
267 */
268 public $replace = array();
269
270 /**
271 * E-mail type: plain, html or multipart.
272 *
273 * @var string
274 */
275 public $email_type;
276
277 /**
278 * Whether email improvements feature is enabled.
279 *
280 * @var bool
281 */
282 public $email_improvements_enabled;
283
284 /**
285 * Whether email block editor feature is enabled.
286 *
287 * @var bool
288 */
289 public $block_email_editor_enabled;
290
291
292
293 /**
294 * Personalizer instance for converting Personalization tags.
295 *
296 * @var TransactionalEmailPersonalizer
297 */
298 public $personalizer;
299
300 /**
301 * Block content template path.
302 *
303 * @var string
304 */
305 public $template_block_content = 'emails/block/general-block-email.php';
306
307 /**
308 * Constructor.
309 */
310 public function __construct() {
311 $this->email_improvements_enabled = FeaturesUtil::feature_is_enabled( 'email_improvements' );
312 $this->block_email_editor_enabled = FeaturesUtil::feature_is_enabled( 'block_email_editor' );
313
314 // Find/replace.
315 $this->placeholders = array_merge(
316 array(
317 '{site_title}' => $this->get_blogname(),
318 '{site_address}' => wp_parse_url( home_url(), PHP_URL_HOST ),
319 '{site_url}' => wp_parse_url( home_url(), PHP_URL_HOST ),
320 '{store_email}' => $this->get_from_address(),
321 ),
322 $this->placeholders
323 );
324
325 // Init settings.
326 $this->init_form_fields();
327 $this->init_settings();
328
329 // Default template base if not declared in child constructor.
330 if ( is_null( $this->template_base ) ) {
331 $this->template_base = WC()->plugin_path() . '/templates/';
332 }
333
334 $this->email_type = $this->get_option( 'email_type' );
335 $this->enabled = $this->get_option( 'enabled' );
336 if ( FeaturesUtil::feature_is_enabled( 'email_improvements' ) ) {
337 $this->cc = $this->get_option( 'cc', '' );
338 $this->bcc = $this->get_option( 'bcc', '' );
339 }
340
341 if ( $this->block_email_editor_enabled ) {
342 $this->personalizer = wc_get_container()->get( TransactionalEmailPersonalizer::class );
343 }
344 add_action( 'phpmailer_init', array( $this, 'handle_multipart' ) );
345 add_action( 'woocommerce_update_options_email_' . $this->id, array( $this, 'process_admin_options' ) );
346
347 // Use priority 1 to ensure our skip classes are added before lazy loading plugins process the images.
348 add_filter( 'wp_get_attachment_image_attributes', array( $this, 'prevent_lazy_loading_on_attachment' ), 1, 1 );
349 }
350
351 /**
352 * Handle multipart mail.
353 *
354 * @param PHPMailer $mailer PHPMailer object.
355 * @return PHPMailer
356 */
357 public function handle_multipart( $mailer ) {
358 if ( ! $this->sending ) {
359 return $mailer;
360 }
361
362 if ( 'multipart' === $this->get_email_type() ) {
363 $mailer->AltBody = wordwrap( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
364 preg_replace( $this->plain_search, $this->plain_replace, wp_strip_all_tags( $this->get_content_plain() ) )
365 );
366 } else {
367 $mailer->AltBody = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
368 }
369
370 $this->sending = false;
371 return $mailer;
372 }
373
374 /**
375 * Format email string.
376 *
377 * @param mixed $string Text to replace placeholders in.
378 * @return string
379 */
380 public function format_string( $string ) {
381 $find = array_keys( $this->placeholders );
382 $replace = array_values( $this->placeholders );
383
384 // If using legacy find replace, add those to our find/replace arrays first. @todo deprecate in 4.0.0.
385 $find = array_merge( (array) $this->find, $find );
386 $replace = array_merge( (array) $this->replace, $replace );
387
388 // Take care of blogname which is no longer defined as a valid placeholder.
389 $find[] = '{blogname}';
390 $replace[] = $this->get_blogname();
391
392 // If using the older style filters for find and replace, ensure the array is associative and then pass through filters. @todo deprecate in 4.0.0.
393 if ( has_filter( 'woocommerce_email_format_string_replace' ) || has_filter( 'woocommerce_email_format_string_find' ) ) {
394 $legacy_find = $this->find;
395 $legacy_replace = $this->replace;
396
397 foreach ( $this->placeholders as $find => $replace ) {
398 $legacy_key = sanitize_title( str_replace( '_', '-', trim( $find, '{}' ) ) );
399 $legacy_find[ $legacy_key ] = $find;
400 $legacy_replace[ $legacy_key ] = $replace;
401 }
402
403 $string = str_replace( apply_filters( 'woocommerce_email_format_string_find', $legacy_find, $this ), apply_filters( 'woocommerce_email_format_string_replace', $legacy_replace, $this ), $string );
404 }
405
406 /**
407 * Filter for main find/replace.
408 *
409 * @since 3.2.0
410 */
411 return apply_filters( 'woocommerce_email_format_string', str_replace( $find, $replace, $string ), $this );
412 }
413
414 /**
415 * Set the locale to the store locale for customer emails to make sure emails are in the store language.
416 */
417 public function setup_locale() {
418
419 /**
420 * Filter the ability to switch email locale.
421 *
422 * @since 6.8.0
423 *
424 * @param bool $default_value The default returned value.
425 * @param WC_Email $email The WC_Email object.
426 */
427 $switch_email_locale = apply_filters( 'woocommerce_allow_switching_email_locale', true, $this );
428
429 if ( $switch_email_locale && $this->is_customer_email() && apply_filters( 'woocommerce_email_setup_locale', true ) ) {
430 wc_switch_to_site_locale();
431 }
432 }
433
434 /**
435 * Restore the locale to the default locale. Use after finished with setup_locale.
436 */
437 public function restore_locale() {
438
439 /**
440 * Filter the ability to restore email locale.
441 *
442 * @since 6.8.0
443 *
444 * @param bool $default_value The default returned value.
445 * @param WC_Email $email The WC_Email object.
446 */
447 $restore_email_locale = apply_filters( 'woocommerce_allow_restoring_email_locale', true, $this );
448
449 if ( $restore_email_locale && $this->is_customer_email() && apply_filters( 'woocommerce_email_restore_locale', true ) ) {
450 wc_restore_locale();
451 }
452 }
453
454 /**
455 * Get available email groups with their titles.
456 *
457 * @since 10.3.0
458 * @return array Associative array of email group slugs => titles.
459 */
460 public function get_email_groups() {
461 $email_groups = array(
462 'accounts' => __( 'Accounts', 'woocommerce' ),
463 'orders' => __( 'Orders', 'woocommerce' ),
464 'order-processing' => __( 'Order updates', 'woocommerce' ), // @deprecated Please use 'order-updates' instead. Will be removed in 10.5.0.
465 'order-updates' => __( 'Order updates', 'woocommerce' ),
466 'order-exceptions' => __( 'Order changes', 'woocommerce' ), // @deprecated Please use 'order-changes' instead. Will be removed in 10.5.0.
467 'order-changes' => __( 'Order changes', 'woocommerce' ),
468 'payments' => __( 'Payments', 'woocommerce' ),
469 );
470
471 /**
472 * Filter the available email groups.
473 *
474 * @since 10.3.0
475 * @param array $email_groups Associative array of email group slugs => titles.
476 */
477 return apply_filters( 'woocommerce_email_groups', $email_groups );
478 }
479
480 /**
481 * Get the title for the current email group.
482 *
483 * @since 10.3.0
484 * @return string The email group title. Falls back to the email group slug if not found.
485 */
486 public function get_email_group_title() {
487 $email_groups = $this->get_email_groups();
488 $title = isset( $email_groups[ $this->email_group ] ) ? $email_groups[ $this->email_group ] : $this->email_group;
489
490 /**
491 * Filter the email group title.
492 *
493 * @since 10.3.0
494 * @param string $title The email group title.
495 * @param string $email_group The email group slug.
496 * @param array $email_groups Associative array of email group slugs => titles.
497 */
498 return (string) apply_filters( 'woocommerce_email_group_title', $title, $this->email_group, $email_groups );
499 }
500
501 /**
502 * Get email subject.
503 *
504 * @since 3.1.0
505 * @return string
506 */
507 public function get_default_subject() {
508 return $this->subject;
509 }
510
511 /**
512 * Get email heading.
513 *
514 * @since 3.1.0
515 * @return string
516 */
517 public function get_default_heading() {
518 return $this->heading;
519 }
520
521 /**
522 * Default content to show below main email content.
523 *
524 * @since 3.7.0
525 * @return string
526 */
527 public function get_default_additional_content() {
528 return '';
529 }
530
531 /**
532 * Return content from the additional_content field.
533 *
534 * Displayed above the footer.
535 *
536 * @since 3.7.0
537 * @return string
538 */
539 public function get_additional_content() {
540 /**
541 * Provides an opportunity to inspect and modify additional content for the email.
542 *
543 * @since 3.7.0
544 *
545 * @param string $additional_content Additional content to be added to the email.
546 * @param object|bool $object The object (ie, product or order) this email relates to, if any.
547 * @param WC_Email $email WC_Email instance managing the email.
548 */
549 return apply_filters( 'woocommerce_email_additional_content_' . $this->id, $this->format_string( $this->get_option_or_transient( 'additional_content' ) ), $this->object, $this );
550 }
551
552 /**
553 * Get email subject.
554 *
555 * @return string
556 */
557 public function get_subject() {
558 /**
559 * Provides an opportunity to inspect and modify subject for the email.
560 *
561 * @since 2.0.0
562 *
563 * @param string $subject Subject of the email.
564 * @param object|bool $object The object (ie, product or order) this email relates to, if any.
565 * @param WC_Email $email WC_Email instance managing the email.
566 */
567 $subject = apply_filters( 'woocommerce_email_subject_' . $this->id, $this->format_string( $this->get_option_or_transient( 'subject', $this->get_default_subject() ) ), $this->object, $this );
568 if ( $this->block_email_editor_enabled ) {
569 // Because the new email editor uses rich-text component for subject editing, to be ensure that the subject is always in plain text, we need to strip all tags.
570 $subject = wp_strip_all_tags( $this->personalizer->personalize_transactional_content( $subject, $this, Personalizer::RENDERING_CONTEXT_TEXT ) );
571 }
572 return $subject;
573 }
574
575
576
577 /**
578 * Get email preheader.
579 *
580 * @return string
581 */
582 public function get_preheader() {
583 /**
584 * Provides an opportunity to inspect and modify preheader for the email.
585 *
586 * @since 9.9.0
587 *
588 * @param string $preheader Preheader of the email.
589 * @param object|bool $object The object (ie, product or order) this email relates to, if any.
590 * @param WC_Email $email WC_Email instance managing the email.
591 */
592 $preheader = apply_filters( 'woocommerce_email_preheader' . $this->id, $this->format_string( $this->get_option_or_transient( 'preheader', '' ) ), $this->object, $this );
593 if ( $this->block_email_editor_enabled ) {
594 $preheader = $this->personalizer->personalize_transactional_content( $preheader, $this, Personalizer::RENDERING_CONTEXT_TEXT );
595 }
596 return $preheader;
597 }
598
599 /**
600 * Get email heading.
601 *
602 * @return string
603 */
604 public function get_heading() {
605 /**
606 * Provides an opportunity to inspect and modify heading for the email.
607 *
608 * @since 2.0.0
609 *
610 * @param string $heading Heading to be added to the email.
611 * @param object|bool $object The object (ie, product or order) this email relates to, if any.
612 * @param WC_Email $email WC_Email instance managing the email.
613 */
614 return apply_filters( 'woocommerce_email_heading_' . $this->id, $this->format_string( $this->get_option_or_transient( 'heading', $this->get_default_heading() ) ), $this->object, $this );
615 }
616
617 /**
618 * Get valid recipients.
619 *
620 * @return string
621 */
622 public function get_recipient() {
623 /**
624 * Filter the recipient for the email.
625 *
626 * @since 2.0.0
627 * @since 3.7.0 Added $email parameter.
628 * @param string $recipient Recipient.
629 * @param object $object The object (ie, product or order) this email relates to, if any.
630 * @param WC_Email $email WC_Email instance managing the email.
631 */
632 $recipient = apply_filters( 'woocommerce_email_recipient_' . $this->id, $this->recipient, $this->object, $this );
633 $recipients = array_map( 'trim', explode( ',', $recipient ?? '' ) );
634 $recipients = array_filter( $recipients, 'is_email' );
635 return implode( ', ', $recipients );
636 }
637
638 /**
639 * Get valid Cc recipients.
640 *
641 * @return string
642 */
643 public function get_cc_recipient() {
644 /**
645 * Filter the Cc recipient for the email.
646 *
647 * @since 9.8.0
648 * @param string $cc Cc recipient.
649 * @param object $object The object (ie, product or order) this email relates to, if any.
650 * @param WC_Email $email WC_Email instance managing the email.
651 */
652 $cc = apply_filters( 'woocommerce_email_cc_recipient_' . $this->id, $this->cc, $this->object, $this );
653 $ccs = array_map( 'trim', explode( ',', $cc ?? '' ) );
654 $ccs = array_filter( $ccs, 'is_email' );
655 $ccs = array_map( 'sanitize_email', $ccs );
656 return implode( ', ', $ccs );
657 }
658
659 /**
660 * Get valid Bcc recipients.
661 *
662 * @return string
663 */
664 public function get_bcc_recipient() {
665 /**
666 * Filter the Bcc recipient for the email.
667 *
668 * @since 9.8.0
669 * @param string $bcc Bcc recipient.
670 * @param object $object The object (ie, product or order) this email relates to, if any.
671 * @param WC_Email $email WC_Email instance managing the email.
672 */
673 $bcc = apply_filters( 'woocommerce_email_bcc_recipient_' . $this->id, $this->bcc, $this->object, $this );
674 $bccs = array_map( 'trim', explode( ',', $bcc ?? '' ) );
675 $bccs = array_filter( $bccs, 'is_email' );
676 $bccs = array_map( 'sanitize_email', $bccs );
677 return implode( ', ', $bccs );
678 }
679
680 /**
681 * Get email headers.
682 *
683 * @return string
684 */
685 public function get_headers() {
686 $header = 'Content-Type: ' . $this->get_content_type() . "\r\n";
687
688 // For order notification emails sent to admin, always use customer's billing email as reply-to.
689 if ( in_array( $this->id, array( 'new_order', 'cancelled_order', 'failed_order' ), true ) ) {
690 if ( $this->object && $this->object->get_billing_email() && ( $this->object->get_billing_first_name() || $this->object->get_billing_last_name() ) ) {
691 $header .= 'Reply-to: ' . $this->object->get_billing_first_name() . ' ' . $this->object->get_billing_last_name() . ' <' . $this->object->get_billing_email() . ">\r\n";
692 }
693 } else {
694 // Check if custom reply-to is enabled and configured for non-admin notification emails.
695 $reply_to_enabled = $this->get_reply_to_enabled();
696 $reply_to_address = $this->get_reply_to_address();
697 $reply_to_name = $this->get_reply_to_name();
698
699 if ( $reply_to_enabled && ! empty( $reply_to_address ) && is_email( $reply_to_address ) ) {
700 $reply_to_name = ! empty( $reply_to_name ) ? $reply_to_name : $this->get_from_name();
701 $header .= 'Reply-to: ' . $reply_to_name . ' <' . $reply_to_address . ">\r\n";
702 } elseif ( $this->get_from_address() && $this->get_from_name() ) {
703 $header .= 'Reply-to: ' . $this->get_from_name() . ' <' . $this->get_from_address() . ">\r\n";
704 }
705 }
706
707 if ( FeaturesUtil::feature_is_enabled( 'email_improvements' ) ) {
708 $cc = $this->get_cc_recipient();
709 if ( ! empty( $cc ) ) {
710 $header .= 'Cc: ' . sanitize_text_field( $cc ) . "\r\n";
711 }
712
713 $bcc = $this->get_bcc_recipient();
714 if ( ! empty( $bcc ) ) {
715 $header .= 'Bcc: ' . sanitize_text_field( $bcc ) . "\r\n";
716 }
717 }
718
719 return apply_filters( 'woocommerce_email_headers', $header, $this->id, $this->object, $this );
720 }
721
722 /**
723 * Get email attachments.
724 *
725 * @return array
726 */
727 public function get_attachments() {
728 return apply_filters( 'woocommerce_email_attachments', array(), $this->id, $this->object, $this );
729 }
730
731 /**
732 * Return email type.
733 *
734 * @return string
735 */
736 public function get_email_type() {
737 $email_type = $this->email_type;
738 /**
739 * This filter is documented in templates/emails/email-styles.php
740 *
741 * @since 9.6.0
742 * @param bool $is_email_preview Whether the email is being previewed.
743 */
744 $is_email_preview = apply_filters( 'woocommerce_is_email_preview', false );
745 // Transient is used for live email preview without saving the settings.
746 if ( $is_email_preview ) {
747 $transient = get_transient( "woocommerce_{$this->id}_email_type" );
748 $email_type = $transient ? $transient : $email_type;
749 }
750 return $email_type && class_exists( 'DOMDocument' ) ? $email_type : 'plain';
751 }
752
753 /**
754 * Get block editor email template content.
755 *
756 * @return string
757 */
758 public function get_block_editor_email_template_content() {
759 return wc_get_template_html(
760 $this->template_block_content,
761 array(
762 'order' => $this->object,
763 'sent_to_admin' => false,
764 'plain_text' => false,
765 'email' => $this,
766 )
767 );
768 }
769
770 /**
771 * Get email content type.
772 *
773 * @param string $default_content_type Default wp_mail() content type.
774 * @return string
775 */
776 public function get_content_type( $default_content_type = '' ) {
777 switch ( $this->get_email_type() ) {
778 case 'html':
779 $content_type = 'text/html';
780 break;
781 case 'multipart':
782 $content_type = 'multipart/alternative';
783 break;
784 default:
785 $content_type = 'text/plain';
786 break;
787 }
788
789 return apply_filters( 'woocommerce_email_content_type', $content_type, $this, $default_content_type );
790 }
791
792 /**
793 * Return the email's title
794 *
795 * @return string
796 */
797 public function get_title() {
798 return apply_filters( 'woocommerce_email_title', $this->title, $this );
799 }
800
801 /**
802 * Return the email's description
803 *
804 * @return string
805 */
806 public function get_description() {
807 return apply_filters( 'woocommerce_email_description', $this->description, $this );
808 }
809
810 /**
811 * Proxy to parent's get_option and attempt to localize the result using gettext.
812 *
813 * @param string $key Option key.
814 * @param mixed $empty_value Value to use when option is empty.
815 * @return string
816 */
817 public function get_option( $key, $empty_value = null ) {
818 $value = parent::get_option( $key, $empty_value );
819 return apply_filters( 'woocommerce_email_get_option', $value, $this, $value, $key, $empty_value );
820 }
821
822 /**
823 * Checks if this email is enabled and will be sent.
824 *
825 * @return bool
826 */
827 public function is_enabled() {
828 return apply_filters( 'woocommerce_email_enabled_' . $this->id, 'yes' === $this->enabled, $this->object, $this );
829 }
830
831 /**
832 * Checks if this email is manually sent
833 *
834 * @return bool
835 */
836 public function is_manual() {
837 return $this->manual;
838 }
839
840 /**
841 * Checks if this email is customer focussed.
842 *
843 * @return bool
844 */
845 public function is_customer_email() {
846 return $this->customer_email;
847 }
848
849 /**
850 * Get WordPress blog name.
851 *
852 * @return string
853 */
854 public function get_blogname() {
855 return wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
856 }
857
858 /**
859 * Get email content.
860 *
861 * @return string
862 */
863 public function get_content() {
864 $this->sending = true;
865
866 $block_email_content = $this->get_block_email_html_content();
867 if ( $block_email_content ) {
868 $this->email_type = 'plain' === $this->email_type ? 'html' : $this->email_type;
869 return $block_email_content;
870 }
871
872 if ( 'plain' === $this->get_email_type() ) {
873 $email_content = wordwrap( preg_replace( $this->plain_search, $this->plain_replace, wp_strip_all_tags( $this->get_content_plain() ) ), 70 );
874 } else {
875 $email_content = $this->get_content_html();
876 }
877
878 return $email_content;
879 }
880
881 /**
882 * Apply inline styles to dynamic content.
883 *
884 * We only inline CSS for html emails.
885 *
886 * @version 10.2.0
887 * @param string|null $content Content that will receive inline styles.
888 * @return string
889 */
890 public function style_inline( $content ) {
891 if ( in_array( $this->get_content_type(), array( 'text/html', 'multipart/alternative' ), true ) ) {
892 /**
893 * Filter to allow the ability to override the email inline styling method.
894 *
895 * @since 10.2.0
896 *
897 * @param callable $style_inline_callback The default email inline styling callback.
898 * @param string|null $content Content that will receive inline styles.
899 * @param WC_Email $email The WC_Email object.
900 */
901 $style_inline_callback = apply_filters( 'woocommerce_mail_style_inline_callback', array( $this, 'apply_inline_style' ), $content, $this );
902
903 if ( ! is_callable( $style_inline_callback ) ) {
904 $style_inline_callback = array( $this, 'apply_inline_style' );
905 }
906
907 return call_user_func( $style_inline_callback, $content );
908 }
909
910 return $content;
911 }
912
913
914 /**
915 * Apply inline styles to dynamic content using Emogrifier library (if supported).
916 *
917 * @since 10.2.0
918 * @param string|null $content Content that will receive inline styles.
919 * @return string
920 */
921 private function apply_inline_style( $content ) {
922 $css = '';
923 $css .= $this->get_must_use_css_styles();
924 $css .= "\n";
925
926 ob_start();
927 wc_get_template( 'emails/email-styles.php' );
928 $css .= ob_get_clean();
929
930 /**
931 * Provides an opportunity to filter the CSS styles included in e-mails.
932 *
933 * @since 2.3.0
934 *
935 * @param string $css CSS code.
936 * @param \WC_Email $email E-mail instance.
937 */
938 $css = apply_filters( 'woocommerce_email_styles', $css, $this );
939
940 $css_inliner_class = CssInliner::class;
941
942 if ( $this->supports_emogrifier() && class_exists( $css_inliner_class ) ) {
943 try {
944 $css_inliner = CssInliner::fromHtml( $content )->inlineCss( $css );
945
946 /**
947 * Action hook fired when an email content has been processed by Emogrifier CssInliner instance.
948 *
949 * @since 4.1.0
950 *
951 * @param CssInliner $css_inliner CssInliner instance.
952 * @param WC_Email $email WC_Email instance.
953 */
954 do_action( 'woocommerce_emogrifier', $css_inliner, $this );
955
956 $dom_document = $css_inliner->getDomDocument();
957
958 // When the email is rendered in the block editor, we don't want to remove the elements with display: none.
959 // The main reason is using preview text in the email body which is hidden by default.
960 if ( ! $this->block_email_editor_enabled ) {
961 HtmlPruner::fromDomDocument( $dom_document )->removeElementsWithDisplayNone();
962 }
963 $content = CssToAttributeConverter::fromDomDocument( $dom_document )
964 ->convertCssToVisualAttributes()
965 ->render();
966 } catch ( Exception $e ) {
967 $logger = wc_get_logger();
968 $logger->error( $e->getMessage(), array( 'source' => 'emogrifier' ) );
969 }
970 } else {
971 $content = '<style type="text/css">' . $css . '</style>' . $content;
972 }
973
974 return $content;
975 }
976
977 /**
978 * Returns CSS styles that should be included with all HTML e-mails, regardless of theme specific customizations.
979 *
980 * @since 9.1.0
981 *
982 * @return string
983 */
984 protected function get_must_use_css_styles(): string {
985 $css = <<<'EOF'
986
987 /*
988 * Temporary measure until e-mail clients more properly support the correct styles.
989 * See https://github.com/woocommerce/woocommerce/pull/47738.
990 */
991 .screen-reader-text {
992 display: none;
993 }
994
995 EOF;
996
997 return $css;
998 }
999
1000 /**
1001 * Return if emogrifier library is supported.
1002 *
1003 * @version 4.0.0
1004 * @since 3.5.0
1005 * @return bool
1006 */
1007 protected function supports_emogrifier() {
1008 return class_exists( 'DOMDocument' );
1009 }
1010
1011 /**
1012 * Get the email content in plain text format.
1013 *
1014 * @return string
1015 */
1016 public function get_content_plain() {
1017 return '';
1018 }
1019
1020 /**
1021 * Get the email content in HTML format.
1022 *
1023 * @return string
1024 */
1025 public function get_content_html() {
1026 return '';
1027 }
1028
1029 /**
1030 * Get the from name for outgoing emails.
1031 *
1032 * @param string $from_name Default wp_mail() name associated with the "from" email address.
1033 * @return string
1034 */
1035 public function get_from_name( $from_name = '' ) {
1036 $default = get_bloginfo( 'name', 'display' );
1037 /**
1038 * Filters the "from" name for outgoing emails.
1039 *
1040 * @since 2.1.0
1041 *
1042 * @param string|mixed $from_name The from name.
1043 * @param WC_Email $email Email object.
1044 * @param string $default_from_name Default from name.
1045 */
1046 $from_name = apply_filters( 'woocommerce_email_from_name', get_option( 'woocommerce_email_from_name', $default ), $this, $from_name );
1047 return wp_specialchars_decode( esc_html( $from_name ), ENT_QUOTES );
1048 }
1049
1050 /**
1051 * Get the from address for outgoing emails.
1052 *
1053 * @param string $from_email Default wp_mail() email address to send from.
1054 * @return string
1055 */
1056 public function get_from_address( $from_email = '' ) {
1057 $from_email = apply_filters( 'woocommerce_email_from_address', get_option( 'woocommerce_email_from_address' ), $this, $from_email );
1058 return sanitize_email( $from_email );
1059 }
1060
1061 /**
1062 * Check if reply-to is enabled for outgoing emails.
1063 *
1064 * @return bool
1065 */
1066 public function get_reply_to_enabled() {
1067 /**
1068 * Filter whether reply-to is enabled for emails.
1069 *
1070 * @since 10.4.0
1071 * @param bool $enabled Whether reply-to is enabled.
1072 * @param WC_Email $email WC_Email instance managing the email.
1073 */
1074 $enabled = apply_filters( 'woocommerce_email_reply_to_enabled', 'yes' === get_option( 'woocommerce_email_reply_to_enabled', 'no' ), $this );
1075 return (bool) $enabled;
1076 }
1077
1078 /**
1079 * Get the reply-to name for outgoing emails.
1080 *
1081 * @param string $reply_to_name Default reply-to name.
1082 * @return string
1083 */
1084 public function get_reply_to_name( $reply_to_name = '' ) {
1085 /**
1086 * Filter the reply-to name for emails.
1087 *
1088 * @since 10.4.0
1089 * @param string $reply_to_name Reply-to name.
1090 * @param WC_Email $email WC_Email instance managing the email.
1091 * @param string $default_name Default reply-to name.
1092 */
1093 $reply_to_name = apply_filters( 'woocommerce_email_reply_to_name', get_option( 'woocommerce_email_reply_to_name', '' ), $this, $reply_to_name );
1094 return wp_specialchars_decode( sanitize_text_field( $reply_to_name ), ENT_QUOTES );
1095 }
1096
1097 /**
1098 * Get the reply-to address for outgoing emails.
1099 *
1100 * @param string $reply_to_email Default reply-to email address.
1101 * @return string
1102 */
1103 public function get_reply_to_address( $reply_to_email = '' ) {
1104 /**
1105 * Filter the reply-to address for emails.
1106 *
1107 * @since 10.4.0
1108 * @param string $reply_to_email Reply-to email address.
1109 * @param WC_Email $email WC_Email instance managing the email.
1110 * @param string $default_email Default reply-to email address.
1111 */
1112 $reply_to_email = apply_filters( 'woocommerce_email_reply_to_address', get_option( 'woocommerce_email_reply_to_address', '' ), $this, $reply_to_email );
1113 return sanitize_email( $reply_to_email );
1114 }
1115
1116 /**
1117 * Set the object for the outgoing email.
1118 *
1119 * @param object $object Object this email is for, e.g. customer, or product.
1120 * @return void
1121 */
1122 public function set_object( $object ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
1123 $this->object = $object;
1124 }
1125
1126 /**
1127 * Send the email notification when enabled and a recipient is available.
1128 *
1129 * This is the standard helper used by trigger() methods. It checks whether the email
1130 * is enabled and whether a recipient address exists, fires appropriate action hooks for
1131 * the disabled or skipped outcome, and otherwise delegates to send() with the
1132 * standard content parameters.
1133 *
1134 * Subclasses that intentionally bypass the enabled check (e.g. manually-triggered invoice
1135 * emails, POS receipts) should NOT call this method and should continue to call send()
1136 * directly.
1137 *
1138 * @since 10.9.0
1139 * @return bool Whether the email was sent successfully.
1140 */
1141 protected function send_notification(): bool {
1142 if ( ! $this->is_enabled() ) {
1143 /**
1144 * Fires when a transactional email is not sent because the email type is disabled.
1145 *
1146 * @since 10.9.0
1147 *
1148 * @param string $email_id The email type ID (e.g. `customer_processing_order`).
1149 * @param WC_Email $email The WC_Email instance.
1150 */
1151 do_action( 'woocommerce_email_disabled', $this->id, $this );
1152 return false;
1153 }
1154
1155 $recipient = $this->get_recipient();
1156
1157 if ( ! $recipient ) {
1158 /**
1159 * Fires when a transactional email is not sent for a reason other than being disabled.
1160 *
1161 * The $reason parameter identifies why the email was not sent:
1162 * - WC_Email::SKIP_REASON_NO_RECIPIENT: No recipient address was available at send time.
1163 *
1164 * @since 10.9.0
1165 *
1166 * @param string $reason Short identifier for why the email was skipped.
1167 * @param string $email_id The email type ID.
1168 * @param WC_Email $email The WC_Email instance.
1169 */
1170 do_action( 'woocommerce_email_skipped', self::SKIP_REASON_NO_RECIPIENT, $this->id, $this );
1171 return false;
1172 }
1173
1174 return $this->send(
1175 $recipient,
1176 $this->get_subject(),
1177 $this->get_content(),
1178 $this->get_headers(),
1179 $this->get_attachments()
1180 );
1181 }
1182
1183 /**
1184 * Send the email when a recipient is available, regardless of the enabled setting.
1185 *
1186 * This helper is intended for manually-triggered emails (e.g. invoice resend, POS receipts)
1187 * that intentionally bypass the enabled/disabled check. It fires
1188 * `woocommerce_email_skipped` with reason {@see WC_Email::SKIP_REASON_NO_RECIPIENT} when
1189 * no recipient is available so the outcome is still observable via the EmailLogger, and
1190 * otherwise delegates to send().
1191 *
1192 * @since 10.9.0
1193 * @return bool Whether the email was sent successfully.
1194 */
1195 protected function send_if_recipient(): bool {
1196 $recipient = $this->get_recipient();
1197
1198 if ( ! $recipient ) {
1199 /**
1200 * Fires when a transactional email is not sent for a reason other than being disabled.
1201 *
1202 * This action is documented in includes/emails/class-wc-email.php
1203 *
1204 * @since 10.9.0
1205 */
1206 do_action( 'woocommerce_email_skipped', self::SKIP_REASON_NO_RECIPIENT, $this->id, $this );
1207 return false;
1208 }
1209
1210 return $this->send(
1211 $recipient,
1212 $this->get_subject(),
1213 $this->get_content(),
1214 $this->get_headers(),
1215 $this->get_attachments()
1216 );
1217 }
1218
1219 /**
1220 * Send an email.
1221 *
1222 * @param string $to Email to.
1223 * @param string $subject Email subject.
1224 * @param string $message Email message.
1225 * @param string $headers Email headers.
1226 * @param array $attachments Email attachments.
1227 * @return bool success
1228 */
1229 public function send( $to, $subject, $message, $headers, $attachments ) {
1230 add_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );
1231 add_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) );
1232 add_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) );
1233
1234 $message = apply_filters( 'woocommerce_mail_content', $this->style_inline( $message ) );
1235 $mail_callback = apply_filters( 'woocommerce_mail_callback', 'wp_mail', $this );
1236 $mail_callback_params = apply_filters( 'woocommerce_mail_callback_params', array( $to, wp_specialchars_decode( $subject ), $message, $headers, $attachments ), $this );
1237 $return = $mail_callback( ...$mail_callback_params );
1238 if ( ! is_bool( $return ) ) {
1239 $original_type = gettype( $return );
1240
1241 $return = is_scalar( $return ) ? wc_string_to_bool( (string) $return ) : false;
1242
1243 wc_doing_it_wrong(
1244 __METHOD__,
1245 sprintf(
1246 'The callback registered to the woocommerce_mail_callback filter should return a boolean; %s returned.',
1247 $original_type
1248 ),
1249 '11.1.0'
1250 );
1251 }
1252
1253 remove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );
1254 remove_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) );
1255 remove_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) );
1256
1257 // Clear the AltBody (if set) so that it does not leak across to different emails.
1258 $this->clear_alt_body_field();
1259
1260 /**
1261 * Action hook fired when an email is sent.
1262 *
1263 * @since 5.6.0
1264 * @param bool $return Whether the email was sent successfully.
1265 * @param string $id Email ID.
1266 * @param WC_Email $email WC_Email instance.
1267 */
1268 do_action( 'woocommerce_email_sent', $return, (string) $this->id, $this );
1269
1270 return $return;
1271 }
1272
1273 /**
1274 * Initialise Settings Form Fields - these are generic email options most will use.
1275 */
1276 public function init_form_fields() {
1277 /* translators: %s: list of placeholders */
1278 $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '<code>' . esc_html( implode( '</code>, <code>', array_keys( $this->placeholders ) ) ) . '</code>' );
1279 $this->form_fields = array(
1280 'enabled' => array(
1281 'title' => __( 'Enable/Disable', 'woocommerce' ),
1282 'type' => 'checkbox',
1283 'label' => __( 'Enable this email notification', 'woocommerce' ),
1284 'default' => 'yes',
1285 ),
1286 'subject' => array(
1287 'title' => __( 'Subject', 'woocommerce' ),
1288 'type' => 'text',
1289 'desc_tip' => true,
1290 'description' => $placeholder_text,
1291 'placeholder' => $this->get_default_subject(),
1292 'default' => '',
1293 ),
1294 'heading' => array(
1295 'title' => __( 'Email heading', 'woocommerce' ),
1296 'type' => 'text',
1297 'desc_tip' => true,
1298 'description' => $placeholder_text,
1299 'placeholder' => $this->get_default_heading(),
1300 'default' => '',
1301 ),
1302 'additional_content' => array(
1303 'title' => __( 'Additional content', 'woocommerce' ),
1304 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text,
1305 'css' => 'width:400px; height: 75px;',
1306 'placeholder' => __( 'N/A', 'woocommerce' ),
1307 'type' => 'textarea',
1308 'default' => $this->get_default_additional_content(),
1309 'desc_tip' => true,
1310 ),
1311 'email_type' => array(
1312 'title' => __( 'Email type', 'woocommerce' ),
1313 'type' => 'select',
1314 'description' => __( 'Choose which format of email to send.', 'woocommerce' ),
1315 'default' => 'html',
1316 'class' => 'email_type wc-enhanced-select',
1317 'options' => $this->get_email_type_options(),
1318 'desc_tip' => true,
1319 ),
1320 );
1321 if ( FeaturesUtil::feature_is_enabled( 'email_improvements' ) ) {
1322 $this->form_fields['cc'] = $this->get_cc_field();
1323 $this->form_fields['bcc'] = $this->get_bcc_field();
1324 }
1325 if ( $this->block_email_editor_enabled ) {
1326 $this->form_fields['preheader'] = $this->get_preheader_field();
1327 }
1328 }
1329
1330 /**
1331 * Get the cc field definition.
1332 *
1333 * @return array
1334 */
1335 protected function get_cc_field() {
1336 return array(
1337 'title' => __( 'Cc(s)', 'woocommerce' ),
1338 'type' => 'text',
1339 /* translators: %s: admin email */
1340 'description' => __( 'Enter Cc recipients (comma-separated) for this email.', 'woocommerce' ),
1341 'placeholder' => '',
1342 'default' => '',
1343 'desc_tip' => true,
1344 );
1345 }
1346
1347 /**
1348 * Get the bcc field definition.
1349 *
1350 * @return array
1351 */
1352 protected function get_bcc_field() {
1353 return array(
1354 'title' => __( 'Bcc(s)', 'woocommerce' ),
1355 'type' => 'text',
1356 /* translators: %s: admin email */
1357 'description' => __( 'Enter Bcc recipients (comma-separated) for this email.', 'woocommerce' ),
1358 'placeholder' => '',
1359 'default' => '',
1360 'desc_tip' => true,
1361 );
1362 }
1363
1364 /**
1365 * Get the preheader field definition.
1366 *
1367 * @return array
1368 */
1369 protected function get_preheader_field() {
1370 return array(
1371 'title' => __( 'Preheader', 'woocommerce' ),
1372 'description' => __( 'Shown as a preview in the Inbox, next to the subject line. (Max 150 characters).', 'woocommerce' ),
1373 'placeholder' => '',
1374 'type' => 'text',
1375 'default' => '',
1376 'desc_tip' => true,
1377 );
1378 }
1379
1380 /**
1381 * Email type options.
1382 *
1383 * @return array
1384 */
1385 public function get_email_type_options() {
1386 $types = array( 'plain' => __( 'Plain text', 'woocommerce' ) );
1387
1388 if ( class_exists( 'DOMDocument' ) ) {
1389 $types['html'] = __( 'HTML', 'woocommerce' );
1390 $types['multipart'] = __( 'Multipart', 'woocommerce' );
1391 }
1392
1393 return $types;
1394 }
1395
1396 /**
1397 * Admin Panel Options Processing.
1398 */
1399 public function process_admin_options() {
1400 // Save regular options.
1401 parent::process_admin_options();
1402
1403 $post_data = $this->get_post_data();
1404
1405 // Save templates.
1406 if ( isset( $post_data['template_html_code'] ) ) {
1407 $this->save_template( $post_data['template_html_code'], $this->template_html );
1408 }
1409 if ( isset( $post_data['template_plain_code'] ) ) {
1410 $this->save_template( $post_data['template_plain_code'], $this->template_plain );
1411 }
1412 }
1413
1414 /**
1415 * Get template.
1416 *
1417 * @param string $type Template type. Can be either 'template_html', 'template_plain' or 'template_block'.
1418 * @return string
1419 */
1420 public function get_template( $type ) {
1421 $type = basename( $type );
1422
1423 if ( 'template_html' === $type ) {
1424 return $this->template_html;
1425 } elseif ( 'template_plain' === $type ) {
1426 return $this->template_plain;
1427 } elseif ( 'template_block' === $type ) {
1428 return $this->template_block;
1429 }
1430 return '';
1431 }
1432
1433 /**
1434 * Save the email templates.
1435 *
1436 * @since 2.4.0
1437 * @param string $template_code Template code.
1438 * @param string $template_path Template path.
1439 */
1440 protected function save_template( $template_code, $template_path ) {
1441 if ( current_user_can( 'edit_themes' ) && ! empty( $template_code ) && ! empty( $template_path ) ) {
1442 $saved = false;
1443 $file = $this->get_theme_template_file( $template_path );
1444 $code = wp_unslash( $template_code );
1445
1446 if ( is_writeable( $file ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writeable
1447 $f = fopen( $file, 'w+' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen
1448
1449 if ( false !== $f ) {
1450 fwrite( $f, $code ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite
1451 fclose( $f ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
1452 $saved = true;
1453 }
1454 }
1455
1456 if ( ! $saved ) {
1457 $redirect = add_query_arg( 'wc_error', rawurlencode( __( 'Could not write to template file.', 'woocommerce' ) ) );
1458 wp_safe_redirect( $redirect );
1459 exit;
1460 }
1461 wc_clear_template_cache();
1462 }
1463 }
1464
1465 /**
1466 * Get the template file in the current theme.
1467 *
1468 * @param string $template Template name.
1469 *
1470 * @return string
1471 */
1472 public function get_theme_template_file( $template ) {
1473 return get_stylesheet_directory() . '/' . apply_filters( 'woocommerce_template_directory', 'woocommerce', $template ) . '/' . $template;
1474 }
1475
1476 /**
1477 * Move template action.
1478 *
1479 * @param string $template_type Template type.
1480 */
1481 protected function move_template_action( $template_type ) {
1482 $template = $this->get_template( $template_type );
1483 if ( ! empty( $template ) ) {
1484 $theme_file = $this->get_theme_template_file( $template );
1485
1486 if ( wp_mkdir_p( dirname( $theme_file ) ) && ! file_exists( $theme_file ) ) {
1487
1488 // Locate template file.
1489 $core_file = $this->template_base . $template;
1490 $template_file = apply_filters( 'woocommerce_locate_core_template', $core_file, $template, $this->template_base, $this->id );
1491
1492 // Copy template file.
1493 copy( $template_file, $theme_file );
1494
1495 /**
1496 * Action hook fired after copying email template file.
1497 *
1498 * @param string $template_type The copied template type
1499 * @param string $email The email object
1500 */
1501 do_action( 'woocommerce_copy_email_template', $template_type, $this );
1502
1503 wc_clear_template_cache();
1504 ?>
1505 <div class="updated">
1506 <p><?php echo esc_html__( 'Template file copied to theme.', 'woocommerce' ); ?></p>
1507 </div>
1508 <?php
1509 }
1510 }
1511 }
1512
1513 /**
1514 * Delete template action.
1515 *
1516 * @param string $template_type Template type.
1517 */
1518 protected function delete_template_action( $template_type ) {
1519 $template = $this->get_template( $template_type );
1520
1521 if ( $template ) {
1522 if ( ! empty( $template ) ) {
1523 $theme_file = $this->get_theme_template_file( $template );
1524
1525 if ( file_exists( $theme_file ) ) {
1526 unlink( $theme_file ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink
1527
1528 /**
1529 * Action hook fired after deleting template file.
1530 *
1531 * @param string $template The deleted template type
1532 * @param string $email The email object
1533 */
1534 do_action( 'woocommerce_delete_email_template', $template_type, $this );
1535
1536 wc_clear_template_cache();
1537 ?>
1538 <div class="updated">
1539 <p><?php echo esc_html__( 'Template file deleted from theme.', 'woocommerce' ); ?></p>
1540 </div>
1541 <?php
1542 }
1543 }
1544 }
1545 }
1546
1547 /**
1548 * Admin actions.
1549 */
1550 protected function admin_actions() {
1551 // Handle any actions.
1552 if (
1553 ( ! empty( $this->template_html ) || ! empty( $this->template_plain ) )
1554 && ( ! empty( $_GET['move_template'] ) || ! empty( $_GET['delete_template'] ) )
1555 && 'GET' === $_SERVER['REQUEST_METHOD'] // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
1556 ) {
1557 if ( empty( $_GET['_wc_email_nonce'] ) || ! wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wc_email_nonce'] ) ), 'woocommerce_email_template_nonce' ) ) {
1558 wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) );
1559 }
1560
1561 if ( ! current_user_can( 'edit_themes' ) ) {
1562 wp_die( esc_html__( 'You don&#8217;t have permission to do this.', 'woocommerce' ) );
1563 }
1564
1565 if ( ! empty( $_GET['move_template'] ) ) {
1566 $this->move_template_action( wc_clean( wp_unslash( $_GET['move_template'] ) ) );
1567 }
1568
1569 if ( ! empty( $_GET['delete_template'] ) ) {
1570 $this->delete_template_action( wc_clean( wp_unslash( $_GET['delete_template'] ) ) );
1571 }
1572 }
1573 }
1574
1575 /**
1576 * Admin Options.
1577 *
1578 * Setup the email settings screen.
1579 * Override this in your email.
1580 *
1581 * @since 1.0.0
1582 */
1583 public function admin_options() {
1584 // Do admin actions.
1585 $this->admin_actions();
1586 ?>
1587 <?php wc_back_header( $this->get_title(), __( 'Return to emails', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=email' ) ); ?>
1588
1589 <?php echo wpautop( wp_kses_post( $this->get_description() ) ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
1590
1591 <?php
1592 /**
1593 * Action hook fired before displaying email settings.
1594 *
1595 * @param string $email The email object
1596 */
1597 do_action( 'woocommerce_email_settings_before', $this );
1598 ?>
1599
1600 <table class="form-table">
1601 <?php $this->generate_settings_html(); ?>
1602 </table>
1603
1604 <?php
1605 /**
1606 * Action hook fired after displaying email settings.
1607 *
1608 * @param string $email The email object
1609 */
1610 do_action( 'woocommerce_email_settings_after', $this );
1611 ?>
1612
1613 <?php
1614
1615 if ( current_user_can( 'edit_themes' ) && ( ! empty( $this->template_html ) || ! empty( $this->template_plain ) ) ) {
1616 ?>
1617 <div id="template">
1618 <?php
1619 $templates = array(
1620 'template_html' => __( 'HTML template', 'woocommerce' ),
1621 'template_plain' => __( 'Plain text template', 'woocommerce' ),
1622 );
1623
1624 foreach ( $templates as $template_type => $title ) :
1625 $template = $this->get_template( $template_type );
1626
1627 if ( empty( $template ) ) {
1628 continue;
1629 }
1630
1631 $local_file = $this->get_theme_template_file( $template );
1632 $core_file = $this->template_base . $template;
1633 $template_file = apply_filters( 'woocommerce_locate_core_template', $core_file, $template, $this->template_base, $this->id );
1634 $template_dir = apply_filters( 'woocommerce_template_directory', 'woocommerce', $template );
1635 ?>
1636 <div class="template <?php echo esc_attr( $template_type ); ?>">
1637 <h4><?php echo wp_kses_post( $title ); ?></h4>
1638
1639 <?php if ( file_exists( $local_file ) ) : ?>
1640 <p>
1641 <a href="#" class="button toggle_editor"></a>
1642
1643 <?php if ( is_writable( $local_file ) ) : // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable ?>
1644 <a href="<?php echo esc_url( wp_nonce_url( remove_query_arg( array( 'move_template', 'saved' ), add_query_arg( 'delete_template', $template_type ) ), 'woocommerce_email_template_nonce', '_wc_email_nonce' ) ); ?>" class="delete_template button">
1645 <?php esc_html_e( 'Delete template file', 'woocommerce' ); ?>
1646 </a>
1647 <?php endif; ?>
1648
1649 <?php
1650 /* translators: %s: Path to template file */
1651 printf( esc_html__( 'This template has been overridden by your theme and can be found in: %s.', 'woocommerce' ), '<code>' . esc_html( trailingslashit( basename( get_stylesheet_directory() ) ) . $template_dir . '/' . $template ) . '</code>' );
1652 ?>
1653 </p>
1654
1655 <div class="editor" style="display:none">
1656 <textarea class="code" cols="25" rows="20"
1657 <?php
1658 if ( ! is_writable( $local_file ) ) : // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable
1659 ?>
1660 readonly="readonly" disabled="disabled"
1661 <?php else : ?>
1662 data-name="<?php echo esc_attr( $template_type ) . '_code'; ?>"<?php endif; ?>><?php echo esc_html( file_get_contents( $local_file ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents ?></textarea>
1663 </div>
1664 <?php elseif ( file_exists( $template_file ) ) : ?>
1665 <p>
1666 <a href="#" class="button toggle_editor"></a>
1667
1668 <?php
1669 $emails_dir = get_stylesheet_directory() . '/' . $template_dir . '/emails';
1670 $templates_dir = get_stylesheet_directory() . '/' . $template_dir;
1671 $theme_dir = get_stylesheet_directory();
1672
1673 if ( is_dir( $emails_dir ) ) {
1674 $target_dir = $emails_dir;
1675 } elseif ( is_dir( $templates_dir ) ) {
1676 $target_dir = $templates_dir;
1677 } else {
1678 $target_dir = $theme_dir;
1679 }
1680
1681 if ( is_writable( $target_dir ) ) : // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable
1682 ?>
1683 <a href="<?php echo esc_url( wp_nonce_url( remove_query_arg( array( 'delete_template', 'saved' ), add_query_arg( 'move_template', $template_type ) ), 'woocommerce_email_template_nonce', '_wc_email_nonce' ) ); ?>" class="button">
1684 <?php esc_html_e( 'Copy file to theme', 'woocommerce' ); ?>
1685 </a>
1686 <?php endif; ?>
1687
1688 <?php
1689 /* translators: 1: Path to template file 2: Path to theme folder */
1690 printf( esc_html__( 'To override and edit this email template copy %1$s to your theme folder: %2$s.', 'woocommerce' ), '<code>' . esc_html( plugin_basename( $template_file ) ) . '</code>', '<code>' . esc_html( trailingslashit( basename( get_stylesheet_directory() ) ) . $template_dir . '/' . $template ) . '</code>' );
1691 ?>
1692 </p>
1693
1694 <div class="editor" style="display:none">
1695 <textarea class="code" readonly="readonly" disabled="disabled" cols="25" rows="20"><?php echo esc_html( file_get_contents( $template_file ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents ?></textarea>
1696 </div>
1697 <?php else : ?>
1698 <p><?php esc_html_e( 'File was not found.', 'woocommerce' ); ?></p>
1699 <?php endif; ?>
1700 </div>
1701 <?php endforeach; ?>
1702 </div>
1703
1704 <?php
1705 $handle = 'wc-admin-settings-email';
1706 wp_register_script( $handle, '', array( 'jquery' ), WC_VERSION, array( 'in_footer' => true ) );
1707 wp_enqueue_script( $handle );
1708 wp_add_inline_script(
1709 $handle,
1710 "jQuery( 'select.email_type' ).on( 'change', function() {
1711
1712 const val = jQuery( this ).val();
1713
1714 jQuery( '.template_plain, .template_html' ).show();
1715
1716 if ( val != 'multipart' && val != 'html' ) {
1717 jQuery('.template_html').hide();
1718 }
1719
1720 if ( val != 'multipart' && val != 'plain' ) {
1721 jQuery('.template_plain').hide();
1722 }
1723
1724 }).trigger( 'change' );
1725
1726 const view = '" . esc_js( __( 'View template', 'woocommerce' ) ) . "';
1727 const hide = '" . esc_js( __( 'Hide template', 'woocommerce' ) ) . "';
1728
1729 jQuery( 'a.toggle_editor' ).text( view ).on( 'click', function() {
1730 let label = hide;
1731
1732 if ( jQuery( this ).closest(' .template' ).find( '.editor' ).is(':visible') ) {
1733 label = view;
1734 }
1735
1736 jQuery( this ).text( label ).closest(' .template' ).find( '.editor' ).slideToggle();
1737 return false;
1738 } );
1739
1740 jQuery( 'a.delete_template' ).on( 'click', function() {
1741 if ( window.confirm('" . esc_js( __( 'Are you sure you want to delete this template file?', 'woocommerce' ) ) . "') ) {
1742 return true;
1743 }
1744
1745 return false;
1746 });
1747
1748 jQuery( '.editor textarea' ).on( 'change', function() {
1749 const name = jQuery( this ).attr( 'data-name' );
1750
1751 if ( name ) {
1752 jQuery( this ).attr( 'name', name );
1753 }
1754 });"
1755 );
1756 }
1757 }
1758
1759 /**
1760 * Clears the PhpMailer AltBody field, to prevent that content from leaking across emails.
1761 */
1762 private function clear_alt_body_field(): void {
1763 global $phpmailer;
1764
1765 if ( $phpmailer instanceof PHPMailer\PHPMailer\PHPMailer ) {
1766 $phpmailer->AltBody = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1767 }
1768 }
1769
1770 /**
1771 * Get an option or transient for email preview.
1772 *
1773 * @param string $key Option key.
1774 * @param mixed $empty_value Value to use when option is empty.
1775 */
1776 protected function get_option_or_transient( string $key, $empty_value = null ) {
1777 $option = $this->get_option( $key, $empty_value );
1778
1779 /**
1780 * This filter is documented in templates/emails/email-styles.php
1781 *
1782 * @since 9.6.0
1783 * @param bool $is_email_preview Whether the email is being previewed.
1784 */
1785 $is_email_preview = apply_filters( 'woocommerce_is_email_preview', false );
1786 if ( $is_email_preview ) {
1787 $plugin_id = $this->plugin_id;
1788 $email_id = $this->id;
1789 $transient = get_transient( "{$plugin_id}{$email_id}_{$key}" );
1790 if ( false !== $transient ) {
1791 $option = $transient ? $transient : $empty_value;
1792 }
1793 }
1794
1795 return $option;
1796 }
1797
1798 /**
1799 * Gerenerates the HTML content for the email from a block based email.
1800 * and if so, it renders the block email content.
1801 *
1802 * @return string|null
1803 */
1804 private function get_block_email_html_content(): ?string {
1805 if ( ! $this->block_email_editor_enabled ) {
1806 return null;
1807 }
1808
1809 /** Service for rendering emails from block content @var BlockEmailRenderer $renderer */
1810 $renderer = wc_get_container()->get( BlockEmailRenderer::class );
1811 return $renderer->maybe_render_block_email( $this );
1812 }
1813
1814 /**
1815 * Prevent lazy loading on attachment images in email context by adding skip classes.
1816 * This is hooked into the wp_get_attachment_image_attributes filter.
1817 *
1818 * @param array $attributes The image attributes array.
1819 * @return array The modified image attributes array.
1820 */
1821 public function prevent_lazy_loading_on_attachment( $attributes ) {
1822 // Only process if we're currently sending an email.
1823 if ( ! $this->sending ) {
1824 return $attributes;
1825 }
1826
1827 // Skip classes to prevent lazy loading plugins from applying lazy loading.
1828 // These are the most common skip classes used by popular lazy loading plugins.
1829 $skip_classes = array( 'skip-lazy', 'no-lazyload', 'lazyload-disabled', 'no-lazy', 'skip-lazyload' );
1830
1831 // Add skip classes to prevent lazy loading plugins from applying lazy loading.
1832 if ( isset( $attributes['class'] ) ) {
1833 $classes = array_filter( array_map( 'trim', explode( ' ', $attributes['class'] ) ) );
1834 $classes = array_unique( array_merge( $classes, $skip_classes ) );
1835 $attributes['class'] = implode( ' ', $classes );
1836 } else {
1837 // No class attribute exists, add one with skip classes.
1838 $attributes['class'] = implode( ' ', $skip_classes );
1839 }
1840
1841 // Add data-skip-lazy attribute as an additional safeguard.
1842 $attributes['data-skip-lazy'] = 'true';
1843
1844 return $attributes;
1845 }
1846 }
1847