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