PluginProbe
Mailgun for WordPress / trunk
Mailgun for WordPress vtrunk
2.2.3 2.2.2 2.2.1 trunk 1.9.9 2.0.0 2.0.1 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0
mailgun / includes / wp-mail-api.php

wp-mail-api.php in Mailgun for WordPress trunk, at includes/wp-mail-api.php

800 lines 29.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Mailgun-wordpress-plugin - Sending mail from Wordpress using Mailgun
4 * Copyright (C) 2016 Mailgun, et al.
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 *
20 * @package Mailgun
21 */
22
23 // Include MG filter functions
24 if ( ! include __DIR__ . '/mg-filter.php') {
25 ( new Mailgun() )->deactivate_and_die(__DIR__ . '/mg-filter.php');
26 }
27
28 /**
29 * g_api_last_error is a compound getter/setter for the last error that was
30 * encountered during a Mailgun API call.
31 *
32 * @param string|null $error OPTIONAL
33 * @return string|null Last error that occurred.
34 * @since 1.5.0
35 */
36 function mg_api_last_error( ?string $error = null ): ?string {
37 static $last_error;
38
39 if (null === $error) {
40 return $last_error;
41 }
42
43 do_action('mailgun_error_track', $error);
44 $tmp = $last_error;
45 $last_error = $error;
46
47 return $tmp;
48 }
49
50 /*
51 * Wordpress filter to mutate a `To` header to use recipient variables.
52 * Uses the `mg_use_recipient_vars_syntax` filter to apply the actual
53 * change. Otherwise, just a list of `To` addresses will be returned.
54 *
55 * @param string|array $to_addrs Array or comma-separated list of email addresses to mutate.
56 *
57 * @return array Array containing list of `To` addresses and recipient vars array
58 *
59 * @since 1.5.7
60 */
61 add_filter('mg_mutate_to_rcpt_vars', 'mg_mutate_to_rcpt_vars_cb');
62
63 /**
64 * @param string|array $to_addrs Array or comma-separated list of email addresses to mutate.
65 * @return array
66 * @throws JsonException
67 */
68 function mg_mutate_to_rcpt_vars_cb( $to_addrs ): array {
69 if (is_string($to_addrs)) {
70 $to_addrs = explode(',', $to_addrs);
71 }
72
73 if (has_filter('mg_use_recipient_vars_syntax')) {
74 $rcpt_vars = [];
75 $use_rcpt_vars = apply_filters('mg_use_recipient_vars_syntax', null);
76 if ($use_rcpt_vars) {
77
78 $idx = 0;
79 foreach ($to_addrs as $addr) {
80 $rcpt_vars[ $addr ] = array( 'batch_msg_id' => $idx );
81 ++$idx;
82 }
83
84 return array(
85 'to' => '%recipient%',
86 'rcpt_vars' => json_encode($rcpt_vars, JSON_THROW_ON_ERROR),
87 );
88 }
89 }
90
91 return array(
92 'to' => $to_addrs,
93 'rcpt_vars' => null,
94 );
95 }
96
97 /**
98 * wp_mail function to be loaded in to override the core wp_mail function
99 * from wp-includes/pluggable.php.
100 *
101 * Based off of the core wp_mail function, but with modifications required to
102 * send email using the Mailgun HTTP API
103 *
104 * @param string|array $to Array or comma-separated list of email addresses to send message.
105 * @param string $subject Email subject
106 * @param string $message Message contents
107 * @param string|array $headers Optional. Additional headers.
108 * @param string|array $attachments Optional. Files to attach.
109 *
110 * @return bool Whether the email contents were sent successfully.
111 *
112 * @global PHPMailer\PHPMailer\PHPMailer $phpmailer
113 */
114 if ( ! function_exists('wp_mail')) {
115
116 /**
117 * @param string $to
118 * @param string $subject
119 * @param mixed $message
120 * @param array $headers
121 * @param array $attachments
122 * @return bool
123 * @throws \PHPMailer\PHPMailer\Exception
124 */
125 function wp_mail( $to, $subject, $message, $headers = '', $attachments = []) {
126 $mailgun = get_option('mailgun');
127 $region = ( defined('MAILGUN_REGION') && MAILGUN_REGION ) ? MAILGUN_REGION : $mailgun['region'];
128 $apiKey = ( defined('MAILGUN_APIKEY') && MAILGUN_APIKEY ) ? MAILGUN_APIKEY : $mailgun['apiKey'];
129 $domain = ( defined('MAILGUN_DOMAIN') && MAILGUN_DOMAIN ) ? MAILGUN_DOMAIN : $mailgun['domain'];
130
131 if (empty($apiKey) || empty($domain)) {
132 return false;
133 }
134
135 // If a region is not set via defines or through the options page, default to US region.
136 if ( ! ( $region )) {
137 error_log('[Mailgun] No region configuration was found! Defaulting to US region.');
138 $region = 'us';
139 }
140
141 // Respect WordPress core filters
142 $atts = apply_filters( 'wp_mail', compact( 'to', 'subject', 'message', 'headers', 'attachments' ) );
143
144 if (isset($atts['to'])) {
145 $to = $atts['to'];
146 }
147
148 if ( ! is_array($to)) {
149 $to = explode(',', $to);
150 }
151
152 if (isset($atts['subject'])) {
153 $subject = $atts['subject'];
154 }
155
156 if (isset($atts['message'])) {
157 $message = $atts['message'];
158 }
159
160 if (isset($atts['headers'])) {
161 $headers = $atts['headers'];
162 }
163
164 if (isset($atts['attachments'])) {
165 $attachments = $atts['attachments'];
166 }
167
168 if ( ! is_array($attachments)) {
169 $attachments = explode("\n", str_replace("\r\n", "\n", $attachments));
170 }
171
172 $cc = [];
173 $bcc = [];
174
175 // Headers
176 if (empty($headers)) {
177 $headers = [];
178 } else {
179 if ( ! is_array($headers)) {
180 // Explode the headers out, so this function can take both
181 // string headers and an array of headers.
182 $tempheaders = explode("\n", str_replace("\r\n", "\n", $headers));
183 } else {
184 $tempheaders = $headers;
185 }
186 $headers = [];
187 $cc = [];
188 $bcc = [];
189
190 // If it's actually got contents
191 if ( ! empty($tempheaders)) {
192 // Iterate through the raw headers
193 foreach ( (array) $tempheaders as $header) {
194 if (strpos($header, ':') === false) {
195 if (false !== stripos($header, 'boundary=')) {
196 $parts = preg_split('/boundary=/i', trim($header));
197 $boundary = trim(str_replace(array( "'", '"' ), '', $parts[1]));
198 }
199 continue;
200 }
201 // Explode them out
202 [$name, $content] = explode(':', trim($header), 2);
203
204 // Cleanup crew
205 $name = trim($name);
206 $content = trim($content);
207
208 switch (strtolower($name)) {
209 // Mainly for legacy -- process a From: header if it's there
210 case 'from':
211 if (strpos($content, '<') !== false) {
212 $from_name = substr($content, 0, strpos($content, '<') - 1);
213 $from_name = str_replace('"', '', $from_name);
214 $from_name = trim($from_name);
215
216 $from_email = substr($content, strpos($content, '<') + 1);
217 $from_email = str_replace('>', '', $from_email);
218 $from_email = trim($from_email);
219 } else {
220 $from_email = trim($content);
221 }
222 break;
223 case 'content-type':
224 if (strpos($content, ';') !== false) {
225 [$type, $charset] = explode(';', $content);
226 $content_type = trim($type);
227 if (false !== stripos($charset, 'charset=')) {
228 $charset = trim(str_replace(array( 'charset=', '"' ), '', $charset));
229 } elseif (false !== stripos($charset, 'boundary=')) {
230 $boundary = trim(str_replace(array( 'BOUNDARY=', 'boundary=', '"' ), '', $charset));
231 $charset = '';
232 }
233 } else {
234 $content_type = trim($content);
235 }
236 break;
237 case 'cc':
238 $cc = array_merge( (array) $cc, explode(',', $content));
239 break;
240 case 'bcc':
241 $bcc = array_merge( (array) $bcc, explode(',', $content));
242 break;
243 default:
244 // Add it to our grand headers array
245 $headers[ trim($name) ] = trim($content);
246 break;
247 }
248 }
249 }
250 }
251
252 if ( ! isset($from_name)) {
253 $from_name = null;
254 }
255
256 if ( ! isset($from_email)) {
257 $from_email = null;
258 }
259
260 $from_name = mg_detect_from_name($from_name);
261 $from_email = mg_detect_from_address($from_email);
262 $fromString = "{$from_name} <{$from_email}>";
263
264 $body = array(
265 'from' => $fromString,
266 'h:Sender' => $from_email,
267 'to' => $to,
268 'subject' => $subject,
269 );
270
271 $rcpt_data = apply_filters('mg_mutate_to_rcpt_vars', $to);
272 if ( ! is_null($rcpt_data['rcpt_vars'])) {
273 $body['recipient-variables'] = $rcpt_data['rcpt_vars'];
274 }
275
276 $body['o:tag'] = [];
277 if (defined('MAILGUN_TRACK_CLICKS')) {
278 $trackClicks = MAILGUN_TRACK_CLICKS;
279 } else {
280 $trackClicks = ! empty($mailgun['track-clicks']) ? $mailgun['track-clicks'] : 'no';
281 }
282 if (defined('MAILGUN_TRACK_OPENS')) {
283 $trackOpens = MAILGUN_TRACK_OPENS;
284 } else {
285 $trackOpens = empty($mailgun['track-opens']) ? 'no' : 'yes';
286 }
287
288 if (isset($mailgun['suppress_clicks']) && $mailgun['suppress_clicks'] === 'yes') {
289 $passwordResetSubject = __('Password Reset Request', 'mailgun') ?: __( 'Password Reset Request', 'woocommerce' );
290 if ( ! empty($passwordResetSubject) && stripos($subject, $passwordResetSubject) !== false) {
291 $trackClicks = 'no';
292 }
293 }
294
295 $body['o:tracking-clicks'] = $trackClicks;
296 $body['o:tracking-opens'] = $trackOpens;
297
298 // this is the wordpress site tag
299 if (isset($mailgun['tag'])) {
300 $tags = explode(',', str_replace(' ', '', $mailgun['tag']));
301 $body['o:tag'] = $tags;
302 }
303
304 // campaign-id now refers to a list of tags which will be appended to the site tag
305 if ( ! empty($mailgun['campaign-id'])) {
306 $tags = explode(',', str_replace(' ', '', $mailgun['campaign-id']));
307 if (empty($body['o:tag'])) {
308 $body['o:tag'] = $tags;
309 } elseif (is_array($body['o:tag'])) {
310 $body['o:tag'] = array_merge($body['o:tag'], $tags);
311 } else {
312 $body['o:tag'] .= ',' . implode(',', $tags);
313 }
314 }
315
316 /**
317 * Filter tags.
318 *
319 * @param array $tags Mailgun tags.
320 * @param string $to To address.
321 * @param string $subject Subject line.
322 * @param string $message Message content.
323 * @param array $headers Headers array.
324 * @param array $attachments Attachments array.
325 * @param string $region Mailgun region.
326 * @param string $domain Mailgun domain.
327 *
328 * @return array Mailgun tags.
329 */
330 $body['o:tag'] = apply_filters('mailgun_tags', $body['o:tag'], $to, $subject, $message, $headers, $attachments, $region, $domain);
331
332 if ( ! empty($cc) && is_array($cc)) {
333 $body['cc'] = implode(', ', $cc);
334 }
335
336 if ( ! empty($bcc) && is_array($bcc)) {
337 $body['bcc'] = implode(', ', $bcc);
338 }
339
340 // If we are not given a Content-Type in the supplied headers,
341 // write the message body to a file and try to determine the mimetype
342 // using get_mime_content_type.
343 if ( ! isset($content_type)) {
344 $tmppath = tempnam(get_temp_dir(), 'mg');
345 $tmp = fopen($tmppath, 'w+');
346
347 fwrite($tmp, $message);
348 fclose($tmp);
349
350 $content_type = get_mime_content_type($tmppath, 'text/plain');
351
352 unlink($tmppath);
353 }
354
355 // Allow external content type filter to function normally
356 if (has_filter('wp_mail_content_type')) {
357 $content_type = apply_filters(
358 'wp_mail_content_type',
359 $content_type
360 );
361 }
362
363 if ('text/plain' === $content_type) {
364 $body['text'] = $message;
365 } elseif ('text/html' === $content_type) {
366 $body['html'] = $message;
367 } else {
368 $body['text'] = $message;
369 $body['html'] = $message;
370 }
371
372 // Some plugins, such as WooCommerce (@see WC_Email::handle_multipart()), to handle multipart/alternative with html
373 // and plaintext messages hooks into phpmailer_init action to override AltBody property directly in $phpmailer,
374 // so we should allow them to do this, and then get overridden plain text body from $phpmailer.
375 // Partly, this logic is taken from original wp_mail function.
376 if (false !== stripos($content_type, 'multipart')) {
377 global $phpmailer;
378
379 // (Re)create it, if it's gone missing.
380 if ( ! ( $phpmailer instanceof PHPMailer\PHPMailer\PHPMailer )) {
381 require_once ABSPATH . WPINC . '/PHPMailer/PHPMailer.php';
382 require_once ABSPATH . WPINC . '/PHPMailer/SMTP.php';
383 require_once ABSPATH . WPINC . '/PHPMailer/Exception.php';
384 $phpmailer = new PHPMailer\PHPMailer\PHPMailer(true);
385
386 $phpmailer::$validator = static function ( $email ) {
387 return (bool) is_email($email);
388 };
389 }
390
391 /**
392 * Fires after PHPMailer is initialized.
393 *
394 * @param PHPMailer $phpmailer The PHPMailer instance (passed by reference).
395 */
396 do_action_ref_array('phpmailer_init', array( &$phpmailer ));
397
398 $plainTextMessage = $phpmailer->AltBody;
399
400 if ($plainTextMessage) {
401 $body['text'] = $plainTextMessage;
402 }
403 }
404
405 // If we don't have a charset from the input headers
406 if ( ! isset($charset)) {
407 $charset = get_bloginfo('charset');
408 }
409
410 // Set the content-type and charset
411 $charset = apply_filters('wp_mail_charset', $charset);
412 if (isset($headers['Content-Type'])) {
413 if ( ! strstr($headers['Content-Type'], 'charset')) {
414 $headers['Content-Type'] = rtrim($headers['Content-Type'], '; ') . "; charset={$charset}";
415 }
416 }
417
418 $replyTo = ( defined('MAILGUN_REPLY_TO_ADDRESS') && MAILGUN_REPLY_TO_ADDRESS ) ? MAILGUN_REPLY_TO_ADDRESS : get_option('reply_to');
419 if ( ! empty($replyTo)) {
420 $headers['Reply-To'] = $replyTo;
421 }
422
423 // Set custom headers
424 if ( ! empty($headers)) {
425 foreach ( (array) $headers as $name => $content) {
426 $body[ "h:{$name}" ] = $content;
427 }
428 }
429
430 /*
431 * Deconstruct post array and create POST payload.
432 * This entire routine is because wp_remote_post does
433 * not support files directly.
434 */
435
436 $payload = '';
437
438 // First, generate a boundary for the multipart message.
439 $boundary = hash('sha256', uniqid('', true));
440
441 // Allow other plugins to apply body changes before creating the payload.
442 $body = apply_filters('mg_mutate_message_body', $body);
443 if (($body_payload = mg_build_payload_from_body($body, $boundary)) != null) {
444 $payload .= $body_payload;
445 }
446
447 // Allow other plugins to apply attachment changes before writing to the payload.
448 $attachments = apply_filters('mg_mutate_attachments', $attachments);
449 if (($attachment_payload = mg_build_attachments_payload($attachments, $boundary)) != null) {
450 $payload .= $attachment_payload;
451 }
452
453 $payload .= '--' . $boundary . '--';
454
455 $data = array(
456 'body' => $payload,
457 'headers' => array(
458 'Authorization' => 'Basic ' . base64_encode("api:{$apiKey}"),
459 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
460 ),
461 );
462
463 $endpoint = mg_api_get_region($region);
464 $endpoint = ( $endpoint ) ?: 'https://api.mailgun.net/v3/';
465 $url = $endpoint . "{$domain}/messages";
466
467 $isFallbackNeeded = false;
468 try {
469 $response = wp_remote_post($url, $data);
470 if (is_wp_error($response)) {
471 // Store WP error in last error.
472 mg_api_last_error($response->get_error_message());
473
474 $isFallbackNeeded = true;
475 }
476
477 $response_code = wp_remote_retrieve_response_code($response);
478 $response_body = json_decode(wp_remote_retrieve_body($response));
479
480 if ( (int) $response_code !== 200 || ! isset($response_body->message)) {
481 // Store response code and HTTP response message in last error.
482 $response_message = wp_remote_retrieve_response_message($response);
483 $errmsg = "$response_code - $response_message";
484 mg_api_last_error($errmsg);
485
486 $isFallbackNeeded = true;
487 }
488 if ($response_body->message !== 'Queued. Thank you.') {
489 mg_api_last_error($response_body->message);
490
491 $isFallbackNeeded = true;
492 }
493 } catch (Throwable $throwable) {
494 $isFallbackNeeded = true;
495 }
496
497 // Email Fallback
498 if (!$isFallbackNeeded) return true;
499
500 $isFallbackEnabled = get_option('email_fallback') ?: 'no';
501 if (!($isFallbackEnabled === 'yes')) return false;
502
503 global $phpmailer;
504
505 // (Re)create it, if it's gone missing.
506 if ( ! ( $phpmailer instanceof PHPMailer\PHPMailer\PHPMailer )) {
507 require_once ABSPATH . WPINC . '/PHPMailer/PHPMailer.php';
508 require_once ABSPATH . WPINC . '/PHPMailer/SMTP.php';
509 require_once ABSPATH . WPINC . '/PHPMailer/Exception.php';
510 $phpmailer = new PHPMailer\PHPMailer\PHPMailer(true);
511
512 $phpmailer::$validator = static function ( $email ) {
513 return (bool) is_email($email);
514 };
515 }
516
517 // Empty out the values that may be set.
518 $phpmailer->clearAllRecipients();
519 $phpmailer->clearAttachments();
520 $phpmailer->clearCustomHeaders();
521 $phpmailer->clearReplyTos();
522 $phpmailer->Body = '';
523 $phpmailer->AltBody = '';
524
525 // Set "From" name and email.
526
527 // If we don't have a name from the input headers.
528 if ( ! isset($from_name)) {
529 $from_name = 'WordPress';
530 }
531
532 /*
533 * If we don't have an email from the input headers, default to wordpress@$sitename
534 * Some hosts will block outgoing mail from this address if it doesn't exist,
535 * but there's no easy alternative. Defaulting to admin_email might appear to be
536 * another option, but some hosts may refuse to relay mail from an unknown domain.
537 * See https://core.trac.wordpress.org/ticket/5007.
538 */
539 if ( ! isset($from_email)) {
540 // Get the site domain and get rid of www.
541 $sitename = wp_parse_url(network_home_url(), PHP_URL_HOST);
542 $from_email = 'wordpress@';
543
544 if (null !== $sitename) {
545 if (str_starts_with($sitename, 'www.')) {
546 $sitename = substr($sitename, 4);
547 }
548
549 $from_email .= $sitename;
550 }
551 }
552
553 /**
554 * Filters the email address to send from.
555 *
556 * @param string $from_email Email address to send from.
557 * @since 2.2.0
558 */
559 $from_email = apply_filters('wp_mail_from', $from_email);
560
561 /**
562 * Filters the name to associate with the "from" email address.
563 *
564 * @param string $from_name Name associated with the "from" email address.
565 * @since 2.3.0
566 */
567 $from_name = apply_filters('wp_mail_from_name', $from_name);
568
569 try {
570 $phpmailer->setFrom($from_email, $from_name, false);
571 } catch (PHPMailer\PHPMailer\Exception $e) {
572 $mail_error_data = compact('to', 'subject', 'message', 'headers', 'attachments');
573 $mail_error_data['phpmailer_exception_code'] = $e->getCode();
574
575 /** This filter is documented in wp-includes/pluggable.php */
576 do_action('wp_mail_failed', new WP_Error('wp_mail_failed', $e->getMessage(), $mail_error_data));
577
578 return false;
579 }
580
581 // Set mail's subject and body.
582 $phpmailer->Subject = $subject;
583 $phpmailer->Body = $message;
584
585 // Set destination addresses, using appropriate methods for handling addresses.
586 $address_headers = compact('to', 'cc', 'bcc', 'replyTo');
587
588 foreach ($address_headers as $address_header => $addresses) {
589 if (empty($addresses)) {
590 continue;
591 }
592
593 foreach ( (array) $addresses as $address) {
594 try {
595 // Break $recipient into name and address parts if in the format "Foo <bar@baz.com>".
596 $recipient_name = '';
597
598 if (preg_match('/(.*)<(.+)>/', $address, $matches)) {
599 if (count($matches) === 3) {
600 $recipient_name = $matches[1];
601 $address = $matches[2];
602 }
603 }
604
605 switch ($address_header) {
606 case 'to':
607 $phpmailer->addAddress($address, $recipient_name);
608 break;
609 case 'cc':
610 $phpmailer->addCc($address, $recipient_name);
611 break;
612 case 'bcc':
613 $phpmailer->addBcc($address, $recipient_name);
614 break;
615 case 'reply_to':
616 $phpmailer->addReplyTo($address, $recipient_name);
617 break;
618 }
619 } catch (PHPMailer\PHPMailer\Exception $e) {
620 continue;
621 }
622 }
623 }
624
625 // Set to use PHP's mail().
626 $phpmailer->isMail();
627
628 // Set Content-Type and charset.
629
630 // If we don't have a Content-Type from the input headers.
631 if ( ! isset($content_type)) {
632 $content_type = 'text/plain';
633 }
634
635 /**
636 * Filters the wp_mail() content type.
637 *
638 * @param string $content_type Default wp_mail() content type.
639 * @since 2.3.0
640 */
641 $content_type = apply_filters('wp_mail_content_type', $content_type);
642
643 $phpmailer->ContentType = $content_type;
644
645 // Set whether it's plaintext, depending on $content_type.
646 if ('text/html' === $content_type) {
647 $phpmailer->isHTML(true);
648 }
649
650 // If we don't have a charset from the input headers.
651 if ( ! isset($charset)) {
652 $charset = get_bloginfo('charset');
653 }
654
655 /**
656 * Filters the default wp_mail() charset.
657 *
658 * @param string $charset Default email charset.
659 * @since 2.3.0
660 */
661 $phpmailer->CharSet = apply_filters('wp_mail_charset', $charset);
662
663 // Set custom headers.
664 if ( ! empty($headers)) {
665 foreach ( (array) $headers as $name => $content) {
666 // Only add custom headers not added automatically by PHPMailer.
667 if ( ! in_array($name, array( 'MIME-Version', 'X-Mailer' ), true)) {
668 try {
669 $phpmailer->addCustomHeader(sprintf('%1$s: %2$s', $name, $content));
670 } catch (PHPMailer\PHPMailer\Exception $e) {
671 continue;
672 }
673 }
674 }
675
676 if (false !== stripos($content_type, 'multipart') && ! empty($boundary)) {
677 $phpmailer->addCustomHeader(sprintf('Content-Type: %s; boundary="%s"', $content_type, $boundary));
678 }
679 }
680
681 if ( ! empty($attachments)) {
682 foreach ($attachments as $filename => $attachment) {
683 $filename = is_string($filename) ? $filename : '';
684
685 try {
686 $phpmailer->addAttachment($attachment, $filename);
687 } catch (PHPMailer\PHPMailer\Exception $e) {
688 continue;
689 }
690 }
691 }
692
693 /**
694 * Fires after PHPMailer is initialized.
695 *
696 * @param PHPMailer $phpmailer The PHPMailer instance (passed by reference).
697 * @since 2.2.0
698 */
699 do_action_ref_array('phpmailer_init', array( &$phpmailer ));
700
701 $mail_data = compact('to', 'subject', 'message', 'headers', 'attachments');
702
703 // Send!
704 try {
705 $send = $phpmailer->send();
706
707 /**
708 * Fires after PHPMailer has successfully sent an email.
709 * The firing of this action does not necessarily mean that the recipient(s) received the
710 * email successfully. It only means that the `send` method above was able to
711 * process the request without any errors.
712 *
713 * @param array $mail_data {
714 * An array containing the email recipient(s), subject, message, headers, and attachments.
715 * @type string[] $to Email addresses to send message.
716 * @type string $subject Email subject.
717 * @type string $message Message contents.
718 * @type string[] $headers Additional headers.
719 * @type string[] $attachments Paths to files to attach.
720 * }
721 * @since 5.9.0
722 */
723 do_action('wp_mail_succeeded', $mail_data);
724
725 return $send;
726 } catch (PHPMailer\PHPMailer\Exception $e) {
727 $mail_data['phpmailer_exception_code'] = $e->getCode();
728
729 /**
730 * Fires after a PHPMailer\PHPMailer\Exception is caught.
731 *
732 * @param WP_Error $error A WP_Error object with the PHPMailer\PHPMailer\Exception message, and an array
733 * containing the mail recipient, subject, message, headers, and attachments.
734 * @since 4.4.0
735 */
736 do_action('wp_mail_failed', new WP_Error('wp_mail_failed', $e->getMessage(), $mail_data));
737
738 return false;
739 }
740 }
741 }
742
743 /**
744 * @param array $body
745 * @param mixed $boundary
746 * @return string
747 */
748 function mg_build_payload_from_body( $body, $boundary ): string {
749 $payload = '';
750
751 // Iterate through pre-built params and build payload:
752 foreach ($body as $key => $value) {
753 if (is_array($value)) {
754 $parent_key = $key;
755 foreach ($value as $key => $value) {
756 $payload .= '--' . $boundary;
757 $payload .= "\r\n";
758 $payload .= 'Content-Disposition: form-data; name="' . $parent_key . "\"\r\n\r\n";
759 $payload .= $value;
760 $payload .= "\r\n";
761 }
762 } else {
763 $payload .= '--' . $boundary;
764 $payload .= "\r\n";
765 $payload .= 'Content-Disposition: form-data; name="' . $key . '"' . "\r\n\r\n";
766 $payload .= $value;
767 $payload .= "\r\n";
768 }
769 }
770
771 return $payload;
772 }
773
774 /**
775 * @param array $attachments
776 * @param mixed $boundary
777 * @return string|null
778 */
779 function mg_build_attachments_payload( $attachments, $boundary ): ?string {
780 $payload = '';
781
782 if (empty($attachments)) {
783 return null;
784 }
785 // If we have attachments, add them to the payload.
786 $i = 0;
787 foreach ($attachments as $attachment) {
788 if ( ! empty($attachment)) {
789 $payload .= '--' . $boundary;
790 $payload .= "\r\n";
791 $payload .= 'Content-Disposition: form-data; name="attachment[' . $i . ']"; filename="' . basename($attachment) . '"' . "\r\n\r\n";
792 $payload .= file_get_contents($attachment);
793 $payload .= "\r\n";
794 ++$i;
795 }
796 }
797
798 return $payload;
799 }
800