PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.0.2
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.0.2
11.1.1 11.1.0 11.0.5 11.0.4 11.0.3 11.0.2 11.0.1 11.0.0 10.11.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 trunk 10.0.0 10.0.1 10.1.0 10.1.1 10.1.2 10.1.3 10.1.4 10.2.0 10.2.1 10.2.2 All 62 releases
acymailing / back / Libraries / Mailer / Mailer.php

Mailer.php in AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress 11.0.2, at back/Libraries/Mailer/Mailer.php

5,271 lines 178.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Customized version of PHPMailer by Acyba
5 * PHPMailer - PHP email creation and transport class.
6 * PHP Version 5.5.
7 *
8 * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
9 *
10 * @author Marcus Bointon (Synchro/coolbru) <[email protected]>
11 * @author Jim Jagielski (jimjag) <[email protected]>
12 * @author Andy Prevost (codeworxtech) <[email protected]>
13 * @author Brent R. Matzelle (original founder)
14 * @copyright 2012 - 2020 Marcus Bointon
15 * @copyright 2010 - 2012 Jim Jagielski
16 * @copyright 2004 - 2009 Andy Prevost
17 * @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License
18 * @note This program is distributed in the hope that it will be useful - WITHOUT
19 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
20 * FITNESS FOR A PARTICULAR PURPOSE.
21 */
22
23 namespace AcyMailerPhp;
24
25 acym_cmsLoaded();
26
27 /**
28 * PHPMailer - PHP email creation and transport class.
29 *
30 * @author Marcus Bointon (Synchro/coolbru) <[email protected]>
31 * @author Jim Jagielski (jimjag) <[email protected]>
32 * @author Andy Prevost (codeworxtech) <[email protected]>
33 * @author Brent R. Matzelle (original founder)
34 */
35 class Mailer
36 {
37 const CHARSET_ASCII = 'us-ascii';
38 const CHARSET_ISO88591 = 'iso-8859-1';
39 const CHARSET_UTF8 = 'utf-8';
40
41 const CONTENT_TYPE_PLAINTEXT = 'text/plain';
42 const CONTENT_TYPE_TEXT_CALENDAR = 'text/calendar';
43 const CONTENT_TYPE_TEXT_HTML = 'text/html';
44 const CONTENT_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative';
45 const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed';
46 const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related';
47
48 const ENCODING_7BIT = '7bit';
49 const ENCODING_8BIT = '8bit';
50 const ENCODING_BASE64 = 'base64';
51 const ENCODING_BINARY = 'binary';
52 const ENCODING_QUOTED_PRINTABLE = 'quoted-printable';
53
54 const ENCRYPTION_STARTTLS = 'tls';
55 const ENCRYPTION_SMTPS = 'ssl';
56
57 const ICAL_METHOD_REQUEST = 'REQUEST';
58 const ICAL_METHOD_PUBLISH = 'PUBLISH';
59 const ICAL_METHOD_REPLY = 'REPLY';
60 const ICAL_METHOD_ADD = 'ADD';
61 const ICAL_METHOD_CANCEL = 'CANCEL';
62 const ICAL_METHOD_REFRESH = 'REFRESH';
63 const ICAL_METHOD_COUNTER = 'COUNTER';
64 const ICAL_METHOD_DECLINECOUNTER = 'DECLINECOUNTER';
65
66 /**
67 * Email priority.
68 * Options: null (default), 1 = High, 3 = Normal, 5 = low.
69 * When null, the header is not set at all.
70 *
71 * @var int|null
72 */
73 public $Priority;
74
75 /**
76 * The character set of the message.
77 *
78 * @var string
79 */
80 public $CharSet = self::CHARSET_ISO88591;
81
82 /**
83 * The MIME Content-type of the message.
84 *
85 * @var string
86 */
87 public $ContentType = self::CONTENT_TYPE_PLAINTEXT;
88
89 /**
90 * The message encoding.
91 * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable".
92 *
93 * @var string
94 */
95 public $Encoding = self::ENCODING_8BIT;
96
97 /**
98 * Holds the most recent mailer error message.
99 *
100 * @var string
101 */
102 public $ErrorInfo = '';
103
104 /**
105 * The From email address for the message.
106 *
107 * @var string
108 */
109 public $From = '';
110
111 /**
112 * The From name of the message.
113 *
114 * @var string
115 */
116 public $FromName = '';
117
118 /**
119 * The envelope sender of the message.
120 * This will usually be turned into a Return-Path header by the receiver,
121 * and is the address that bounces will be sent to.
122 * If not empty, will be passed via `-f` to sendmail or as the 'MAIL FROM' value over SMTP.
123 *
124 * @var string
125 */
126 public $Sender = '';
127
128 /**
129 * The Subject of the message.
130 *
131 * @var string
132 */
133 public $Subject = '';
134
135 /**
136 * An HTML or plain text message body.
137 * If HTML then call isHTML(true).
138 *
139 * @var string
140 */
141 public $Body = '';
142
143 /**
144 * The plain-text message body.
145 * This body can be read by mail clients that do not have HTML email
146 * capability such as mutt & Eudora.
147 * Clients that can read HTML will view the normal Body.
148 *
149 * @var string
150 */
151 public $AltBody = '';
152
153 /**
154 * An iCal message part body.
155 * Only supported in simple alt or alt_inline message types
156 * To generate iCal event structures, use classes like EasyPeasyICS or iCalcreator.
157 *
158 * @see https://kigkonsult.se/iCalcreator/
159 *
160 * @var string
161 */
162 public $Ical = '';
163
164 /**
165 * Value-array of "method" in Contenttype header "text/calendar"
166 *
167 * @var string[]
168 */
169 protected static $IcalMethods = [
170 self::ICAL_METHOD_REQUEST,
171 self::ICAL_METHOD_PUBLISH,
172 self::ICAL_METHOD_REPLY,
173 self::ICAL_METHOD_ADD,
174 self::ICAL_METHOD_CANCEL,
175 self::ICAL_METHOD_REFRESH,
176 self::ICAL_METHOD_COUNTER,
177 self::ICAL_METHOD_DECLINECOUNTER,
178 ];
179
180 /**
181 * The complete compiled MIME message body.
182 *
183 * @var string
184 */
185 protected $MIMEBody = '';
186
187 /**
188 * The complete compiled MIME message headers.
189 *
190 * @var string
191 */
192 protected $MIMEHeader = '';
193
194 /**
195 * Extra headers that createHeader() doesn't fold in.
196 *
197 * @var string
198 */
199 protected $mailHeader = '';
200
201 /**
202 * Word-wrap the message body to this number of chars.
203 * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance.
204 *
205 * @see static::STD_LINE_LENGTH
206 *
207 * @var int
208 */
209 public $WordWrap = 0;
210
211 /**
212 * Which method to use to send mail.
213 * Options: "mail", "sendmail", or "smtp".
214 *
215 * @var string
216 */
217 public $Mailer = 'mail';
218
219 /**
220 * The path to the sendmail program.
221 *
222 * @var string
223 */
224 public $Sendmail = '/usr/sbin/sendmail';
225
226 /**
227 * Whether mail() uses a fully sendmail-compatible MTA.
228 * One which supports sendmail's "-oi -f" options.
229 *
230 * @var bool
231 */
232 public $UseSendmailOptions = true;
233
234 /**
235 * The email address that a reading confirmation should be sent to, also known as read receipt.
236 *
237 * @var string
238 */
239 public $ConfirmReadingTo = '';
240
241 /**
242 * The hostname to use in the Message-ID header and as default HELO string.
243 * If empty, PHPMailer attempts to find one with, in order,
244 * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value
245 * 'localhost.localdomain'.
246 *
247 * @see PHPMailer::$Helo
248 *
249 * @var string
250 */
251 public $Hostname = '';
252
253 /**
254 * An ID to be used in the Message-ID header.
255 * If empty, a unique id will be generated.
256 * You can set your own, but it must be in the format "<id@domain>",
257 * as defined in RFC5322 section 3.6.4 or it will be ignored.
258 *
259 * @see https://tools.ietf.org/html/rfc5322#section-3.6.4
260 *
261 * @var string
262 */
263 public $MessageID = '';
264
265 /**
266 * The message Date to be used in the Date header.
267 * If empty, the current date will be added.
268 *
269 * @var string
270 */
271 public $MessageDate = '';
272
273 /**
274 * SMTP hosts.
275 * Either a single hostname or multiple semicolon-delimited hostnames.
276 * You can also specify a different port
277 * for each host by using this format: [hostname:port]
278 * (e.g. "smtp1.example.com:25;smtp2.example.com").
279 * You can also specify encryption type, for example:
280 * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465").
281 * Hosts will be tried in order.
282 *
283 * @var string
284 */
285 public $Host = 'localhost';
286
287 /**
288 * The default SMTP server port.
289 *
290 * @var int
291 */
292 public $Port = 25;
293
294 /**
295 * The SMTP HELO/EHLO name used for the SMTP connection.
296 * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find
297 * one with the same method described above for $Hostname.
298 *
299 * @see PHPMailer::$Hostname
300 *
301 * @var string
302 */
303 public $Helo = '';
304
305 /**
306 * What kind of encryption to use on the SMTP connection.
307 * Options: '', static::ENCRYPTION_STARTTLS, or static::ENCRYPTION_SMTPS.
308 *
309 * @var string
310 */
311 public $SMTPSecure = '';
312
313 /**
314 * Whether to enable TLS encryption automatically if a server supports it,
315 * even if `SMTPSecure` is not set to 'tls'.
316 * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid.
317 *
318 * @var bool
319 */
320 public $SMTPAutoTLS = true;
321
322 /**
323 * Whether to use SMTP authentication.
324 * Uses the Username and Password properties.
325 *
326 * @see PHPMailer::$Username
327 * @see PHPMailer::$Password
328 *
329 * @var bool
330 */
331 public $SMTPAuth = false;
332
333 /**
334 * Options array passed to stream_context_create when connecting via SMTP.
335 *
336 * @var array
337 */
338 public $SMTPOptions = [];
339
340 /**
341 * SMTP username.
342 *
343 * @var string
344 */
345 public $Username = '';
346
347 /**
348 * SMTP password.
349 *
350 * @var string
351 */
352 public $Password = '';
353
354 /**
355 * SMTP authentication type. Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2.
356 * If not specified, the first one from that list that the server supports will be selected.
357 *
358 * @var string
359 */
360 public $AuthType = '';
361
362 /**
363 * SMTP SMTPXClient command attibutes
364 *
365 * @var array
366 */
367 protected $SMTPXClient = [];
368
369 /**
370 * An implementation of the PHPMailer OAuthTokenProvider interface.
371 *
372 * @var OAuthTokenProvider
373 */
374 protected $oauth;
375
376 /**
377 * The SMTP server timeout in seconds.
378 * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
379 *
380 * @var int
381 */
382 public $Timeout = 300;
383
384 /**
385 * Comma separated list of DSN notifications
386 * 'NEVER' under no circumstances a DSN must be returned to the sender.
387 * If you use NEVER all other notifications will be ignored.
388 * 'SUCCESS' will notify you when your mail has arrived at its destination.
389 * 'FAILURE' will arrive if an error occurred during delivery.
390 * 'DELAY' will notify you if there is an unusual delay in delivery, but the actual
391 * delivery's outcome (success or failure) is not yet decided.
392 *
393 * @see https://tools.ietf.org/html/rfc3461 See section 4.1 for more information about NOTIFY
394 */
395 public $dsn = '';
396
397 /**
398 * SMTP class debug output mode.
399 * Debug output level.
400 * Options:
401 * @see SMTP::DEBUG_OFF: No output
402 * @see SMTP::DEBUG_CLIENT: Client messages
403 * @see SMTP::DEBUG_SERVER: Client and server messages
404 * @see SMTP::DEBUG_CONNECTION: As SERVER plus connection status
405 * @see SMTP::DEBUG_LOWLEVEL: Noisy, low-level data output, rarely needed
406 *
407 * @see SMTP::$do_debug
408 *
409 * @var int
410 */
411 public $SMTPDebug = 0;
412
413 /**
414 * How to handle debug output.
415 * Options:
416 * * `echo` Output plain-text as-is, appropriate for CLI
417 * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
418 * * `error_log` Output to error log as configured in php.ini
419 * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise.
420 * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
421 *
422 * ```php
423 * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
424 * ```
425 *
426 * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
427 * level output is used:
428 *
429 * ```php
430 * $mail->Debugoutput = new myPsr3Logger;
431 * ```
432 *
433 * @see SMTP::$Debugoutput
434 *
435 * @var string|callable|\Psr\Log\LoggerInterface
436 */
437 public $Debugoutput = 'echo';
438
439 /**
440 * Whether to keep the SMTP connection open after each message.
441 * If this is set to true then the connection will remain open after a send,
442 * and closing the connection will require an explicit call to smtpClose().
443 * It's a good idea to use this if you are sending multiple messages as it reduces overhead.
444 * See the mailing list example for how to use it.
445 *
446 * @var bool
447 */
448 public $SMTPKeepAlive = false;
449
450 /**
451 * Whether to split multiple to addresses into multiple messages
452 * or send them all in one message.
453 * Only supported in `mail` and `sendmail` transports, not in SMTP.
454 *
455 * @var bool
456 *
457 * @deprecated 6.0.0 PHPMailer isn't a mailing list manager!
458 */
459 public $SingleTo = false;
460
461 /**
462 * Storage for addresses when SingleTo is enabled.
463 *
464 * @var array
465 */
466 protected $SingleToArray = [];
467
468 /**
469 * Whether to generate VERP addresses on send.
470 * Only applicable when sending via SMTP.
471 *
472 * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path
473 * @see https://www.postfix.org/VERP_README.html Postfix VERP info
474 *
475 * @var bool
476 */
477 public $do_verp = false;
478
479 /**
480 * Whether to allow sending messages with an empty body.
481 *
482 * @var bool
483 */
484 public $AllowEmpty = false;
485
486 /**
487 * DKIM selector.
488 *
489 * @var string
490 */
491 public $DKIM_selector = '';
492
493 /**
494 * DKIM Identity.
495 * Usually the email address used as the source of the email.
496 *
497 * @var string
498 */
499 public $DKIM_identity = '';
500
501 /**
502 * DKIM passphrase.
503 * Used if your key is encrypted.
504 *
505 * @var string
506 */
507 public $DKIM_passphrase = '';
508
509 /**
510 * DKIM signing domain name.
511 *
512 * @example 'example.com'
513 *
514 * @var string
515 */
516 public $DKIM_domain = '';
517
518 /**
519 * DKIM Copy header field values for diagnostic use.
520 *
521 * @var bool
522 */
523 public $DKIM_copyHeaderFields = true;
524
525 /**
526 * DKIM Extra signing headers.
527 *
528 * @example ['List-Unsubscribe', 'List-Help']
529 *
530 * @var array
531 */
532 public $DKIM_extraHeaders = [];
533
534 /**
535 * DKIM private key file path.
536 *
537 * @var string
538 */
539 public $DKIM_private = '';
540
541 /**
542 * DKIM private key string.
543 *
544 * If set, takes precedence over `$DKIM_private`.
545 *
546 * @var string
547 */
548 public $DKIM_private_string = '';
549
550 /**
551 * Callback Action function name.
552 *
553 * The function that handles the result of the send email action.
554 * It is called out by send() for each email sent.
555 *
556 * Value can be any php callable: https://www.php.net/is_callable
557 *
558 * Parameters:
559 * bool $result result of the send action
560 * array $to email addresses of the recipients
561 * array $cc cc email addresses
562 * array $bcc bcc email addresses
563 * string $subject the subject
564 * string $body the email body
565 * string $from email address of sender
566 * string $extra extra information of possible use
567 * "smtp_transaction_id' => last smtp transaction id
568 *
569 * @var string
570 */
571 public $action_function = '';
572
573 /**
574 * What to put in the X-Mailer header.
575 * Options: An empty string for PHPMailer default, whitespace/null for none, or a string to use.
576 *
577 * @var string|null
578 */
579 public $XMailer = '';
580
581 /**
582 * Which validator to use by default when validating email addresses.
583 * May be a callable to inject your own validator, but there are several built-in validators.
584 * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option.
585 *
586 * @see PHPMailer::validateAddress()
587 *
588 * @var string|callable
589 */
590 public static $validator = 'php';
591
592 /**
593 * An instance of the SMTP sender class.
594 *
595 * @var SMTP
596 */
597 protected $smtp;
598
599 /**
600 * The array of 'to' names and addresses.
601 *
602 * @var array
603 */
604 protected $to = [];
605
606 /**
607 * The array of 'cc' names and addresses.
608 *
609 * @var array
610 */
611 protected $cc = [];
612
613 /**
614 * The array of 'bcc' names and addresses.
615 *
616 * @var array
617 */
618 protected $bcc = [];
619
620 /**
621 * The array of reply-to names and addresses.
622 *
623 * @var array
624 */
625 protected $ReplyTo = [];
626
627 /**
628 * An array of all kinds of addresses.
629 * Includes all of $to, $cc, $bcc.
630 *
631 * @see PHPMailer::$to
632 * @see PHPMailer::$cc
633 * @see PHPMailer::$bcc
634 *
635 * @var array
636 */
637 protected $all_recipients = [];
638
639 /**
640 * An array of names and addresses queued for validation.
641 * In send(), valid and non duplicate entries are moved to $all_recipients
642 * and one of $to, $cc, or $bcc.
643 * This array is used only for addresses with IDN.
644 *
645 * @see PHPMailer::$to
646 * @see PHPMailer::$cc
647 * @see PHPMailer::$bcc
648 * @see PHPMailer::$all_recipients
649 *
650 * @var array
651 */
652 protected $RecipientsQueue = [];
653
654 /**
655 * An array of reply-to names and addresses queued for validation.
656 * In send(), valid and non duplicate entries are moved to $ReplyTo.
657 * This array is used only for addresses with IDN.
658 *
659 * @see PHPMailer::$ReplyTo
660 *
661 * @var array
662 */
663 protected $ReplyToQueue = [];
664
665 /**
666 * The array of attachments.
667 *
668 * @var array
669 */
670 protected $attachment = [];
671
672 /**
673 * The array of custom headers.
674 *
675 * @var array
676 */
677 protected $CustomHeader = [];
678
679 /**
680 * The most recent Message-ID (including angular brackets).
681 *
682 * @var string
683 */
684 protected $lastMessageID = '';
685
686 /**
687 * The message's MIME type.
688 *
689 * @var string
690 */
691 protected $message_type = '';
692
693 /**
694 * The array of MIME boundary strings.
695 *
696 * @var array
697 */
698 protected $boundary = [];
699
700 /**
701 * The array of available text strings for the current language.
702 *
703 * @var array
704 */
705 protected $language = [];
706
707 /**
708 * The number of errors encountered.
709 *
710 * @var int
711 */
712 protected $error_count = 0;
713
714 /**
715 * The S/MIME certificate file path.
716 *
717 * @var string
718 */
719 protected $sign_cert_file = '';
720
721 /**
722 * The S/MIME key file path.
723 *
724 * @var string
725 */
726 protected $sign_key_file = '';
727
728 /**
729 * The optional S/MIME extra certificates ("CA Chain") file path.
730 *
731 * @var string
732 */
733 protected $sign_extracerts_file = '';
734
735 /**
736 * The S/MIME password for the key.
737 * Used only if the key is encrypted.
738 *
739 * @var string
740 */
741 protected $sign_key_pass = '';
742
743 /**
744 * Whether to throw exceptions for errors.
745 *
746 * @var bool
747 */
748 protected $exceptions = false;
749
750 /**
751 * Unique ID used for message ID and boundaries.
752 *
753 * @var string
754 */
755 protected $uniqueid = '';
756
757 /**
758 * The PHPMailer Version number.
759 *
760 * @var string
761 */
762 const VERSION = '6.9.1';
763
764 /**
765 * Error severity: message only, continue processing.
766 *
767 * @var int
768 */
769 const STOP_MESSAGE = 0;
770
771 /**
772 * Error severity: message, likely ok to continue processing.
773 *
774 * @var int
775 */
776 const STOP_CONTINUE = 1;
777
778 /**
779 * Error severity: message, plus full stop, critical error reached.
780 *
781 * @var int
782 */
783 const STOP_CRITICAL = 2;
784
785 /**
786 * The SMTP standard CRLF line break.
787 * If you want to change line break format, change static::$LE, not this.
788 */
789 const CRLF = "\r\n";
790
791 /**
792 * "Folding White Space" a white space string used for line folding.
793 */
794 const FWS = ' ';
795
796 /**
797 * SMTP RFC standard line ending; Carriage Return, Line Feed.
798 *
799 * @var string
800 */
801 protected static $LE = self::CRLF;
802
803 /**
804 * The maximum line length supported by mail().
805 *
806 * Background: mail() will sometimes corrupt messages
807 * with headers longer than 65 chars, see #818.
808 *
809 * @var int
810 */
811 const MAIL_MAX_LINE_LENGTH = 63;
812
813 /**
814 * The maximum line length allowed by RFC 2822 section 2.1.1.
815 *
816 * @var int
817 */
818 const MAX_LINE_LENGTH = 998;
819
820 /**
821 * The lower maximum line length allowed by RFC 2822 section 2.1.1.
822 * This length does NOT include the line break
823 * 76 means that lines will be 77 or 78 chars depending on whether
824 * the line break format is LF or CRLF; both are valid.
825 *
826 * @var int
827 */
828 const STD_LINE_LENGTH = 76;
829
830 /**
831 * Constructor.
832 *
833 * @param bool $exceptions Should we throw external exceptions?
834 */
835 public function __construct($exceptions = null)
836 {
837 if (null !== $exceptions) {
838 $this->exceptions = (bool)$exceptions;
839 }
840 //Pick an appropriate debug output format automatically
841 $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html');
842 }
843
844 /**
845 * Destructor.
846 */
847 public function __destruct()
848 {
849 //Close any open SMTP connection nicely
850 $this->smtpClose();
851 }
852
853 /**
854 * Call mail() in a safe_mode-aware fashion.
855 * Also, unless sendmail_path points to sendmail (or something that
856 * claims to be sendmail), don't pass params (not a perfect fix,
857 * but it will do).
858 *
859 * @param string $to To
860 * @param string $subject Subject
861 * @param string $body Message Body
862 * @param string $header Additional Header(s)
863 * @param string|null $params Params
864 *
865 * @return bool
866 */
867 private function mailPassthru($to, $subject, $body, $header, $params)
868 {
869 //Check overloading of mail function to avoid double-encoding
870 if ((int)ini_get('mbstring.func_overload') & 1) {
871 $subject = $this->secureHeader($subject);
872 } else {
873 $subject = $this->encodeHeader($this->secureHeader($subject));
874 }
875 //Calling mail() with null params breaks
876 $this->edebug('Sending with mail()');
877 $this->edebug('Sendmail path: '.ini_get('sendmail_path'));
878 $this->edebug("Envelope sender: {$this->Sender}");
879 $this->edebug("To: {$to}");
880 $this->edebug("Subject: {$subject}");
881 $this->edebug("Headers: {$header}");
882 if (!$this->UseSendmailOptions || null === $params) {
883 $result = @mail($to, $subject, $body, $header);
884 } else {
885 $this->edebug("Additional params: {$params}");
886 $result = @mail($to, $subject, $body, $header, $params);
887 }
888 $this->edebug('Result: '.($result ? 'true' : 'false'));
889
890 return $result;
891 }
892
893 /**
894 * Output debugging info via a user-defined method.
895 * Only generates output if debug output is enabled.
896 *
897 * @param string $str
898 *
899 * @see PHPMailer::$SMTPDebug
900 *
901 * @see PHPMailer::$Debugoutput
902 */
903 protected function edebug($str)
904 {
905 if ($this->SMTPDebug <= 0) {
906 return;
907 }
908 //Is this a PSR-3 logger?
909 if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
910 $this->Debugoutput->debug(rtrim($str, "\r\n"));
911
912 return;
913 }
914 //Avoid clash with built-in function names
915 if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
916 call_user_func($this->Debugoutput, $str, $this->SMTPDebug);
917
918 return;
919 }
920 switch ($this->Debugoutput) {
921 case 'error_log':
922 //Don't output, just log
923 /** @noinspection ForgottenDebugOutputInspection */
924 error_log($str);
925 break;
926 case 'html':
927 //Cleans up output a bit for a better looking, HTML-safe output
928 echo htmlentities(
929 preg_replace('/[\r\n]+/', '', $str),
930 ENT_QUOTES,
931 'UTF-8'
932 ), "<br>\n";
933 break;
934 case 'echo':
935 default:
936 //Normalize line breaks
937 $str = preg_replace('/\r\n|\r/m', "\n", $str);
938 echo gmdate('Y-m-d H:i:s'),
939 "\t",
940 //Trim trailing space
941 trim(
942 //Indent for readability, except for trailing break
943 str_replace(
944 "\n",
945 "\n \t ",
946 trim($str)
947 )
948 ),
949 "\n";
950 }
951 }
952
953 /**
954 * Sets message type to HTML or plain.
955 *
956 * @param bool $isHtml True for HTML mode
957 */
958 public function isHTML($isHtml = true)
959 {
960 if ($isHtml) {
961 $this->ContentType = static::CONTENT_TYPE_TEXT_HTML;
962 } else {
963 $this->ContentType = static::CONTENT_TYPE_PLAINTEXT;
964 }
965 }
966
967 /**
968 * Send messages using SMTP.
969 */
970 public function isSMTP()
971 {
972 $this->Mailer = 'smtp';
973 }
974
975 /**
976 * Send messages using PHP's mail() function.
977 */
978 public function isMail()
979 {
980 $this->Mailer = 'mail';
981 }
982
983 /**
984 * Send messages using $Sendmail.
985 */
986 public function isSendmail()
987 {
988 $ini_sendmail_path = ini_get('sendmail_path');
989
990 if (false === stripos($ini_sendmail_path, 'sendmail')) {
991 $this->Sendmail = '/usr/sbin/sendmail';
992 } else {
993 $this->Sendmail = $ini_sendmail_path;
994 }
995 $this->Mailer = 'sendmail';
996 }
997
998 /**
999 * Send messages using qmail.
1000 */
1001 public function isQmail()
1002 {
1003 $ini_sendmail_path = ini_get('sendmail_path');
1004
1005 if (false === stripos($ini_sendmail_path, 'qmail')) {
1006 $this->Sendmail = '/var/qmail/bin/qmail-inject';
1007 } else {
1008 $this->Sendmail = $ini_sendmail_path;
1009 }
1010 $this->Mailer = 'qmail';
1011 }
1012
1013 /**
1014 * Add a "To" address.
1015 *
1016 * @param string $address The email address to send to
1017 * @param string $name
1018 *
1019 * @return bool true on success, false if address already used or invalid in some way
1020 * @throws Exception
1021 *
1022 */
1023 public function addAddress($address, $name = '')
1024 {
1025 return $this->addOrEnqueueAnAddress('to', $address, $name);
1026 }
1027
1028 /**
1029 * Add a "CC" address.
1030 *
1031 * @param string $address The email address to send to
1032 * @param string $name
1033 *
1034 * @return bool true on success, false if address already used or invalid in some way
1035 * @throws Exception
1036 *
1037 */
1038 public function addCC($address, $name = '')
1039 {
1040 return $this->addOrEnqueueAnAddress('cc', $address, $name);
1041 }
1042
1043 /**
1044 * Add a "BCC" address.
1045 *
1046 * @param string $address The email address to send to
1047 * @param string $name
1048 *
1049 * @return bool true on success, false if address already used or invalid in some way
1050 * @throws Exception
1051 *
1052 */
1053 public function addBCC($address, $name = '')
1054 {
1055 return $this->addOrEnqueueAnAddress('bcc', $address, $name);
1056 }
1057
1058 /**
1059 * Add a "Reply-To" address.
1060 *
1061 * @param string $address The email address to reply to
1062 * @param string $name
1063 *
1064 * @return bool true on success, false if address already used or invalid in some way
1065 * @throws Exception
1066 *
1067 */
1068 public function addReplyTo($address, $name = '')
1069 {
1070 return $this->addOrEnqueueAnAddress('Reply-To', $address, $name);
1071 }
1072
1073 /**
1074 * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer
1075 * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still
1076 * be modified after calling this function), addition of such addresses is delayed until send().
1077 * Addresses that have been added already return false, but do not throw exceptions.
1078 *
1079 * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo'
1080 * @param string $address The email address
1081 * @param string $name An optional username associated with the address
1082 *
1083 * @return bool true on success, false if address already used or invalid in some way
1084 * @throws Exception
1085 *
1086 */
1087 protected function addOrEnqueueAnAddress($kind, $address, $name)
1088 {
1089 $pos = false;
1090 if ($address !== null) {
1091 $address = trim($address);
1092 $pos = strrpos($address, '@');
1093 }
1094 if (false === $pos) {
1095 //At-sign is missing.
1096 $error_message = sprintf(
1097 '%s (%s): %s',
1098 $this->lang('invalid_address'),
1099 $kind,
1100 $address
1101 );
1102 $this->setError($error_message);
1103 $this->edebug($error_message);
1104 if ($this->exceptions) {
1105 throw new Exception($error_message);
1106 }
1107
1108 return false;
1109 }
1110 if ($name !== null && is_string($name)) {
1111 $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
1112 } else {
1113 $name = '';
1114 }
1115 $params = [$kind, $address, $name];
1116 //Enqueue addresses with IDN until we know the PHPMailer::$CharSet.
1117 //Domain is assumed to be whatever is after the last @ symbol in the address
1118 if (static::idnSupported() && $this->has8bitChars(substr($address, ++$pos))) {
1119 if ('Reply-To' !== $kind) {
1120 if (!array_key_exists($address, $this->RecipientsQueue)) {
1121 $this->RecipientsQueue[$address] = $params;
1122
1123 return true;
1124 }
1125 } elseif (!array_key_exists($address, $this->ReplyToQueue)) {
1126 $this->ReplyToQueue[$address] = $params;
1127
1128 return true;
1129 }
1130
1131 return false;
1132 }
1133
1134 //Immediately add standard addresses without IDN.
1135 return call_user_func_array([$this, 'addAnAddress'], $params);
1136 }
1137
1138 /**
1139 * Set the boundaries to use for delimiting MIME parts.
1140 * If you override this, ensure you set all 3 boundaries to unique values.
1141 * The default boundaries include a "=_" sequence which cannot occur in quoted-printable bodies,
1142 * as suggested by https://www.rfc-editor.org/rfc/rfc2045#section-6.7
1143 *
1144 * @return void
1145 */
1146 public function setBoundaries()
1147 {
1148 $this->uniqueid = $this->generateId();
1149 $this->boundary[1] = 'b1=_'.$this->uniqueid;
1150 $this->boundary[2] = 'b2=_'.$this->uniqueid;
1151 $this->boundary[3] = 'b3=_'.$this->uniqueid;
1152 }
1153
1154 /**
1155 * Add an address to one of the recipient arrays or to the ReplyTo array.
1156 * Addresses that have been added already return false, but do not throw exceptions.
1157 *
1158 * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo'
1159 * @param string $address The email address to send, resp. to reply to
1160 * @param string $name
1161 *
1162 * @return bool true on success, false if address already used or invalid in some way
1163 * @throws Exception
1164 *
1165 */
1166 protected function addAnAddress($kind, $address, $name = '')
1167 {
1168 if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) {
1169 $error_message = sprintf(
1170 '%s: %s',
1171 $this->lang('Invalid recipient kind'),
1172 $kind
1173 );
1174 $this->setError($error_message);
1175 $this->edebug($error_message);
1176 if ($this->exceptions) {
1177 throw new Exception($error_message);
1178 }
1179
1180 return false;
1181 }
1182 if (!static::validateAddress($address)) {
1183 $error_message = sprintf(
1184 '%s (%s): %s',
1185 $this->lang('invalid_address'),
1186 $kind,
1187 $address
1188 );
1189 $this->setError($error_message);
1190 $this->edebug($error_message);
1191 if ($this->exceptions) {
1192 throw new Exception($error_message);
1193 }
1194
1195 return false;
1196 }
1197 if ('Reply-To' !== $kind) {
1198 if (!array_key_exists(strtolower($address), $this->all_recipients)) {
1199 $this->{$kind}[] = [$address, $name];
1200 $this->all_recipients[strtolower($address)] = true;
1201
1202 return true;
1203 }
1204 } elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) {
1205 $this->ReplyTo[strtolower($address)] = [$address, $name];
1206
1207 return true;
1208 }
1209
1210 return false;
1211 }
1212
1213 /**
1214 * Parse and validate a string containing one or more RFC822-style comma-separated email addresses
1215 * of the form "display name <address>" into an array of name/address pairs.
1216 * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available.
1217 * Note that quotes in the name part are removed.
1218 *
1219 * @see https://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation
1220 *
1221 * @param string $addrstr The address list string
1222 * @param bool $useimap Whether to use the IMAP extension to parse the list
1223 * @param string $charset The charset to use when decoding the address list string.
1224 *
1225 * @return array
1226 */
1227 public static function parseAddresses($addrstr, $useimap = true, $charset = self::CHARSET_ISO88591)
1228 {
1229 $addresses = [];
1230 if ($useimap && function_exists('imap_rfc822_parse_adrlist')) {
1231 //Use this built-in parser if it's available
1232 $list = imap_rfc822_parse_adrlist($addrstr, '');
1233 // Clear any potential IMAP errors to get rid of notices being thrown at end of script.
1234 imap_errors();
1235 foreach ($list as $address) {
1236 if (
1237 '.SYNTAX-ERROR.' !== $address->host &&
1238 static::validateAddress($address->mailbox.'@'.$address->host)
1239 ) {
1240 //Decode the name part if it's present and encoded
1241 if (
1242 property_exists($address, 'personal') &&
1243 //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled
1244 defined('MB_CASE_UPPER') &&
1245 preg_match('/^=\?.*\?=$/s', $address->personal)
1246 ) {
1247 $origCharset = mb_internal_encoding();
1248 mb_internal_encoding($charset);
1249 //Undo any RFC2047-encoded spaces-as-underscores
1250 $address->personal = str_replace('_', '=20', $address->personal);
1251 //Decode the name
1252 $address->personal = mb_decode_mimeheader($address->personal);
1253 mb_internal_encoding($origCharset);
1254 }
1255
1256 $addresses[] = [
1257 'name' => (property_exists($address, 'personal') ? $address->personal : ''),
1258 'address' => $address->mailbox.'@'.$address->host,
1259 ];
1260 }
1261 }
1262 } else {
1263 //Use this simpler parser
1264 $list = explode(',', $addrstr);
1265 foreach ($list as $address) {
1266 $address = trim($address);
1267 //Is there a separate name part?
1268 if (strpos($address, '<') === false) {
1269 //No separate name, just use the whole thing
1270 if (static::validateAddress($address)) {
1271 $addresses[] = [
1272 'name' => '',
1273 'address' => $address,
1274 ];
1275 }
1276 } else {
1277 [$name, $email] = explode('<', $address);
1278 $email = trim(str_replace('>', '', $email));
1279 $name = trim($name);
1280 if (static::validateAddress($email)) {
1281 //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled
1282 //If this name is encoded, decode it
1283 if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) {
1284 $origCharset = mb_internal_encoding();
1285 mb_internal_encoding($charset);
1286 //Undo any RFC2047-encoded spaces-as-underscores
1287 $name = str_replace('_', '=20', $name);
1288 //Decode the name
1289 $name = mb_decode_mimeheader($name);
1290 mb_internal_encoding($origCharset);
1291 }
1292 $addresses[] = [
1293 //Remove any surrounding quotes and spaces from the name
1294 'name' => trim($name, '\'" '),
1295 'address' => $email,
1296 ];
1297 }
1298 }
1299 }
1300 }
1301
1302 return $addresses;
1303 }
1304
1305 /**
1306 * Set the From and FromName properties.
1307 *
1308 * @param string $address
1309 * @param string $name
1310 * @param bool $auto Whether to also set the Sender address, defaults to true
1311 *
1312 * @return bool
1313 * @throws Exception
1314 *
1315 */
1316 public function setFrom($address, $name = '', $auto = true)
1317 {
1318 $address = trim((string)$address);
1319 $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
1320 //Don't validate now addresses with IDN. Will be done in send().
1321 $pos = strrpos($address, '@');
1322 if (
1323 (false === $pos)
1324 || ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnSupported())
1325 && !static::validateAddress($address))
1326 ) {
1327 $error_message = sprintf(
1328 '%s (From): %s',
1329 $this->lang('invalid_address'),
1330 $address
1331 );
1332 $this->setError($error_message);
1333 $this->edebug($error_message);
1334 if ($this->exceptions) {
1335 throw new Exception($error_message);
1336 }
1337
1338 return false;
1339 }
1340 $this->From = $address;
1341 $this->FromName = $name;
1342 if ($auto && empty($this->Sender)) {
1343 $this->Sender = $address;
1344 }
1345
1346 return true;
1347 }
1348
1349 /**
1350 * Return the Message-ID header of the last email.
1351 * Technically this is the value from the last time the headers were created,
1352 * but it's also the message ID of the last sent message except in
1353 * pathological cases.
1354 *
1355 * @return string
1356 */
1357 public function getLastMessageID()
1358 {
1359 return $this->lastMessageID;
1360 }
1361
1362 /**
1363 * Check that a string looks like an email address.
1364 * Validation patterns supported:
1365 * * `auto` Pick best pattern automatically;
1366 * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0;
1367 * * `pcre` Use old PCRE implementation;
1368 * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL;
1369 * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements.
1370 * * `noregex` Don't use a regex: super fast, really dumb.
1371 * Alternatively you may pass in a callable to inject your own validator, for example:
1372 *
1373 * ```php
1374 * PHPMailer::validateAddress('user@example.com', function($address) {
1375 * return (strpos($address, '@') !== false);
1376 * });
1377 * ```
1378 *
1379 * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator.
1380 *
1381 * @param string $address The email address to check
1382 * @param string|callable $patternselect Which pattern to use
1383 *
1384 * @return bool
1385 */
1386 public static function validateAddress($address, $patternselect = null)
1387 {
1388 if (null === $patternselect) {
1389 $patternselect = static::$validator;
1390 }
1391 //Don't allow strings as callables, see SECURITY.md and CVE-2021-3603
1392 if (is_callable($patternselect) && !is_string($patternselect)) {
1393 return call_user_func($patternselect, $address);
1394 }
1395 //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321
1396 if (strpos($address, "\n") !== false || strpos($address, "\r") !== false) {
1397 return false;
1398 }
1399 switch ($patternselect) {
1400 case 'pcre': //Kept for BC
1401 case 'pcre8':
1402 /*
1403 * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL
1404 * is based.
1405 * In addition to the addresses allowed by filter_var, also permits:
1406 * * dotless domains: `a@b`
1407 * * comments: `1234 @ local(blah) .machine .example`
1408 * * quoted elements: `'"test blah"@example.org'`
1409 * * numeric TLDs: `[email protected]`
1410 * * unbracketed IPv4 literals: `[email protected]`
1411 * * IPv6 literals: 'first.last@[IPv6:a1::]'
1412 * Not all of these will necessarily work for sending!
1413 *
1414 * @copyright 2009-2010 Michael Rushton
1415 * Feel free to use and redistribute this code. But please keep this copyright notice.
1416 */
1417 return (bool)preg_match(
1418 '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)'.
1419 '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)'.
1420 '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)'.
1421 '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*'.
1422 '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)'.
1423 '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}'.
1424 '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:'.
1425 '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}'.
1426 '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD',
1427 $address
1428 );
1429 case 'html5':
1430 /*
1431 * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.
1432 *
1433 * @see https://html.spec.whatwg.org/#e-mail-state-(type=email)
1434 */
1435 return (bool)preg_match(
1436 '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}'.
1437 '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',
1438 $address
1439 );
1440 case 'php':
1441 default:
1442 return filter_var($address, FILTER_VALIDATE_EMAIL) !== false;
1443 }
1444 }
1445
1446 /**
1447 * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the
1448 * `intl` and `mbstring` PHP extensions.
1449 *
1450 * @return bool `true` if required functions for IDN support are present
1451 */
1452 public static function idnSupported()
1453 {
1454 return function_exists('idn_to_ascii') && function_exists('mb_convert_encoding');
1455 }
1456
1457 /**
1458 * Converts IDN in given email address to its ASCII form, also known as punycode, if possible.
1459 * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet.
1460 * This function silently returns unmodified address if:
1461 * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form)
1462 * - Conversion to punycode is impossible (e.g. required PHP functions are not available)
1463 * or fails for any reason (e.g. domain contains characters not allowed in an IDN).
1464 *
1465 * @param string $address The email address to convert
1466 *
1467 * @return string The encoded address in ASCII form
1468 * @see PHPMailer::$CharSet
1469 *
1470 */
1471 public function punyencodeAddress($address)
1472 {
1473 //Verify we have required functions, CharSet, and at-sign.
1474 $pos = strrpos($address, '@');
1475 if (
1476 !empty($this->CharSet) &&
1477 false !== $pos &&
1478 static::idnSupported()
1479 ) {
1480 $domain = substr($address, ++$pos);
1481 //Verify CharSet string is a valid one, and domain properly encoded in this CharSet.
1482 if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) {
1483 //Convert the domain from whatever charset it's in to UTF-8
1484 $domain = mb_convert_encoding($domain, self::CHARSET_UTF8, $this->CharSet);
1485 //Ignore IDE complaints about this line - method signature changed in PHP 5.4
1486 $errorcode = 0;
1487 if (defined('INTL_IDNA_VARIANT_UTS46')) {
1488 //Use the current punycode standard (appeared in PHP 7.2)
1489 $punycode = idn_to_ascii(
1490 $domain,
1491 \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI |
1492 \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII,
1493 \INTL_IDNA_VARIANT_UTS46
1494 );
1495 } elseif (defined('INTL_IDNA_VARIANT_2003')) {
1496 //Fall back to this old, deprecated/removed encoding
1497 $punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VARIANT_2003);
1498 } else {
1499 //Fall back to a default we don't know about
1500 $punycode = idn_to_ascii($domain, $errorcode);
1501 }
1502 if (false !== $punycode) {
1503 return substr($address, 0, $pos).$punycode;
1504 }
1505 }
1506 }
1507
1508 return $address;
1509 }
1510
1511 /**
1512 * Create a message and send it.
1513 * Uses the sending method specified by $Mailer.
1514 *
1515 * @return bool false on error - See the ErrorInfo property for details of the error
1516 * @throws Exception
1517 *
1518 */
1519 public function send()
1520 {
1521 try {
1522 if (!$this->preSend()) {
1523 return false;
1524 }
1525
1526 return $this->postSend();
1527 } catch (Exception $exc) {
1528 $this->mailHeader = '';
1529 $this->setError($exc->getMessage());
1530 if ($this->exceptions) {
1531 throw $exc;
1532 }
1533
1534 return false;
1535 }
1536 }
1537
1538 /**
1539 * Prepare a message for sending.
1540 *
1541 * @return bool
1542 * @throws Exception
1543 *
1544 */
1545 public function preSend()
1546 {
1547 if (
1548 'smtp' === $this->Mailer
1549 || ('mail' === $this->Mailer && (\PHP_VERSION_ID >= 80000 || stripos(PHP_OS, 'WIN') === 0))
1550 ) {
1551 //SMTP mandates RFC-compliant line endings
1552 //and it's also used with mail() on Windows
1553 static::setLE(self::CRLF);
1554 } else {
1555 //Maintain backward compatibility with legacy Linux command line mailers
1556 static::setLE(PHP_EOL);
1557 }
1558 //Check for buggy PHP versions that add a header with an incorrect line break
1559 if (
1560 'mail' === $this->Mailer
1561 && ((\PHP_VERSION_ID >= 70000 && \PHP_VERSION_ID < 70017)
1562 || (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70103))
1563 && ini_get('mail.add_x_header') === '1'
1564 && stripos(PHP_OS, 'WIN') === 0
1565 ) {
1566 trigger_error($this->lang('buggy_php'), E_USER_WARNING);
1567 }
1568
1569 try {
1570 $this->error_count = 0; //Reset errors
1571 $this->mailHeader = '';
1572
1573 //Dequeue recipient and Reply-To addresses with IDN
1574 foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) {
1575 $params[1] = $this->punyencodeAddress($params[1]);
1576 call_user_func_array([$this, 'addAnAddress'], $params);
1577 }
1578 if (count($this->to) + count($this->cc) + count($this->bcc) < 1) {
1579 throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL);
1580 }
1581
1582 //Validate From, Sender, and ConfirmReadingTo addresses
1583 foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) {
1584 if ($this->{$address_kind} === null) {
1585 $this->{$address_kind} = '';
1586 continue;
1587 }
1588 $this->{$address_kind} = trim($this->{$address_kind});
1589 if (empty($this->{$address_kind})) {
1590 continue;
1591 }
1592 $this->{$address_kind} = $this->punyencodeAddress($this->{$address_kind});
1593 if (!static::validateAddress($this->{$address_kind})) {
1594 $error_message = sprintf(
1595 '%s (%s): %s',
1596 $this->lang('invalid_address'),
1597 $address_kind,
1598 $this->{$address_kind}
1599 );
1600 $this->setError($error_message);
1601 $this->edebug($error_message);
1602 if ($this->exceptions) {
1603 throw new Exception($error_message);
1604 }
1605
1606 return false;
1607 }
1608 }
1609
1610 //Set whether the message is multipart/alternative
1611 if ($this->alternativeExists()) {
1612 $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE;
1613 }
1614
1615 $this->setMessageType();
1616 //Refuse to send an empty message unless we are specifically allowing it
1617 if (!$this->AllowEmpty && empty($this->Body)) {
1618 throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
1619 }
1620
1621 //Trim subject consistently
1622 $this->Subject = trim($this->Subject);
1623 //Create body before headers in case body makes changes to headers (e.g. altering transfer encoding)
1624 $this->MIMEHeader = '';
1625 $this->MIMEBody = $this->createBody();
1626 //createBody may have added some headers, so retain them
1627 $tempheaders = $this->MIMEHeader;
1628 $this->MIMEHeader = $this->createHeader();
1629 $this->MIMEHeader .= $tempheaders;
1630
1631 //To capture the complete message when using mail(), create
1632 //an extra header list which createHeader() doesn't fold in
1633 if ('mail' === $this->Mailer) {
1634 if (count($this->to) > 0) {
1635 $this->mailHeader .= $this->addrAppend('To', $this->to);
1636 } else {
1637 $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;');
1638 }
1639 $this->mailHeader .= $this->headerLine(
1640 'Subject',
1641 $this->encodeHeader($this->secureHeader($this->Subject))
1642 );
1643 }
1644
1645 //Sign with DKIM if enabled
1646 if (
1647 !empty($this->DKIM_domain)
1648 && !empty($this->DKIM_selector)
1649 && (!empty($this->DKIM_private_string)
1650 || (!empty($this->DKIM_private)
1651 && static::isPermittedPath($this->DKIM_private)
1652 && file_exists($this->DKIM_private)
1653 )
1654 )
1655 ) {
1656 $header_dkim = $this->DKIM_Add(
1657 $this->MIMEHeader.$this->mailHeader,
1658 $this->encodeHeader($this->secureHeader($this->Subject)),
1659 $this->MIMEBody
1660 );
1661 $this->MIMEHeader = static::stripTrailingWSP($this->MIMEHeader).static::$LE.
1662 static::normalizeBreaks($header_dkim).static::$LE;
1663 }
1664
1665 return true;
1666 } catch (Exception $exc) {
1667 $this->setError($exc->getMessage());
1668 if ($this->exceptions) {
1669 throw $exc;
1670 }
1671
1672 return false;
1673 }
1674 }
1675
1676 /**
1677 * Actually send a message via the selected mechanism.
1678 *
1679 * @return bool
1680 * @throws Exception
1681 *
1682 */
1683 public function postSend()
1684 {
1685 try {
1686 //Choose the mailer and send through it
1687 switch ($this->Mailer) {
1688 case 'sendmail':
1689 case 'qmail':
1690 return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody);
1691 case 'smtp':
1692 return $this->smtpSend($this->MIMEHeader, $this->MIMEBody);
1693 case 'mail':
1694 return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
1695 default:
1696 $sendMethod = $this->Mailer.'Send';
1697 if (method_exists($this, $sendMethod)) {
1698 return $this->{$sendMethod}($this->MIMEHeader, $this->MIMEBody);
1699 }
1700
1701 return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
1702 }
1703 } catch (Exception $exc) {
1704 $this->setError($exc->getMessage());
1705 $this->edebug($exc->getMessage());
1706 if ($this->Mailer === 'smtp' && $this->SMTPKeepAlive == true && $this->smtp->connected()) {
1707 $this->smtp->reset();
1708 }
1709 if ($this->exceptions) {
1710 throw $exc;
1711 }
1712 }
1713
1714 return false;
1715 }
1716
1717 /**
1718 * Send mail using the $Sendmail program.
1719 *
1720 * @param string $header The message headers
1721 * @param string $body The message body
1722 *
1723 * @return bool
1724 * @throws Exception
1725 *
1726 * @see PHPMailer::$Sendmail
1727 *
1728 */
1729 protected function sendmailSend($header, $body)
1730 {
1731 if ($this->Mailer === 'qmail') {
1732 $this->edebug('Sending with qmail');
1733 } else {
1734 $this->edebug('Sending with sendmail');
1735 }
1736 $header = static::stripTrailingWSP($header).static::$LE.static::$LE;
1737 //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
1738 //A space after `-f` is optional, but there is a long history of its presence
1739 //causing problems, so we don't use one
1740 //Exim docs: https://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
1741 //Sendmail docs: https://www.sendmail.org/~ca/email/man/sendmail.html
1742 //Example problem: https://www.drupal.org/node/1057954
1743
1744 //PHP 5.6 workaround
1745 $sendmail_from_value = ini_get('sendmail_from');
1746 if (empty($this->Sender) && !empty($sendmail_from_value)) {
1747 //PHP config has a sender address we can use
1748 $this->Sender = ini_get('sendmail_from');
1749 }
1750 //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
1751 if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) {
1752 if ($this->Mailer === 'qmail') {
1753 $sendmailFmt = '%s -f%s';
1754 } else {
1755 $sendmailFmt = '%s -oi -f%s -t';
1756 }
1757 } else {
1758 //allow sendmail to choose a default envelope sender. It may
1759 //seem preferable to force it to use the From header as with
1760 //SMTP, but that introduces new problems (see
1761 //<https://github.com/PHPMailer/PHPMailer/issues/2298>), and
1762 //it has historically worked this way.
1763 $sendmailFmt = '%s -oi -t';
1764 }
1765
1766 $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender);
1767 $this->edebug('Sendmail path: '.$this->Sendmail);
1768 $this->edebug('Sendmail command: '.$sendmail);
1769 $this->edebug('Envelope sender: '.$this->Sender);
1770 $this->edebug("Headers: {$header}");
1771
1772 if ($this->SingleTo) {
1773 foreach ($this->SingleToArray as $toAddr) {
1774 $mail = @popen($sendmail, 'w');
1775 if (!$mail) {
1776 throw new Exception($this->lang('execute').$this->Sendmail, self::STOP_CRITICAL);
1777 }
1778 $this->edebug("To: {$toAddr}");
1779 fwrite($mail, 'To: '.$toAddr."\n");
1780 fwrite($mail, $header);
1781 fwrite($mail, $body);
1782 $result = pclose($mail);
1783 $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet);
1784 $this->doCallback(
1785 ($result === 0),
1786 [[$addrinfo['address'], $addrinfo['name']]],
1787 $this->cc,
1788 $this->bcc,
1789 $this->Subject,
1790 $body,
1791 $this->From,
1792 []
1793 );
1794 $this->edebug("Result: ".($result === 0 ? 'true' : 'false'));
1795 if (0 !== $result) {
1796 throw new Exception($this->lang('execute').$this->Sendmail, self::STOP_CRITICAL);
1797 }
1798 }
1799 } else {
1800 $mail = @popen($sendmail, 'w');
1801 if (!$mail) {
1802 throw new Exception($this->lang('execute').$this->Sendmail, self::STOP_CRITICAL);
1803 }
1804 fwrite($mail, $header);
1805 fwrite($mail, $body);
1806 $result = pclose($mail);
1807 $this->doCallback(
1808 ($result === 0),
1809 $this->to,
1810 $this->cc,
1811 $this->bcc,
1812 $this->Subject,
1813 $body,
1814 $this->From,
1815 []
1816 );
1817 $this->edebug("Result: ".($result === 0 ? 'true' : 'false'));
1818 if (0 !== $result) {
1819 throw new Exception($this->lang('execute').$this->Sendmail, self::STOP_CRITICAL);
1820 }
1821 }
1822
1823 return true;
1824 }
1825
1826 /**
1827 * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters.
1828 * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows.
1829 *
1830 * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report
1831 *
1832 * @param string $string The string to be validated
1833 *
1834 * @return bool
1835 */
1836 protected static function isShellSafe($string)
1837 {
1838 //It's not possible to use shell commands safely (which includes the mail() function) without escapeshellarg,
1839 //but some hosting providers disable it, creating a security problem that we don't want to have to deal with,
1840 //so we don't.
1841 if (!function_exists('escapeshellarg') || !function_exists('escapeshellcmd')) {
1842 return false;
1843 }
1844
1845 if (
1846 escapeshellcmd($string) !== $string
1847 || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])
1848 ) {
1849 return false;
1850 }
1851
1852 $length = strlen($string);
1853
1854 for ($i = 0; $i < $length; ++$i) {
1855 $c = $string[$i];
1856
1857 //All other characters have a special meaning in at least one common shell, including = and +.
1858 //Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
1859 //Note that this does permit non-Latin alphanumeric characters based on the current locale.
1860 if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
1861 return false;
1862 }
1863 }
1864
1865 return true;
1866 }
1867
1868 /**
1869 * Check whether a file path is of a permitted type.
1870 * Used to reject URLs and phar files from functions that access local file paths,
1871 * such as addAttachment.
1872 *
1873 * @param string $path A relative or absolute path to a file
1874 *
1875 * @return bool
1876 */
1877 protected static function isPermittedPath($path)
1878 {
1879 //Matches scheme definition from https://tools.ietf.org/html/rfc3986#section-3.1
1880 return !preg_match('#^[a-z][a-z\d+.-]*://#i', $path);
1881 }
1882
1883 /**
1884 * Check whether a file path is safe, accessible, and readable.
1885 *
1886 * @param string $path A relative or absolute path to a file
1887 *
1888 * @return bool
1889 */
1890 protected static function fileIsAccessible($path)
1891 {
1892 if (!static::isPermittedPath($path)) {
1893 return false;
1894 }
1895 $readable = is_file($path);
1896 //If not a UNC path (expected to start with \\), check read permission, see #2069
1897 if (strpos($path, '\\\\') !== 0) {
1898 $readable = $readable && is_readable($path);
1899 }
1900
1901 return $readable;
1902 }
1903
1904 /**
1905 * Send mail using the PHP mail() function.
1906 *
1907 * @see https://www.php.net/manual/en/book.mail.php
1908 *
1909 * @param string $header The message headers
1910 * @param string $body The message body
1911 *
1912 * @throws Exception
1913 *
1914 * @return bool
1915 */
1916 protected function mailSend($header, $body)
1917 {
1918 $header = static::stripTrailingWSP($header).static::$LE.static::$LE;
1919
1920 $toArr = [];
1921 foreach ($this->to as $toaddr) {
1922 $toArr[] = $this->addrFormat($toaddr);
1923 }
1924 $to = trim(implode(', ', $toArr));
1925
1926 //If there are no To-addresses (e.g. when sending only to BCC-addresses)
1927 //the following should be added to get a correct DKIM-signature.
1928 //Compare with $this->preSend()
1929 if ($to === '') {
1930 $to = 'undisclosed-recipients:;';
1931 }
1932
1933 $params = null;
1934 //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
1935 //A space after `-f` is optional, but there is a long history of its presence
1936 //causing problems, so we don't use one
1937 //Exim docs: https://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
1938 //Sendmail docs: https://www.sendmail.org/~ca/email/man/sendmail.html
1939 //Example problem: https://www.drupal.org/node/1057954
1940 //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
1941
1942 //PHP 5.6 workaround
1943 $sendmail_from_value = ini_get('sendmail_from');
1944 if (empty($this->Sender) && !empty($sendmail_from_value)) {
1945 //PHP config has a sender address we can use
1946 $this->Sender = ini_get('sendmail_from');
1947 }
1948 if (!empty($this->Sender) && static::validateAddress($this->Sender)) {
1949 if (self::isShellSafe($this->Sender)) {
1950 $params = sprintf('-f%s', $this->Sender);
1951 }
1952 $old_from = ini_get('sendmail_from');
1953 ini_set('sendmail_from', $this->Sender);
1954 }
1955 $result = false;
1956 if ($this->SingleTo && count($toArr) > 1) {
1957 foreach ($toArr as $toAddr) {
1958 $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);
1959 $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet);
1960 $this->doCallback(
1961 $result,
1962 [[$addrinfo['address'], $addrinfo['name']]],
1963 $this->cc,
1964 $this->bcc,
1965 $this->Subject,
1966 $body,
1967 $this->From,
1968 []
1969 );
1970 }
1971 } else {
1972 $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params);
1973 $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []);
1974 }
1975 if (isset($old_from)) {
1976 ini_set('sendmail_from', $old_from);
1977 }
1978 if (!$result) {
1979 throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL);
1980 }
1981
1982 return true;
1983 }
1984
1985 /**
1986 * Get an instance to use for SMTP operations.
1987 * Override this function to load your own SMTP implementation,
1988 * or set one with setSMTPInstance.
1989 *
1990 * @return SMTP
1991 */
1992 public function getSMTPInstance()
1993 {
1994 if (!is_object($this->smtp)) {
1995 $this->smtp = new Smtp();
1996 }
1997
1998 return $this->smtp;
1999 }
2000
2001 /**
2002 * Provide an instance to use for SMTP operations.
2003 *
2004 * @return SMTP
2005 */
2006 public function setSMTPInstance(SMTP $smtp)
2007 {
2008 $this->smtp = $smtp;
2009
2010 return $this->smtp;
2011 }
2012
2013 /**
2014 * Provide SMTP XCLIENT attributes
2015 *
2016 * @param string $name Attribute name
2017 * @param ?string $value Attribute value
2018 *
2019 * @return bool
2020 */
2021 public function setSMTPXclientAttribute($name, $value)
2022 {
2023 if (!in_array($name, SMTP::$xclient_allowed_attributes)) {
2024 return false;
2025 }
2026 if (isset($this->SMTPXClient[$name]) && $value === null) {
2027 unset($this->SMTPXClient[$name]);
2028 } elseif ($value !== null) {
2029 $this->SMTPXClient[$name] = $value;
2030 }
2031
2032 return true;
2033 }
2034
2035 /**
2036 * Get SMTP XCLIENT attributes
2037 *
2038 * @return array
2039 */
2040 public function getSMTPXclientAttributes()
2041 {
2042 return $this->SMTPXClient;
2043 }
2044
2045 /**
2046 * Send mail via SMTP.
2047 * Returns false if there is a bad MAIL FROM, RCPT, or DATA input.
2048 *
2049 * @param string $header The message headers
2050 * @param string $body The message body
2051 *
2052 * @return bool
2053 * @throws Exception
2054 *
2055 * @see PHPMailer::setSMTPInstance() to use a different class.
2056 *
2057 * @uses \PHPMailer\PHPMailer\SMTP
2058 *
2059 */
2060 protected function smtpSend($header, $body)
2061 {
2062 $header = static::stripTrailingWSP($header).static::$LE.static::$LE;
2063 $bad_rcpt = [];
2064 if (!$this->smtpConnect($this->SMTPOptions)) {
2065 throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL);
2066 }
2067 //Sender already validated in preSend()
2068 if ('' === $this->Sender) {
2069 $smtp_from = $this->From;
2070 } else {
2071 $smtp_from = $this->Sender;
2072 }
2073 if (count($this->SMTPXClient)) {
2074 $this->smtp->xclient($this->SMTPXClient);
2075 }
2076 if (!$this->smtp->mail($smtp_from)) {
2077 $this->setError($this->lang('from_failed').$smtp_from.' : '.implode(',', $this->smtp->getError()));
2078 throw new Exception($this->ErrorInfo, self::STOP_CRITICAL);
2079 }
2080
2081 $callbacks = [];
2082 //Attempt to send to all recipients
2083 foreach ([$this->to, $this->cc, $this->bcc] as $togroup) {
2084 foreach ($togroup as $to) {
2085 if (!$this->smtp->recipient($to[0], $this->dsn)) {
2086 $error = $this->smtp->getError();
2087 $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']];
2088 $isSent = false;
2089 } else {
2090 $isSent = true;
2091 }
2092
2093 $callbacks[] = ['issent' => $isSent, 'to' => $to[0], 'name' => $to[1]];
2094 }
2095 }
2096
2097 //Only send the DATA command if we have viable recipients
2098 if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header.$body)) {
2099 throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL);
2100 }
2101
2102 $smtp_transaction_id = $this->smtp->getLastTransactionID();
2103
2104 if ($this->SMTPKeepAlive) {
2105 $this->smtp->reset();
2106 } else {
2107 $this->smtp->quit();
2108 $this->smtp->close();
2109 }
2110
2111 foreach ($callbacks as $cb) {
2112 $this->doCallback(
2113 $cb['issent'],
2114 [[$cb['to'], $cb['name']]],
2115 [],
2116 [],
2117 $this->Subject,
2118 $body,
2119 $this->From,
2120 ['smtp_transaction_id' => $smtp_transaction_id]
2121 );
2122 }
2123
2124 //Create error message for any bad addresses
2125 if (count($bad_rcpt) > 0) {
2126 $errstr = '';
2127 foreach ($bad_rcpt as $bad) {
2128 $errstr .= $bad['to'].': '.$bad['error'].', ';
2129 }
2130 $errstr = rtrim($errstr, ', ');
2131
2132 $errorTmp = $this->smtp->getError();
2133 $errorLbl = empty($errorTmp) ? $this->Lang('recipients_failed') : implode(', ', $errorTmp);
2134 $this->setError($errorLbl.' ('.$errstr.') ');
2135
2136 // Avoid the nested MAIL command error
2137 $this->smtp->Reset();
2138 throw new Exception($this->ErrorInfo);
2139 }
2140
2141 return true;
2142 }
2143
2144 /**
2145 * Initiate a connection to an SMTP server.
2146 * Returns false if the operation failed.
2147 *
2148 * @param array $options An array of options compatible with stream_context_create()
2149 *
2150 * @return bool
2151 * @throws Exception
2152 *
2153 * @uses \PHPMailer\PHPMailer\SMTP
2154 *
2155 */
2156 public function smtpConnect($options = null)
2157 {
2158 if (null === $this->smtp) {
2159 $this->smtp = $this->getSMTPInstance();
2160 }
2161
2162 //If no options are provided, use whatever is set in the instance
2163 if (null === $options) {
2164 $options = $this->SMTPOptions;
2165 }
2166
2167 //Already connected?
2168 if ($this->smtp->connected()) {
2169 return true;
2170 }
2171
2172 $this->smtp->setTimeout($this->Timeout);
2173 $this->smtp->setDebugLevel($this->SMTPDebug);
2174 $this->smtp->setDebugOutput($this->Debugoutput);
2175 $this->smtp->setVerp($this->do_verp);
2176 if ($this->Host === null) {
2177 $this->Host = 'localhost';
2178 }
2179 $hosts = explode(';', $this->Host);
2180 $lastexception = null;
2181
2182 foreach ($hosts as $hostentry) {
2183 $hostinfo = [];
2184 if (
2185 !preg_match(
2186 '/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/',
2187 trim($hostentry),
2188 $hostinfo
2189 )
2190 ) {
2191 $this->edebug($this->lang('invalid_hostentry').' '.trim($hostentry));
2192 //Not a valid host entry
2193 continue;
2194 }
2195 //$hostinfo[1]: optional ssl or tls prefix
2196 //$hostinfo[2]: the hostname
2197 //$hostinfo[3]: optional port number
2198 //The host string prefix can temporarily override the current setting for SMTPSecure
2199 //If it's not specified, the default value is used
2200
2201 //Check the host name is a valid name or IP address before trying to use it
2202 if (!static::isValidHost($hostinfo[2])) {
2203 $this->edebug($this->lang('invalid_host').' '.$hostinfo[2]);
2204 continue;
2205 }
2206 $prefix = '';
2207 $secure = $this->SMTPSecure;
2208 $tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure);
2209 if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYPTION_SMTPS === $this->SMTPSecure)) {
2210 $prefix = 'ssl://';
2211 $tls = false; //Can't have SSL and TLS at the same time
2212 $secure = static::ENCRYPTION_SMTPS;
2213 } elseif ('tls' === $hostinfo[1]) {
2214 $tls = true;
2215 //TLS doesn't use a prefix
2216 $secure = static::ENCRYPTION_STARTTLS;
2217 }
2218 //Do we need the OpenSSL extension?
2219 $sslext = defined('OPENSSL_ALGO_SHA256');
2220 if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) {
2221 //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled
2222 if (!$sslext) {
2223 throw new Exception($this->lang('extension_missing').'openssl', self::STOP_CRITICAL);
2224 }
2225 }
2226 $host = $hostinfo[2];
2227 $port = $this->Port;
2228 if (
2229 array_key_exists(3, $hostinfo) &&
2230 is_numeric($hostinfo[3]) &&
2231 $hostinfo[3] > 0 &&
2232 $hostinfo[3] < 65536
2233 ) {
2234 $port = (int)$hostinfo[3];
2235 }
2236 if ($this->smtp->connect($prefix.$host, $port, $this->Timeout, $options)) {
2237 try {
2238 if ($this->Helo) {
2239 $hello = $this->Helo;
2240 } else {
2241 $hello = $this->serverHostname();
2242 }
2243 $this->smtp->hello($hello);
2244 //Automatically enable TLS encryption if:
2245 //* it's not disabled
2246 //* we are not connecting to localhost
2247 //* we have openssl extension
2248 //* we are not already using SSL
2249 //* the server offers STARTTLS
2250 if (
2251 $this->SMTPAutoTLS &&
2252 $this->Host !== 'localhost' &&
2253 $sslext &&
2254 $secure !== 'ssl' &&
2255 $this->smtp->getServerExt('STARTTLS')
2256 ) {
2257 $tls = true;
2258 }
2259 if ($tls) {
2260 if (!$this->smtp->startTLS()) {
2261 $message = $this->getSmtpErrorMessage('connect_host');
2262 throw new Exception($message);
2263 }
2264 //We must resend EHLO after TLS negotiation
2265 $this->smtp->hello($hello);
2266 }
2267 if (
2268 $this->SMTPAuth && !$this->smtp->authenticate(
2269 $this->Username,
2270 $this->Password,
2271 $this->AuthType,
2272 $this->oauth
2273 )
2274 ) {
2275 $errorTmp = $this->smtp->getError();
2276 $errorLbl = empty($errorTmp) ? $this->lang('authenticate') : implode(', ', $errorTmp);
2277 throw new Exception($errorLbl);
2278 }
2279
2280 return true;
2281 } catch (Exception $exc) {
2282 $lastexception = $exc;
2283 $this->edebug($exc->getMessage());
2284 //We must have connected, but then failed TLS or Auth, so close connection nicely
2285 $this->smtp->quit();
2286 }
2287 }
2288 }
2289 //If we get here, all connection attempts have failed, so close connection hard
2290 $this->smtp->close();
2291 //As we've caught all exceptions, just report whatever the last one was
2292 if ($this->exceptions && null !== $lastexception) {
2293 throw $lastexception;
2294 }
2295 if ($this->exceptions) {
2296 // no exception was thrown, likely $this->smtp->connect() failed
2297 $message = $this->getSmtpErrorMessage('connect_host');
2298 throw new Exception($message);
2299 }
2300
2301 return false;
2302 }
2303
2304 /**
2305 * Close the active SMTP session if one exists.
2306 */
2307 public function smtpClose()
2308 {
2309 if ((null !== $this->smtp) && $this->smtp->connected()) {
2310 $this->smtp->quit();
2311 $this->smtp->close();
2312 }
2313 }
2314
2315 /**
2316 * Set the language for error messages.
2317 * The default language is English.
2318 *
2319 * @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr")
2320 * Optionally, the language code can be enhanced with a 4-character
2321 * script annotation and/or a 2-character country annotation.
2322 * @param string $lang_path Path to the language file directory, with trailing separator (slash)
2323 * Do not set this from user input!
2324 *
2325 * @return bool Returns true if the requested language was loaded, false otherwise.
2326 */
2327 public function setLanguage($langcode = 'en', $lang_path = '')
2328 {
2329 //Backwards compatibility for renamed language codes
2330 $renamed_langcodes = [
2331 'br' => 'pt_br',
2332 'cz' => 'cs',
2333 'dk' => 'da',
2334 'no' => 'nb',
2335 'se' => 'sv',
2336 'rs' => 'sr',
2337 'tg' => 'tl',
2338 'am' => 'hy',
2339 ];
2340
2341 if (array_key_exists($langcode, $renamed_langcodes)) {
2342 $langcode = $renamed_langcodes[$langcode];
2343 }
2344
2345 //Define full set of translatable strings in English
2346 $PHPMAILER_LANG = [
2347 'authenticate' => 'SMTP Error: Could not authenticate.',
2348 'buggy_php' => 'Your version of PHP is affected by a bug that may result in corrupted messages.'.
2349 ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in'.
2350 ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.',
2351 'connect_host' => 'SMTP Error: Could not connect to SMTP host.',
2352 'data_not_accepted' => 'SMTP Error: data not accepted.',
2353 'empty_message' => 'Message body empty',
2354 'encoding' => 'Unknown encoding: ',
2355 'execute' => 'Could not execute: ',
2356 'extension_missing' => 'Extension missing: ',
2357 'file_access' => 'Could not access file: ',
2358 'file_open' => 'File Error: Could not open file: ',
2359 'from_failed' => 'The following From address failed: ',
2360 'instantiate' => 'Could not instantiate mail function.',
2361 'invalid_address' => 'Invalid address: ',
2362 'invalid_header' => 'Invalid header name or value',
2363 'invalid_hostentry' => 'Invalid hostentry: ',
2364 'invalid_host' => 'Invalid host: ',
2365 'mailer_not_supported' => ' mailer is not supported.',
2366 'provide_address' => 'You must provide at least one recipient email address.',
2367 'recipients_failed' => 'SMTP Error: The following recipients failed: ',
2368 'signing' => 'Signing Error: ',
2369 'smtp_code' => 'SMTP code: ',
2370 'smtp_code_ex' => 'Additional SMTP info: ',
2371 'smtp_connect_failed' => 'SMTP connect() failed.',
2372 'smtp_detail' => 'Detail: ',
2373 'smtp_error' => 'SMTP server error: ',
2374 'variable_set' => 'Cannot set or reset variable: ',
2375 ];
2376 if (empty($lang_path)) {
2377 //Calculate an absolute path so it can work if CWD is not here
2378 $lang_path = dirname(__DIR__).DIRECTORY_SEPARATOR.'language'.DIRECTORY_SEPARATOR;
2379 }
2380
2381 //Validate $langcode
2382 $foundlang = true;
2383 $langcode = strtolower($langcode);
2384 if (
2385 !preg_match('/^(?P<lang>[a-z]{2})(?P<script>_[a-z]{4})?(?P<country>_[a-z]{2})?$/', $langcode, $matches)
2386 && $langcode !== 'en'
2387 ) {
2388 $foundlang = false;
2389 $langcode = 'en';
2390 }
2391
2392 //There is no English translation file
2393 if ('en' !== $langcode) {
2394 $langcodes = [];
2395 if (!empty($matches['script']) && !empty($matches['country'])) {
2396 $langcodes[] = $matches['lang'].$matches['script'].$matches['country'];
2397 }
2398 if (!empty($matches['country'])) {
2399 $langcodes[] = $matches['lang'].$matches['country'];
2400 }
2401 if (!empty($matches['script'])) {
2402 $langcodes[] = $matches['lang'].$matches['script'];
2403 }
2404 $langcodes[] = $matches['lang'];
2405
2406 //Try and find a readable language file for the requested language.
2407 $foundFile = false;
2408 foreach ($langcodes as $code) {
2409 $lang_file = $lang_path.'phpmailer.lang-'.$code.'.php';
2410 if (static::fileIsAccessible($lang_file)) {
2411 $foundFile = true;
2412 break;
2413 }
2414 }
2415
2416 if ($foundFile === false) {
2417 $foundlang = false;
2418 } else {
2419 $lines = file($lang_file);
2420 foreach ($lines as $line) {
2421 //Translation file lines look like this:
2422 //$PHPMAILER_LANG['authenticate'] = 'SMTP-Fehler: Authentifizierung fehlgeschlagen.';
2423 //These files are parsed as text and not PHP so as to avoid the possibility of code injection
2424 //See https://blog.stevenlevithan.com/archives/match-quoted-string
2425 $matches = [];
2426 if (
2427 preg_match(
2428 '/^\$PHPMAILER_LANG\[\'([a-z\d_]+)\'\]\s*=\s*(["\'])(.+)*?\2;/',
2429 $line,
2430 $matches
2431 ) &&
2432 //Ignore unknown translation keys
2433 array_key_exists($matches[1], $PHPMAILER_LANG)
2434 ) {
2435 //Overwrite language-specific strings so we'll never have missing translation keys.
2436 $PHPMAILER_LANG[$matches[1]] = (string)$matches[3];
2437 }
2438 }
2439 }
2440 }
2441 $this->language = $PHPMAILER_LANG;
2442
2443 return $foundlang; //Returns false if language not found
2444 }
2445
2446 /**
2447 * Get the array of strings for the current language.
2448 *
2449 * @return array
2450 */
2451 public function getTranslations()
2452 {
2453 if (empty($this->language)) {
2454 $this->setLanguage(); // Set the default language.
2455 }
2456
2457 return $this->language;
2458 }
2459
2460 /**
2461 * Create recipient headers.
2462 *
2463 * @param string $type
2464 * @param array $addr An array of recipients,
2465 * where each recipient is a 2-element indexed array with element 0 containing an address
2466 * and element 1 containing a name, like:
2467 * [['joe@example.com', 'Joe User'], ['zoe@example.com', 'Zoe User']]
2468 *
2469 * @return string
2470 */
2471 public function addrAppend($type, $addr)
2472 {
2473 $addresses = [];
2474 foreach ($addr as $address) {
2475 $addresses[] = $this->addrFormat($address);
2476 }
2477
2478 return $type.': '.implode(', ', $addresses).static::$LE;
2479 }
2480
2481 /**
2482 * Format an address for use in a message header.
2483 *
2484 * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name like
2485 * ['joe@example.com', 'Joe User']
2486 *
2487 * @return string
2488 */
2489 public function addrFormat($addr)
2490 {
2491 if (!isset($addr[1]) || ($addr[1] === '')) { //No name provided
2492 return $this->secureHeader($addr[0]);
2493 }
2494
2495 return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase').
2496 ' <'.$this->secureHeader($addr[0]).'>';
2497 }
2498
2499 /**
2500 * Word-wrap message.
2501 * For use with mailers that do not automatically perform wrapping
2502 * and for quoted-printable encoded messages.
2503 * Original written by philippe.
2504 *
2505 * @param string $message The message to wrap
2506 * @param int $length The line length to wrap to
2507 * @param bool $qp_mode Whether to run in Quoted-Printable mode
2508 *
2509 * @return string
2510 */
2511 public function wrapText($message, $length, $qp_mode = false)
2512 {
2513 if ($qp_mode) {
2514 $soft_break = sprintf(' =%s', static::$LE);
2515 } else {
2516 $soft_break = static::$LE;
2517 }
2518 //If utf-8 encoding is used, we will need to make sure we don't
2519 //split multibyte characters when we wrap
2520 $is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet);
2521 $lelen = strlen(static::$LE);
2522 $crlflen = strlen(static::$LE);
2523
2524 $message = static::normalizeBreaks($message);
2525 //Remove a trailing line break
2526 if (substr($message, -$lelen) === static::$LE) {
2527 $message = substr($message, 0, -$lelen);
2528 }
2529
2530 //Split message into lines
2531 $lines = explode(static::$LE, $message);
2532 //Message will be rebuilt in here
2533 $message = '';
2534 foreach ($lines as $line) {
2535 $words = explode(' ', $line);
2536 $buf = '';
2537 $firstword = true;
2538 foreach ($words as $word) {
2539 if ($qp_mode && (strlen($word) > $length)) {
2540 $space_left = $length - strlen($buf) - $crlflen;
2541 if (!$firstword) {
2542 if ($space_left > 20) {
2543 $len = $space_left;
2544 if ($is_utf8) {
2545 $len = $this->utf8CharBoundary($word, $len);
2546 } elseif ('=' === substr($word, $len - 1, 1)) {
2547 --$len;
2548 } elseif ('=' === substr($word, $len - 2, 1)) {
2549 $len -= 2;
2550 }
2551 $part = substr($word, 0, $len);
2552 $word = substr($word, $len);
2553 $buf .= ' '.$part;
2554 $message .= $buf.sprintf('=%s', static::$LE);
2555 } else {
2556 $message .= $buf.$soft_break;
2557 }
2558 $buf = '';
2559 }
2560 while ($word !== '') {
2561 if ($length <= 0) {
2562 break;
2563 }
2564 $len = $length;
2565 if ($is_utf8) {
2566 $len = $this->utf8CharBoundary($word, $len);
2567 } elseif ('=' === substr($word, $len - 1, 1)) {
2568 --$len;
2569 } elseif ('=' === substr($word, $len - 2, 1)) {
2570 $len -= 2;
2571 }
2572 $part = substr($word, 0, $len);
2573 $word = (string)substr($word, $len);
2574
2575 if ($word !== '') {
2576 $message .= $part.sprintf('=%s', static::$LE);
2577 } else {
2578 $buf = $part;
2579 }
2580 }
2581 } else {
2582 $buf_o = $buf;
2583 if (!$firstword) {
2584 $buf .= ' ';
2585 }
2586 $buf .= $word;
2587
2588 if ('' !== $buf_o && strlen($buf) > $length) {
2589 $message .= $buf_o.$soft_break;
2590 $buf = $word;
2591 }
2592 }
2593 $firstword = false;
2594 }
2595 $message .= $buf.static::$LE;
2596 }
2597
2598 return $message;
2599 }
2600
2601 /**
2602 * Find the last character boundary prior to $maxLength in a utf-8
2603 * quoted-printable encoded string.
2604 * Original written by Colin Brown.
2605 *
2606 * @param string $encodedText utf-8 QP text
2607 * @param int $maxLength Find the last character boundary prior to this length
2608 *
2609 * @return int
2610 */
2611 public function utf8CharBoundary($encodedText, $maxLength)
2612 {
2613 $foundSplitPos = false;
2614 $lookBack = 3;
2615 while (!$foundSplitPos) {
2616 $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack);
2617 $encodedCharPos = strpos($lastChunk, '=');
2618 if (false !== $encodedCharPos) {
2619 //Found start of encoded character byte within $lookBack block.
2620 //Check the encoded byte value (the 2 chars after the '=')
2621 $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2);
2622 $dec = hexdec($hex);
2623 if ($dec < 128) {
2624 //Single byte character.
2625 //If the encoded char was found at pos 0, it will fit
2626 //otherwise reduce maxLength to start of the encoded char
2627 if ($encodedCharPos > 0) {
2628 $maxLength -= $lookBack - $encodedCharPos;
2629 }
2630 $foundSplitPos = true;
2631 } elseif ($dec >= 192) {
2632 //First byte of a multi byte character
2633 //Reduce maxLength to split at start of character
2634 $maxLength -= $lookBack - $encodedCharPos;
2635 $foundSplitPos = true;
2636 } elseif ($dec < 192) {
2637 //Middle byte of a multi byte character, look further back
2638 $lookBack += 3;
2639 }
2640 } else {
2641 //No encoded character found
2642 $foundSplitPos = true;
2643 }
2644 }
2645
2646 return $maxLength;
2647 }
2648
2649 /**
2650 * Apply word wrapping to the message body.
2651 * Wraps the message body to the number of chars set in the WordWrap property.
2652 * You should only do this to plain-text bodies as wrapping HTML tags may break them.
2653 * This is called automatically by createBody(), so you don't need to call it yourself.
2654 */
2655 public function setWordWrap()
2656 {
2657 if ($this->WordWrap < 1) {
2658 return;
2659 }
2660
2661 switch ($this->message_type) {
2662 case 'alt':
2663 case 'alt_inline':
2664 case 'alt_attach':
2665 case 'alt_inline_attach':
2666 $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap);
2667 break;
2668 default:
2669 $this->Body = $this->wrapText($this->Body, $this->WordWrap);
2670 break;
2671 }
2672 }
2673
2674 /**
2675 * Assemble message headers.
2676 *
2677 * @return string The assembled headers
2678 */
2679 public function createHeader()
2680 {
2681 $result = '';
2682
2683 $result .= $this->headerLine('Date', '' === $this->MessageDate ? self::rfcDate() : $this->MessageDate);
2684
2685 //The To header is created automatically by mail(), so needs to be omitted here
2686 if ('mail' !== $this->Mailer) {
2687 if ($this->SingleTo) {
2688 foreach ($this->to as $toaddr) {
2689 $this->SingleToArray[] = $this->addrFormat($toaddr);
2690 }
2691 } elseif (count($this->to) > 0) {
2692 $result .= $this->addrAppend('To', $this->to);
2693 } elseif (count($this->cc) === 0) {
2694 $result .= $this->headerLine('To', 'undisclosed-recipients:;');
2695 }
2696 }
2697 $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]);
2698
2699 //sendmail and mail() extract Cc from the header before sending
2700 if (count($this->cc) > 0) {
2701 $result .= $this->addrAppend('Cc', $this->cc);
2702 }
2703
2704 //sendmail and mail() extract Bcc from the header before sending
2705 if (
2706 (
2707 'sendmail' === $this->Mailer || 'qmail' === $this->Mailer || 'mail' === $this->Mailer
2708 )
2709 && count($this->bcc) > 0
2710 ) {
2711 $result .= $this->addrAppend('Bcc', $this->bcc);
2712 }
2713
2714 if (count($this->ReplyTo) > 0) {
2715 $result .= $this->addrAppend('Reply-To', $this->ReplyTo);
2716 }
2717
2718 //mail() sets the subject itself
2719 if ('mail' !== $this->Mailer) {
2720 $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject)));
2721 }
2722
2723 //Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4
2724 //https://tools.ietf.org/html/rfc5322#section-3.6.4
2725 if (
2726 '' !== $this->MessageID &&
2727 preg_match(
2728 '/^<((([a-z\d!#$%&\'*+\/=?^_`{|}~-]+(\.[a-z\d!#$%&\'*+\/=?^_`{|}~-]+)*)'.
2729 '|("(([\x01-\x08\x0B\x0C\x0E-\x1F\x7F]|[\x21\x23-\x5B\x5D-\x7E])'.
2730 '|(\\[\x01-\x09\x0B\x0C\x0E-\x7F]))*"))@(([a-z\d!#$%&\'*+\/=?^_`{|}~-]+'.
2731 '(\.[a-z\d!#$%&\'*+\/=?^_`{|}~-]+)*)|(\[(([\x01-\x08\x0B\x0C\x0E-\x1F\x7F]'.
2732 '|[\x21-\x5A\x5E-\x7E])|(\\[\x01-\x09\x0B\x0C\x0E-\x7F]))*\])))>$/Di',
2733 $this->MessageID
2734 )
2735 ) {
2736 $this->lastMessageID = $this->MessageID;
2737 } else {
2738 $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname());
2739 }
2740 $result .= $this->headerLine('Message-ID', $this->lastMessageID);
2741 if (null !== $this->Priority) {
2742 $result .= $this->headerLine('X-Priority', $this->Priority);
2743 }
2744 if ('' === $this->XMailer) {
2745 //Empty string for default X-Mailer header
2746 $result .= $this->headerLine(
2747 'X-Mailer',
2748 'PHPMailer '.self::VERSION.' (https://github.com/PHPMailer/PHPMailer)'
2749 );
2750 } elseif (is_string($this->XMailer) && trim($this->XMailer) !== '') {
2751 //Some string
2752 $result .= $this->headerLine('X-Mailer', trim($this->XMailer));
2753 } //Other values result in no X-Mailer header
2754
2755 if ('' !== $this->ConfirmReadingTo) {
2756 $result .= $this->headerLine('Disposition-Notification-To', '<'.$this->ConfirmReadingTo.'>');
2757 }
2758
2759 //Add custom headers
2760 foreach ($this->CustomHeader as $header) {
2761 $result .= $this->headerLine(
2762 trim($header[0]),
2763 $this->encodeHeader(trim($header[1]))
2764 );
2765 }
2766 if (!$this->sign_key_file) {
2767 $result .= $this->headerLine('MIME-Version', '1.0');
2768 $result .= $this->getMailMIME();
2769 }
2770
2771 return $result;
2772 }
2773
2774 /**
2775 * Get the message MIME type headers.
2776 *
2777 * @return string
2778 */
2779 public function getMailMIME()
2780 {
2781 $result = '';
2782 $ismultipart = true;
2783 switch ($this->message_type) {
2784 case 'inline':
2785 $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED.';');
2786 $result .= $this->textLine(' boundary="'.$this->boundary[1].'"');
2787 break;
2788 case 'attach':
2789 case 'inline_attach':
2790 case 'alt_attach':
2791 case 'alt_inline_attach':
2792 $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_MIXED.';');
2793 $result .= $this->textLine(' boundary="'.$this->boundary[1].'"');
2794 break;
2795 case 'alt':
2796 case 'alt_inline':
2797 $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE.';');
2798 $result .= $this->textLine(' boundary="'.$this->boundary[1].'"');
2799 break;
2800 default:
2801 //Catches case 'plain': and case '':
2802 $result .= $this->textLine('Content-Type: '.$this->ContentType.'; charset='.$this->CharSet);
2803 $ismultipart = false;
2804 break;
2805 }
2806 //RFC1341 part 5 says 7bit is assumed if not specified
2807 if (static::ENCODING_7BIT !== $this->Encoding) {
2808 //RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE
2809 if ($ismultipart) {
2810 if (static::ENCODING_8BIT === $this->Encoding) {
2811 $result .= $this->headerLine('Content-Transfer-Encoding', static::ENCODING_8BIT);
2812 }
2813 //The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible
2814 } else {
2815 $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding);
2816 }
2817 }
2818
2819 return $result;
2820 }
2821
2822 /**
2823 * Returns the whole MIME message.
2824 * Includes complete headers and body.
2825 * Only valid post preSend().
2826 *
2827 * @return string
2828 * @see PHPMailer::preSend()
2829 *
2830 */
2831 public function getSentMIMEMessage()
2832 {
2833 return static::stripTrailingWSP($this->MIMEHeader.$this->mailHeader).
2834 static::$LE.static::$LE.$this->MIMEBody;
2835 }
2836
2837 /**
2838 * Create a unique ID to use for boundaries.
2839 *
2840 * @return string
2841 */
2842 protected function generateId()
2843 {
2844 $len = 32; //32 bytes = 256 bits
2845 $bytes = '';
2846 if (function_exists('random_bytes')) {
2847 try {
2848 $bytes = random_bytes($len);
2849 } catch (\Exception $e) {
2850 //Do nothing
2851 }
2852 } elseif (function_exists('openssl_random_pseudo_bytes')) {
2853 /** @noinspection CryptographicallySecureRandomnessInspection */
2854 $bytes = openssl_random_pseudo_bytes($len);
2855 }
2856 if ($bytes === '') {
2857 //We failed to produce a proper random string, so make do.
2858 //Use a hash to force the length to the same as the other methods
2859 $bytes = hash('sha256', uniqid((string)mt_rand(), true), true);
2860 }
2861
2862 //We don't care about messing up base64 format here, just want a random string
2863 return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true)));
2864 }
2865
2866 /**
2867 * Assemble the message body.
2868 * Returns an empty string on failure.
2869 *
2870 * @return string The assembled message body
2871 * @throws Exception
2872 *
2873 */
2874 public function createBody()
2875 {
2876 $body = '';
2877 //Create unique IDs and preset boundaries
2878 $this->setBoundaries();
2879
2880 if ($this->sign_key_file) {
2881 $body .= $this->getMailMIME().static::$LE;
2882 }
2883
2884 $this->setWordWrap();
2885
2886 $bodyEncoding = $this->Encoding;
2887 $bodyCharSet = $this->CharSet;
2888 //Can we do a 7-bit downgrade?
2889 if (static::ENCODING_8BIT === $bodyEncoding && !$this->has8bitChars($this->Body)) {
2890 $bodyEncoding = static::ENCODING_7BIT;
2891 //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
2892 $bodyCharSet = static::CHARSET_ASCII;
2893 }
2894 //If lines are too long, and we're not already using an encoding that will shorten them,
2895 //change to quoted-printable transfer encoding for the body part only
2896 if (static::ENCODING_BASE64 !== $this->Encoding && static::hasLineLongerThanMax($this->Body)) {
2897 $bodyEncoding = static::ENCODING_QUOTED_PRINTABLE;
2898 }
2899
2900 $altBodyEncoding = $this->Encoding;
2901 $altBodyCharSet = $this->CharSet;
2902 //Can we do a 7-bit downgrade?
2903 if (static::ENCODING_8BIT === $altBodyEncoding && !$this->has8bitChars($this->AltBody)) {
2904 $altBodyEncoding = static::ENCODING_7BIT;
2905 //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
2906 $altBodyCharSet = static::CHARSET_ASCII;
2907 }
2908 //If lines are too long, and we're not already using an encoding that will shorten them,
2909 //change to quoted-printable transfer encoding for the alt body part only
2910 if (static::ENCODING_BASE64 !== $altBodyEncoding && static::hasLineLongerThanMax($this->AltBody)) {
2911 $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE;
2912 }
2913 //Use this as a preamble in all multipart message types
2914 $mimepre = '';
2915 switch ($this->message_type) {
2916 case 'inline':
2917 $body .= $mimepre;
2918 $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
2919 $body .= $this->encodeString($this->Body, $bodyEncoding);
2920 $body .= static::$LE;
2921 $body .= $this->attachAll('inline', $this->boundary[1]);
2922 break;
2923 case 'attach':
2924 $body .= $mimepre;
2925 $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
2926 $body .= $this->encodeString($this->Body, $bodyEncoding);
2927 $body .= static::$LE;
2928 $body .= $this->attachAll('attachment', $this->boundary[1]);
2929 break;
2930 case 'inline_attach':
2931 $body .= $mimepre;
2932 $body .= $this->textLine('--'.$this->boundary[1]);
2933 $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED.';');
2934 $body .= $this->textLine(' boundary="'.$this->boundary[2].'";');
2935 $body .= $this->textLine(' type="'.static::CONTENT_TYPE_TEXT_HTML.'"');
2936 $body .= static::$LE;
2937 $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding);
2938 $body .= $this->encodeString($this->Body, $bodyEncoding);
2939 $body .= static::$LE;
2940 $body .= $this->attachAll('inline', $this->boundary[2]);
2941 $body .= static::$LE;
2942 $body .= $this->attachAll('attachment', $this->boundary[1]);
2943 break;
2944 case 'alt':
2945 $body .= $mimepre;
2946 $body .= $this->getBoundary(
2947 $this->boundary[1],
2948 $altBodyCharSet,
2949 static::CONTENT_TYPE_PLAINTEXT,
2950 $altBodyEncoding
2951 );
2952 $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2953 $body .= static::$LE;
2954 $body .= $this->getBoundary(
2955 $this->boundary[1],
2956 $bodyCharSet,
2957 static::CONTENT_TYPE_TEXT_HTML,
2958 $bodyEncoding
2959 );
2960 $body .= $this->encodeString($this->Body, $bodyEncoding);
2961 $body .= static::$LE;
2962 if (!empty($this->Ical)) {
2963 $method = static::ICAL_METHOD_REQUEST;
2964 foreach (static::$IcalMethods as $imethod) {
2965 if (stripos($this->Ical, 'METHOD:'.$imethod) !== false) {
2966 $method = $imethod;
2967 break;
2968 }
2969 }
2970 $body .= $this->getBoundary(
2971 $this->boundary[1],
2972 '',
2973 static::CONTENT_TYPE_TEXT_CALENDAR.'; method='.$method,
2974 ''
2975 );
2976 $body .= $this->encodeString($this->Ical, $this->Encoding);
2977 $body .= static::$LE;
2978 }
2979 $body .= $this->endBoundary($this->boundary[1]);
2980 break;
2981 case 'alt_inline':
2982 $body .= $mimepre;
2983 $body .= $this->getBoundary(
2984 $this->boundary[1],
2985 $altBodyCharSet,
2986 static::CONTENT_TYPE_PLAINTEXT,
2987 $altBodyEncoding
2988 );
2989 $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2990 $body .= static::$LE;
2991 $body .= $this->textLine('--'.$this->boundary[1]);
2992 $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED.';');
2993 $body .= $this->textLine(' boundary="'.$this->boundary[2].'";');
2994 $body .= $this->textLine(' type="'.static::CONTENT_TYPE_TEXT_HTML.'"');
2995 $body .= static::$LE;
2996 $body .= $this->getBoundary(
2997 $this->boundary[2],
2998 $bodyCharSet,
2999 static::CONTENT_TYPE_TEXT_HTML,
3000 $bodyEncoding
3001 );
3002 $body .= $this->encodeString($this->Body, $bodyEncoding);
3003 $body .= static::$LE;
3004 $body .= $this->attachAll('inline', $this->boundary[2]);
3005 $body .= static::$LE;
3006 $body .= $this->endBoundary($this->boundary[1]);
3007 break;
3008 case 'alt_attach':
3009 $body .= $mimepre;
3010 $body .= $this->textLine('--'.$this->boundary[1]);
3011 $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE.';');
3012 $body .= $this->textLine(' boundary="'.$this->boundary[2].'"');
3013 $body .= static::$LE;
3014 $body .= $this->getBoundary(
3015 $this->boundary[2],
3016 $altBodyCharSet,
3017 static::CONTENT_TYPE_PLAINTEXT,
3018 $altBodyEncoding
3019 );
3020 $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
3021 $body .= static::$LE;
3022 $body .= $this->getBoundary(
3023 $this->boundary[2],
3024 $bodyCharSet,
3025 static::CONTENT_TYPE_TEXT_HTML,
3026 $bodyEncoding
3027 );
3028 $body .= $this->encodeString($this->Body, $bodyEncoding);
3029 $body .= static::$LE;
3030 if (!empty($this->Ical)) {
3031 $method = static::ICAL_METHOD_REQUEST;
3032 foreach (static::$IcalMethods as $imethod) {
3033 if (stripos($this->Ical, 'METHOD:'.$imethod) !== false) {
3034 $method = $imethod;
3035 break;
3036 }
3037 }
3038 $body .= $this->getBoundary(
3039 $this->boundary[2],
3040 '',
3041 static::CONTENT_TYPE_TEXT_CALENDAR.'; method='.$method,
3042 ''
3043 );
3044 $body .= $this->encodeString($this->Ical, $this->Encoding);
3045 }
3046 $body .= $this->endBoundary($this->boundary[2]);
3047 $body .= static::$LE;
3048 $body .= $this->attachAll('attachment', $this->boundary[1]);
3049 break;
3050 case 'alt_inline_attach':
3051 $body .= $mimepre;
3052 $body .= $this->textLine('--'.$this->boundary[1]);
3053 $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE.';');
3054 $body .= $this->textLine(' boundary="'.$this->boundary[2].'"');
3055 $body .= static::$LE;
3056 $body .= $this->getBoundary(
3057 $this->boundary[2],
3058 $altBodyCharSet,
3059 static::CONTENT_TYPE_PLAINTEXT,
3060 $altBodyEncoding
3061 );
3062 $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
3063 $body .= static::$LE;
3064 $body .= $this->textLine('--'.$this->boundary[2]);
3065 $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED.';');
3066 $body .= $this->textLine(' boundary="'.$this->boundary[3].'";');
3067 $body .= $this->textLine(' type="'.static::CONTENT_TYPE_TEXT_HTML.'"');
3068 $body .= static::$LE;
3069 $body .= $this->getBoundary(
3070 $this->boundary[3],
3071 $bodyCharSet,
3072 static::CONTENT_TYPE_TEXT_HTML,
3073 $bodyEncoding
3074 );
3075 $body .= $this->encodeString($this->Body, $bodyEncoding);
3076 $body .= static::$LE;
3077 $body .= $this->attachAll('inline', $this->boundary[3]);
3078 $body .= static::$LE;
3079 $body .= $this->endBoundary($this->boundary[2]);
3080 $body .= static::$LE;
3081 $body .= $this->attachAll('attachment', $this->boundary[1]);
3082 break;
3083 default:
3084 //Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types
3085 //Reset the `Encoding` property in case we changed it for line length reasons
3086 $this->Encoding = $bodyEncoding;
3087 $body .= $this->encodeString($this->Body, $this->Encoding);
3088 break;
3089 }
3090
3091 if ($this->isError()) {
3092 $body = '';
3093 if ($this->exceptions) {
3094 throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
3095 }
3096 } elseif ($this->sign_key_file) {
3097 try {
3098 if (!defined('PKCS7_TEXT')) {
3099 throw new Exception($this->lang('extension_missing').'openssl');
3100 }
3101
3102 $file = tempnam(sys_get_temp_dir(), 'srcsign');
3103 $signed = tempnam(sys_get_temp_dir(), 'mailsign');
3104 file_put_contents($file, $body);
3105
3106 //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197
3107 if (empty($this->sign_extracerts_file)) {
3108 $sign = @openssl_pkcs7_sign(
3109 $file,
3110 $signed,
3111 'file://'.realpath($this->sign_cert_file),
3112 ['file://'.realpath($this->sign_key_file), $this->sign_key_pass],
3113 []
3114 );
3115 } else {
3116 $sign = @openssl_pkcs7_sign(
3117 $file,
3118 $signed,
3119 'file://'.realpath($this->sign_cert_file),
3120 ['file://'.realpath($this->sign_key_file), $this->sign_key_pass],
3121 [],
3122 PKCS7_DETACHED,
3123 $this->sign_extracerts_file
3124 );
3125 }
3126
3127 @unlink($file);
3128 if ($sign) {
3129 $body = file_get_contents($signed);
3130 @unlink($signed);
3131 //The message returned by openssl contains both headers and body, so need to split them up
3132 $parts = explode("\n\n", $body, 2);
3133 $this->MIMEHeader .= $parts[0].static::$LE.static::$LE;
3134 $body = $parts[1];
3135 } else {
3136 @unlink($signed);
3137 throw new Exception($this->lang('signing').openssl_error_string());
3138 }
3139 } catch (Exception $exc) {
3140 $body = '';
3141 if ($this->exceptions) {
3142 throw $exc;
3143 }
3144 }
3145 }
3146
3147 return $body;
3148 }
3149
3150 /**
3151 * Get the boundaries that this message will use
3152 * @return array
3153 */
3154 public function getBoundaries()
3155 {
3156 if (empty($this->boundary)) {
3157 $this->setBoundaries();
3158 }
3159
3160 return $this->boundary;
3161 }
3162
3163 /**
3164 * Return the start of a message boundary.
3165 *
3166 * @param string $boundary
3167 * @param string $charSet
3168 * @param string $contentType
3169 * @param string $encoding
3170 *
3171 * @return string
3172 */
3173 protected function getBoundary($boundary, $charSet, $contentType, $encoding)
3174 {
3175 $result = '';
3176 if ('' === $charSet) {
3177 $charSet = $this->CharSet;
3178 }
3179 if ('' === $contentType) {
3180 $contentType = $this->ContentType;
3181 }
3182 if ('' === $encoding) {
3183 $encoding = $this->Encoding;
3184 }
3185 $result .= $this->textLine('--'.$boundary);
3186 $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet);
3187 $result .= static::$LE;
3188 //RFC1341 part 5 says 7bit is assumed if not specified
3189 if (static::ENCODING_7BIT !== $encoding) {
3190 $result .= $this->headerLine('Content-Transfer-Encoding', $encoding);
3191 }
3192 $result .= static::$LE;
3193
3194 return $result;
3195 }
3196
3197 /**
3198 * Return the end of a message boundary.
3199 *
3200 * @param string $boundary
3201 *
3202 * @return string
3203 */
3204 protected function endBoundary($boundary)
3205 {
3206 return static::$LE.'--'.$boundary.'--'.static::$LE;
3207 }
3208
3209 /**
3210 * Set the message type.
3211 * PHPMailer only supports some preset message types, not arbitrary MIME structures.
3212 */
3213 protected function setMessageType()
3214 {
3215 $type = [];
3216 if ($this->alternativeExists()) {
3217 $type[] = 'alt';
3218 }
3219 if ($this->inlineImageExists()) {
3220 $type[] = 'inline';
3221 }
3222 if ($this->attachmentExists()) {
3223 $type[] = 'attach';
3224 }
3225 $this->message_type = implode('_', $type);
3226 if ('' === $this->message_type) {
3227 //The 'plain' message_type refers to the message having a single body element, not that it is plain-text
3228 $this->message_type = 'plain';
3229 }
3230 }
3231
3232 /**
3233 * Format a header line.
3234 *
3235 * @param string $name
3236 * @param string|int $value
3237 *
3238 * @return string
3239 */
3240 public function headerLine($name, $value)
3241 {
3242 return $name.': '.$value.static::$LE;
3243 }
3244
3245 /**
3246 * Return a formatted mail line.
3247 *
3248 * @param string $value
3249 *
3250 * @return string
3251 */
3252 public function textLine($value)
3253 {
3254 return $value.static::$LE;
3255 }
3256
3257 /**
3258 * Add an attachment from a path on the filesystem.
3259 * Never use a user-supplied path to a file!
3260 * Returns false if the file could not be found or read.
3261 * Explicitly *does not* support passing URLs; PHPMailer is not an HTTP client.
3262 * If you need to do that, fetch the resource yourself and pass it in via a local file or string.
3263 *
3264 * @param string $path Path to the attachment
3265 * @param string $name Overrides the attachment name
3266 * @param string $encoding File encoding (see $Encoding)
3267 * @param string $type MIME type, e.g. `image/jpeg`; determined automatically from $path if not specified
3268 * @param string $disposition Disposition to use
3269 *
3270 * @return bool
3271 * @throws Exception
3272 *
3273 */
3274 public function addAttachment(
3275 $path,
3276 $name = '',
3277 $encoding = self::ENCODING_BASE64,
3278 $type = '',
3279 $disposition = 'attachment'
3280 ) {
3281 try {
3282 if (!static::fileIsAccessible($path)) {
3283 throw new Exception($this->lang('file_access').$path, self::STOP_CONTINUE);
3284 }
3285
3286 //If a MIME type is not specified, try to work it out from the file name
3287 if ('' === $type) {
3288 $type = static::filenameToType($path);
3289 }
3290
3291 $filename = (string)static::mb_pathinfo($path, PATHINFO_BASENAME);
3292 if ('' === $name) {
3293 $name = $filename;
3294 }
3295 if (!$this->validateEncoding($encoding)) {
3296 throw new Exception($this->lang('encoding').$encoding);
3297 }
3298
3299 $this->attachment[] = [
3300 0 => $path,
3301 1 => $filename,
3302 2 => $name,
3303 3 => $encoding,
3304 4 => $type,
3305 5 => false, //isStringAttachment
3306 6 => $disposition,
3307 7 => $name,
3308 ];
3309 } catch (Exception $exc) {
3310 $this->setError($exc->getMessage());
3311 $this->edebug($exc->getMessage());
3312 if ($this->exceptions) {
3313 throw $exc;
3314 }
3315
3316 return false;
3317 }
3318
3319 return true;
3320 }
3321
3322 /**
3323 * Return the array of attachments.
3324 *
3325 * @return array
3326 */
3327 public function getAttachments()
3328 {
3329 return $this->attachment;
3330 }
3331
3332 /**
3333 * Attach all file, string, and binary attachments to the message.
3334 * Returns an empty string on failure.
3335 *
3336 * @param string $disposition_type
3337 * @param string $boundary
3338 *
3339 * @return string
3340 * @throws Exception
3341 *
3342 */
3343 protected function attachAll($disposition_type, $boundary)
3344 {
3345 //Return text of body
3346 $mime = [];
3347 $cidUniq = [];
3348 $incl = [];
3349
3350 //Add all attachments
3351 foreach ($this->attachment as $attachment) {
3352 //Check if it is a valid disposition_filter
3353 if ($attachment[6] === $disposition_type) {
3354 //Check for string attachment
3355 $string = '';
3356 $path = '';
3357 $bString = $attachment[5];
3358 if ($bString) {
3359 $string = $attachment[0];
3360 } else {
3361 $path = $attachment[0];
3362 }
3363
3364 $inclhash = hash('sha256', serialize($attachment));
3365 if (in_array($inclhash, $incl, true)) {
3366 continue;
3367 }
3368 $incl[] = $inclhash;
3369 $name = $attachment[2];
3370 $encoding = $attachment[3];
3371 $type = $attachment[4];
3372 $disposition = $attachment[6];
3373 $cid = $attachment[7];
3374 if ('inline' === $disposition && array_key_exists($cid, $cidUniq)) {
3375 continue;
3376 }
3377 $cidUniq[$cid] = true;
3378
3379 $mime[] = sprintf('--%s%s', $boundary, static::$LE);
3380 //Only include a filename property if we have one
3381 if (!empty($name)) {
3382 $mime[] = sprintf(
3383 'Content-Type: %s; name=%s%s',
3384 $type,
3385 static::quotedString($this->encodeHeader($this->secureHeader($name))),
3386 static::$LE
3387 );
3388 } else {
3389 $mime[] = sprintf(
3390 'Content-Type: %s%s',
3391 $type,
3392 static::$LE
3393 );
3394 }
3395 //RFC1341 part 5 says 7bit is assumed if not specified
3396 if (static::ENCODING_7BIT !== $encoding) {
3397 $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE);
3398 }
3399
3400 //Only set Content-IDs on inline attachments
3401 if ((string)$cid !== '' && $disposition === 'inline') {
3402 $mime[] = 'Content-ID: <'.$this->encodeHeader($this->secureHeader($cid)).'>'.static::$LE;
3403 }
3404
3405 //Allow for bypassing the Content-Disposition header
3406 if (!empty($disposition)) {
3407 $encoded_name = $this->encodeHeader($this->secureHeader($name));
3408 if (!empty($encoded_name)) {
3409 $mime[] = sprintf(
3410 'Content-Disposition: %s; filename=%s%s',
3411 $disposition,
3412 static::quotedString($encoded_name),
3413 static::$LE.static::$LE
3414 );
3415 } else {
3416 $mime[] = sprintf(
3417 'Content-Disposition: %s%s',
3418 $disposition,
3419 static::$LE.static::$LE
3420 );
3421 }
3422 } else {
3423 $mime[] = static::$LE;
3424 }
3425
3426 //Encode as string attachment
3427 if ($bString) {
3428 $mime[] = $this->encodeString($string, $encoding);
3429 } else {
3430 $mime[] = $this->encodeFile($path, $encoding);
3431 }
3432 if ($this->isError()) {
3433 return '';
3434 }
3435 $mime[] = static::$LE;
3436 }
3437 }
3438
3439 $mime[] = sprintf('--%s--%s', $boundary, static::$LE);
3440
3441 return implode('', $mime);
3442 }
3443
3444 /**
3445 * Encode a file attachment in requested format.
3446 * Returns an empty string on failure.
3447 *
3448 * @param string $path The full path to the file
3449 * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
3450 *
3451 * @return string
3452 */
3453 protected function encodeFile($path, $encoding = self::ENCODING_BASE64)
3454 {
3455 try {
3456 if (!static::fileIsAccessible($path)) {
3457 throw new Exception($this->lang('file_open').$path, self::STOP_CONTINUE);
3458 }
3459 $file_buffer = file_get_contents($path);
3460 if (false === $file_buffer) {
3461 throw new Exception($this->lang('file_open').$path, self::STOP_CONTINUE);
3462 }
3463 $file_buffer = $this->encodeString($file_buffer, $encoding);
3464
3465 return $file_buffer;
3466 } catch (Exception $exc) {
3467 $this->setError($exc->getMessage());
3468 $this->edebug($exc->getMessage());
3469 if ($this->exceptions) {
3470 throw $exc;
3471 }
3472
3473 return '';
3474 }
3475 }
3476
3477 /**
3478 * Encode a string in requested format.
3479 * Returns an empty string on failure.
3480 *
3481 * @param string $str The text to encode
3482 * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
3483 *
3484 * @return string
3485 * @throws Exception
3486 *
3487 */
3488 public function encodeString($str, $encoding = self::ENCODING_BASE64)
3489 {
3490 $encoded = '';
3491 switch (strtolower($encoding)) {
3492 case static::ENCODING_BASE64:
3493 $encoded = chunk_split(
3494 base64_encode($str),
3495 static::STD_LINE_LENGTH,
3496 static::$LE
3497 );
3498 break;
3499 case static::ENCODING_7BIT:
3500 case static::ENCODING_8BIT:
3501 $encoded = static::normalizeBreaks($str);
3502 //Make sure it ends with a line break
3503 if (substr($encoded, -(strlen(static::$LE))) !== static::$LE) {
3504 $encoded .= static::$LE;
3505 }
3506 break;
3507 case static::ENCODING_BINARY:
3508 $encoded = $str;
3509 break;
3510 case static::ENCODING_QUOTED_PRINTABLE:
3511 $encoded = $this->encodeQP($str);
3512 break;
3513 default:
3514 $this->setError($this->lang('encoding').$encoding);
3515 if ($this->exceptions) {
3516 throw new Exception($this->lang('encoding').$encoding);
3517 }
3518 break;
3519 }
3520
3521 return $encoded;
3522 }
3523
3524 /**
3525 * Encode a header value (not including its label) optimally.
3526 * Picks shortest of Q, B, or none. Result includes folding if needed.
3527 * See RFC822 definitions for phrase, comment and text positions.
3528 *
3529 * @param string $str The header value to encode
3530 * @param string $position What context the string will be used in
3531 *
3532 * @return string
3533 */
3534 public function encodeHeader($str, $position = 'text')
3535 {
3536 $matchcount = 0;
3537 switch (strtolower($position)) {
3538 case 'phrase':
3539 if (!preg_match('/[\200-\377]/', $str)) {
3540 //Can't use addslashes as we don't know the value of magic_quotes_sybase
3541 $encoded = addcslashes($str, "\0..\37\177\\\"");
3542 if (($str === $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) {
3543 return $encoded;
3544 }
3545
3546 return "\"$encoded\"";
3547 }
3548 $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches);
3549 break;
3550 /* @noinspection PhpMissingBreakStatementInspection */
3551 case 'comment':
3552 $matchcount = preg_match_all('/[()"]/', $str, $matches);
3553 //fallthrough
3554 case 'text':
3555 default:
3556 $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches);
3557 break;
3558 }
3559
3560 if ($this->has8bitChars($str)) {
3561 $charset = $this->CharSet;
3562 } else {
3563 $charset = static::CHARSET_ASCII;
3564 }
3565
3566 //Q/B encoding adds 8 chars and the charset ("` =?<charset>?[QB]?<content>?=`").
3567 $overhead = 8 + strlen($charset);
3568
3569 if ('mail' === $this->Mailer) {
3570 $maxlen = static::MAIL_MAX_LINE_LENGTH - $overhead;
3571 } else {
3572 $maxlen = static::MAX_LINE_LENGTH - $overhead;
3573 }
3574
3575 //Select the encoding that produces the shortest output and/or prevents corruption.
3576 if ($matchcount > strlen($str) / 3) {
3577 //More than 1/3 of the content needs encoding, use B-encode.
3578 $encoding = 'B';
3579 } elseif ($matchcount > 0) {
3580 //Less than 1/3 of the content needs encoding, use Q-encode.
3581 $encoding = 'Q';
3582 } elseif (strlen($str) > $maxlen) {
3583 //No encoding needed, but value exceeds max line length, use Q-encode to prevent corruption.
3584 $encoding = 'Q';
3585 } else {
3586 //No reformatting needed
3587 $encoding = false;
3588 }
3589
3590 switch ($encoding) {
3591 case 'B':
3592 if ($this->hasMultiBytes($str)) {
3593 //Use a custom function which correctly encodes and wraps long
3594 //multibyte strings without breaking lines within a character
3595 $encoded = $this->base64EncodeWrapMB($str, "\n");
3596 } else {
3597 $encoded = base64_encode($str);
3598 $maxlen -= $maxlen % 4;
3599 $encoded = trim(chunk_split($encoded, $maxlen, "\n"));
3600 }
3601 $encoded = preg_replace('/^(.*)$/m', ' =?'.$charset."?$encoding?\\1?=", $encoded);
3602 break;
3603 case 'Q':
3604 $encoded = $this->encodeQ($str, $position);
3605 $encoded = $this->wrapText($encoded, $maxlen, true);
3606 $encoded = str_replace('='.static::$LE, "\n", trim($encoded));
3607 $encoded = preg_replace('/^(.*)$/m', ' =?'.$charset."?$encoding?\\1?=", $encoded);
3608 break;
3609 default:
3610 return $str;
3611 }
3612
3613 return trim(static::normalizeBreaks($encoded));
3614 }
3615
3616 /**
3617 * Check if a string contains multi-byte characters.
3618 *
3619 * @param string $str multi-byte text to wrap encode
3620 *
3621 * @return bool
3622 */
3623 public function hasMultiBytes($str)
3624 {
3625 if (function_exists('mb_strlen')) {
3626 return strlen($str) > mb_strlen($str, $this->CharSet);
3627 }
3628
3629 //Assume no multibytes (we can't handle without mbstring functions anyway)
3630 return false;
3631 }
3632
3633 /**
3634 * Does a string contain any 8-bit chars (in any charset)?
3635 *
3636 * @param string $text
3637 *
3638 * @return bool
3639 */
3640 public function has8bitChars($text)
3641 {
3642 return (bool)preg_match('/[\x80-\xFF]/', $text);
3643 }
3644
3645 /**
3646 * Encode and wrap long multibyte strings for mail headers
3647 * without breaking lines within a character.
3648 * Adapted from a function by paravoid.
3649 *
3650 * @see https://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283
3651 *
3652 * @param string $str multi-byte text to wrap encode
3653 * @param string $linebreak string to use as linefeed/end-of-line
3654 *
3655 * @return string
3656 */
3657 public function base64EncodeWrapMB($str, $linebreak = null)
3658 {
3659 $start = '=?'.$this->CharSet.'?B?';
3660 $end = '?=';
3661 $encoded = '';
3662 if (null === $linebreak) {
3663 $linebreak = static::$LE;
3664 }
3665
3666 $mb_length = mb_strlen($str, $this->CharSet);
3667 //Each line must have length <= 75, including $start and $end
3668 $length = 75 - strlen($start) - strlen($end);
3669 //Average multi-byte ratio
3670 $ratio = $mb_length / strlen($str);
3671 //Base64 has a 4:3 ratio
3672 $avgLength = floor($length * $ratio * .75);
3673
3674 $offset = 0;
3675 for ($i = 0; $i < $mb_length; $i += $offset) {
3676 $lookBack = 0;
3677 do {
3678 $offset = $avgLength - $lookBack;
3679 $chunk = mb_substr($str, $i, $offset, $this->CharSet);
3680 $chunk = base64_encode($chunk);
3681 ++$lookBack;
3682 } while (strlen($chunk) > $length);
3683 $encoded .= $chunk.$linebreak;
3684 }
3685
3686 //Chomp the last linefeed
3687 return substr($encoded, 0, -strlen($linebreak));
3688 }
3689
3690 /**
3691 * Encode a string in quoted-printable format.
3692 * According to RFC2045 section 6.7.
3693 *
3694 * @param string $string The text to encode
3695 *
3696 * @return string
3697 */
3698 public function encodeQP($string)
3699 {
3700 return static::normalizeBreaks(quoted_printable_encode($string));
3701 }
3702
3703 /**
3704 * Encode a string using Q encoding.
3705 *
3706 * @see https://www.rfc-editor.org/rfc/rfc2047#section-4.2
3707 *
3708 * @param string $str the text to encode
3709 * @param string $position Where the text is going to be used, see the RFC for what that means
3710 *
3711 * @return string
3712 */
3713 public function encodeQ($str, $position = 'text')
3714 {
3715 //There should not be any EOL in the string
3716 $pattern = '';
3717 $encoded = str_replace(["\r", "\n"], '', $str);
3718 switch (strtolower($position)) {
3719 case 'phrase':
3720 //RFC 2047 section 5.3
3721 $pattern = '^A-Za-z0-9!*+\/ -';
3722 break;
3723 /*
3724 * RFC 2047 section 5.2.
3725 * Build $pattern without including delimiters and []
3726 */
3727 /* @noinspection PhpMissingBreakStatementInspection */
3728 case 'comment':
3729 $pattern = '\(\)"';
3730 /* Intentional fall through */
3731 case 'text':
3732 default:
3733 //RFC 2047 section 5.1
3734 //Replace every high ascii, control, =, ? and _ characters
3735 $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377'.$pattern;
3736 break;
3737 }
3738 $matches = [];
3739 if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) {
3740 //If the string contains an '=', make sure it's the first thing we replace
3741 //so as to avoid double-encoding
3742 $eqkey = array_search('=', $matches[0], true);
3743 if (false !== $eqkey) {
3744 unset($matches[0][$eqkey]);
3745 array_unshift($matches[0], '=');
3746 }
3747 foreach (array_unique($matches[0]) as $char) {
3748 $encoded = str_replace($char, '='.sprintf('%02X', ord($char)), $encoded);
3749 }
3750 }
3751 //Replace spaces with _ (more readable than =20)
3752 //RFC 2047 section 4.2(2)
3753 return str_replace(' ', '_', $encoded);
3754 }
3755
3756 /**
3757 * Add a string or binary attachment (non-filesystem).
3758 * This method can be used to attach ascii or binary data,
3759 * such as a BLOB record from a database.
3760 *
3761 * @param string $string String attachment data
3762 * @param string $filename Name of the attachment
3763 * @param string $encoding File encoding (see $Encoding)
3764 * @param string $type File extension (MIME) type
3765 * @param string $disposition Disposition to use
3766 *
3767 * @return bool True on successfully adding an attachment
3768 * @throws Exception
3769 *
3770 */
3771 public function addStringAttachment(
3772 $string,
3773 $filename,
3774 $encoding = self::ENCODING_BASE64,
3775 $type = '',
3776 $disposition = 'attachment'
3777 ) {
3778 try {
3779 //If a MIME type is not specified, try to work it out from the file name
3780 if ('' === $type) {
3781 $type = static::filenameToType($filename);
3782 }
3783
3784 if (!$this->validateEncoding($encoding)) {
3785 throw new Exception($this->lang('encoding').$encoding);
3786 }
3787
3788 //Append to $attachment array
3789 $this->attachment[] = [
3790 0 => $string,
3791 1 => $filename,
3792 2 => static::mb_pathinfo($filename, PATHINFO_BASENAME),
3793 3 => $encoding,
3794 4 => $type,
3795 5 => true, //isStringAttachment
3796 6 => $disposition,
3797 7 => 0,
3798 ];
3799 } catch (Exception $exc) {
3800 $this->setError($exc->getMessage());
3801 $this->edebug($exc->getMessage());
3802 if ($this->exceptions) {
3803 throw $exc;
3804 }
3805
3806 return false;
3807 }
3808
3809 return true;
3810 }
3811
3812 /**
3813 * Add an embedded (inline) attachment from a file.
3814 * This can include images, sounds, and just about any other document type.
3815 * These differ from 'regular' attachments in that they are intended to be
3816 * displayed inline with the message, not just attached for download.
3817 * This is used in HTML messages that embed the images
3818 * the HTML refers to using the `$cid` value in `img` tags, for example `<img src="cid:mylogo">`.
3819 * Never use a user-supplied path to a file!
3820 *
3821 * @param string $path Path to the attachment
3822 * @param string $cid Content ID of the attachment; Use this to reference
3823 * the content when using an embedded image in HTML
3824 * @param string $name Overrides the attachment filename
3825 * @param string $encoding File encoding (see $Encoding) defaults to `base64`
3826 * @param string $type File MIME type (by default mapped from the `$path` filename's extension)
3827 * @param string $disposition Disposition to use: `inline` (default) or `attachment`
3828 * (unlikely you want this – {@see `addAttachment()`} instead)
3829 *
3830 * @return bool True on successfully adding an attachment
3831 * @throws Exception
3832 *
3833 */
3834 public function addEmbeddedImage(
3835 $path,
3836 $cid,
3837 $name = '',
3838 $encoding = self::ENCODING_BASE64,
3839 $type = '',
3840 $disposition = 'inline'
3841 ) {
3842 try {
3843 if (!static::fileIsAccessible($path)) {
3844 throw new Exception($this->lang('file_access').$path, self::STOP_CONTINUE);
3845 }
3846
3847 //If a MIME type is not specified, try to work it out from the file name
3848 if ('' === $type) {
3849 $type = static::filenameToType($path);
3850 }
3851
3852 if (!$this->validateEncoding($encoding)) {
3853 throw new Exception($this->lang('encoding').$encoding);
3854 }
3855
3856 $filename = (string)static::mb_pathinfo($path, PATHINFO_BASENAME);
3857 if ('' === $name) {
3858 $name = $filename;
3859 }
3860
3861 //Append to $attachment array
3862 $this->attachment[] = [
3863 0 => $path,
3864 1 => $filename,
3865 2 => $name,
3866 3 => $encoding,
3867 4 => $type,
3868 5 => false, //isStringAttachment
3869 6 => $disposition,
3870 7 => $cid,
3871 ];
3872 } catch (Exception $exc) {
3873 $this->setError($exc->getMessage());
3874 $this->edebug($exc->getMessage());
3875 if ($this->exceptions) {
3876 throw $exc;
3877 }
3878
3879 return false;
3880 }
3881
3882 return true;
3883 }
3884
3885 /**
3886 * Add an embedded stringified attachment.
3887 * This can include images, sounds, and just about any other document type.
3888 * If your filename doesn't contain an extension, be sure to set the $type to an appropriate MIME type.
3889 *
3890 * @param string $string The attachment binary data
3891 * @param string $cid Content ID of the attachment; Use this to reference
3892 * the content when using an embedded image in HTML
3893 * @param string $name A filename for the attachment. If this contains an extension,
3894 * PHPMailer will attempt to set a MIME type for the attachment.
3895 * For example 'file.jpg' would get an 'image/jpeg' MIME type.
3896 * @param string $encoding File encoding (see $Encoding), defaults to 'base64'
3897 * @param string $type MIME type - will be used in preference to any automatically derived type
3898 * @param string $disposition Disposition to use
3899 *
3900 * @return bool True on successfully adding an attachment
3901 * @throws Exception
3902 *
3903 */
3904 public function addStringEmbeddedImage(
3905 $string,
3906 $cid,
3907 $name = '',
3908 $encoding = self::ENCODING_BASE64,
3909 $type = '',
3910 $disposition = 'inline'
3911 ) {
3912 try {
3913 //If a MIME type is not specified, try to work it out from the name
3914 if ('' === $type && !empty($name)) {
3915 $type = static::filenameToType($name);
3916 }
3917
3918 if (!$this->validateEncoding($encoding)) {
3919 throw new Exception($this->lang('encoding').$encoding);
3920 }
3921
3922 //Append to $attachment array
3923 $this->attachment[] = [
3924 0 => $string,
3925 1 => $name,
3926 2 => $name,
3927 3 => $encoding,
3928 4 => $type,
3929 5 => true, //isStringAttachment
3930 6 => $disposition,
3931 7 => $cid,
3932 ];
3933 } catch (Exception $exc) {
3934 $this->setError($exc->getMessage());
3935 $this->edebug($exc->getMessage());
3936 if ($this->exceptions) {
3937 throw $exc;
3938 }
3939
3940 return false;
3941 }
3942
3943 return true;
3944 }
3945
3946 /**
3947 * Validate encodings.
3948 *
3949 * @param string $encoding
3950 *
3951 * @return bool
3952 */
3953 protected function validateEncoding($encoding)
3954 {
3955 return in_array(
3956 $encoding,
3957 [
3958 self::ENCODING_7BIT,
3959 self::ENCODING_QUOTED_PRINTABLE,
3960 self::ENCODING_BASE64,
3961 self::ENCODING_8BIT,
3962 self::ENCODING_BINARY,
3963 ],
3964 true
3965 );
3966 }
3967
3968 /**
3969 * Check if an embedded attachment is present with this cid.
3970 *
3971 * @param string $cid
3972 *
3973 * @return bool
3974 */
3975 protected function cidExists($cid)
3976 {
3977 foreach ($this->attachment as $attachment) {
3978 if ('inline' === $attachment[6] && $cid === $attachment[7]) {
3979 return true;
3980 }
3981 }
3982
3983 return false;
3984 }
3985
3986 /**
3987 * Check if an inline attachment is present.
3988 *
3989 * @return bool
3990 */
3991 public function inlineImageExists()
3992 {
3993 foreach ($this->attachment as $attachment) {
3994 if ('inline' === $attachment[6]) {
3995 return true;
3996 }
3997 }
3998
3999 return false;
4000 }
4001
4002 /**
4003 * Check if an attachment (non-inline) is present.
4004 *
4005 * @return bool
4006 */
4007 public function attachmentExists()
4008 {
4009 foreach ($this->attachment as $attachment) {
4010 if ('attachment' === $attachment[6]) {
4011 return true;
4012 }
4013 }
4014
4015 return false;
4016 }
4017
4018 /**
4019 * Check if this message has an alternative body set.
4020 *
4021 * @return bool
4022 */
4023 public function alternativeExists()
4024 {
4025 return !empty($this->AltBody);
4026 }
4027
4028 /**
4029 * Clear queued addresses of given kind.
4030 *
4031 * @param string $kind 'to', 'cc', or 'bcc'
4032 */
4033 public function clearQueuedAddresses($kind)
4034 {
4035 $this->RecipientsQueue = array_filter(
4036 $this->RecipientsQueue,
4037 static function ($params) use ($kind) {
4038 return $params[0] !== $kind;
4039 }
4040 );
4041 }
4042
4043 /**
4044 * Clear all To recipients.
4045 */
4046 public function clearAddresses()
4047 {
4048 foreach ($this->to as $to) {
4049 unset($this->all_recipients[strtolower($to[0])]);
4050 }
4051 $this->to = [];
4052 $this->clearQueuedAddresses('to');
4053 }
4054
4055 /**
4056 * Clear all CC recipients.
4057 */
4058 public function clearCCs()
4059 {
4060 foreach ($this->cc as $cc) {
4061 unset($this->all_recipients[strtolower($cc[0])]);
4062 }
4063 $this->cc = [];
4064 $this->clearQueuedAddresses('cc');
4065 }
4066
4067 /**
4068 * Clear all BCC recipients.
4069 */
4070 public function clearBCCs()
4071 {
4072 foreach ($this->bcc as $bcc) {
4073 unset($this->all_recipients[strtolower($bcc[0])]);
4074 }
4075 $this->bcc = [];
4076 $this->clearQueuedAddresses('bcc');
4077 }
4078
4079 /**
4080 * Clear all ReplyTo recipients.
4081 */
4082 public function clearReplyTos()
4083 {
4084 $this->ReplyTo = [];
4085 $this->ReplyToQueue = [];
4086 }
4087
4088 /**
4089 * Clear all recipient types.
4090 */
4091 public function clearAllRecipients()
4092 {
4093 $this->to = [];
4094 $this->cc = [];
4095 $this->bcc = [];
4096 $this->all_recipients = [];
4097 $this->RecipientsQueue = [];
4098 }
4099
4100 /**
4101 * Clear all filesystem, string, and binary attachments.
4102 */
4103 public function clearAttachments()
4104 {
4105 $this->attachment = [];
4106 }
4107
4108 /**
4109 * Clear all custom headers.
4110 */
4111 public function clearCustomHeaders()
4112 {
4113 $this->CustomHeader = [];
4114 }
4115
4116 /**
4117 * Clear a specific custom header by name or name and value.
4118 * $name value can be overloaded to contain
4119 * both header name and value (name:value).
4120 *
4121 * @param string $name Custom header name
4122 * @param string|null $value Header value
4123 *
4124 * @return bool True if a header was replaced successfully
4125 */
4126 public function clearCustomHeader($name, $value = null)
4127 {
4128 if (null === $value && strpos($name, ':') !== false) {
4129 //Value passed in as name:value
4130 [$name, $value] = explode(':', $name, 2);
4131 }
4132 $name = trim($name);
4133 $value = (null === $value) ? null : trim($value);
4134
4135 foreach ($this->CustomHeader as $k => $pair) {
4136 if ($pair[0] == $name) {
4137 // We remove the header if the value is not provided or it matches.
4138 if (null === $value || $pair[1] == $value) {
4139 unset($this->CustomHeader[$k]);
4140 }
4141 }
4142 }
4143
4144 return true;
4145 }
4146
4147 /**
4148 * Replace a custom header.
4149 * $name value can be overloaded to contain
4150 * both header name and value (name:value).
4151 *
4152 * @param string $name Custom header name
4153 * @param string|null $value Header value
4154 *
4155 * @return bool True if a header was replaced successfully
4156 * @throws Exception
4157 */
4158 public function replaceCustomHeader($name, $value = null)
4159 {
4160 if (null === $value && strpos($name, ':') !== false) {
4161 //Value passed in as name:value
4162 [$name, $value] = explode(':', $name, 2);
4163 }
4164 $name = trim($name);
4165 $value = (null === $value) ? '' : trim($value);
4166
4167 $replaced = false;
4168 foreach ($this->CustomHeader as $k => $pair) {
4169 if ($pair[0] == $name) {
4170 if ($replaced) {
4171 unset($this->CustomHeader[$k]);
4172 continue;
4173 }
4174 if (strpbrk($name.$value, "\r\n") !== false) {
4175 if ($this->exceptions) {
4176 throw new Exception($this->lang('invalid_header'));
4177 }
4178
4179 return false;
4180 }
4181 $this->CustomHeader[$k] = [$name, $value];
4182 $replaced = true;
4183 }
4184 }
4185
4186 return true;
4187 }
4188
4189 /**
4190 * Add an error message to the error container.
4191 *
4192 * @param string $msg
4193 */
4194 protected function setError($msg)
4195 {
4196 ++$this->error_count;
4197 if ('smtp' === $this->Mailer && null !== $this->smtp) {
4198 $lasterror = $this->smtp->getError();
4199 if (!empty($lasterror['error'])) {
4200 $msg .= $this->lang('smtp_error').$lasterror['error'];
4201 if (!empty($lasterror['detail'])) {
4202 $msg .= ' '.$this->lang('smtp_detail').$lasterror['detail'];
4203 }
4204 if (!empty($lasterror['smtp_code'])) {
4205 $msg .= ' '.$this->lang('smtp_code').$lasterror['smtp_code'];
4206 }
4207 if (!empty($lasterror['smtp_code_ex'])) {
4208 $msg .= ' '.$this->lang('smtp_code_ex').$lasterror['smtp_code_ex'];
4209 }
4210 }
4211 }
4212 $this->ErrorInfo = $msg;
4213 }
4214
4215 /**
4216 * Return an RFC 822 formatted date.
4217 *
4218 * @return string
4219 */
4220 public static function rfcDate()
4221 {
4222 //Set the time zone to whatever the default is to avoid 500 errors
4223 //Will default to UTC if it's not set properly in php.ini
4224 date_default_timezone_set(@date_default_timezone_get());
4225
4226 return date('D, j M Y H:i:s O');
4227 }
4228
4229 /**
4230 * Get the server hostname.
4231 * Returns 'localhost.localdomain' if unknown.
4232 *
4233 * @return string
4234 */
4235 protected function serverHostname()
4236 {
4237 $result = '';
4238 if (!empty($this->Hostname)) {
4239 $result = $this->Hostname;
4240 } elseif (isset($_SERVER) && array_key_exists('SERVER_NAME', $_SERVER)) {
4241 $result = $_SERVER['SERVER_NAME'];
4242 } elseif (function_exists('gethostname') && gethostname() !== false) {
4243 $result = gethostname();
4244 } elseif (php_uname('n') !== false) {
4245 $result = php_uname('n');
4246 }
4247 if (!static::isValidHost($result)) {
4248 return 'localhost.localdomain';
4249 }
4250
4251 return $result;
4252 }
4253
4254 /**
4255 * Validate whether a string contains a valid value to use as a hostname or IP address.
4256 * IPv6 addresses must include [], e.g. `[::1]`, not just `::1`.
4257 *
4258 * @param string $host The host name or IP address to check
4259 *
4260 * @return bool
4261 */
4262 public static function isValidHost($host)
4263 {
4264 //Simple syntax limits
4265 if (
4266 empty($host)
4267 || !is_string($host)
4268 || strlen($host) > 256
4269 || !preg_match('/^([a-zA-Z\d.-]*|\[[a-fA-F\d:]+\])$/', $host)
4270 ) {
4271 return false;
4272 }
4273 //Looks like a bracketed IPv6 address
4274 if (strlen($host) > 2 && substr($host, 0, 1) === '[' && substr($host, -1, 1) === ']') {
4275 return filter_var(substr($host, 1, -1), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
4276 }
4277 //If removing all the dots results in a numeric string, it must be an IPv4 address.
4278 //Need to check this first because otherwise things like `999.0.0.0` are considered valid host names
4279 if (is_numeric(str_replace('.', '', $host))) {
4280 //Is it a valid IPv4 address?
4281 return filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
4282 }
4283
4284 //Is it a syntactically valid hostname (when embedded in a URL)?
4285 return filter_var('https://'.$host, FILTER_VALIDATE_URL) !== false;
4286 }
4287
4288 /**
4289 * Get an error message in the current language.
4290 *
4291 * @param string $key
4292 *
4293 * @return string
4294 */
4295 protected function lang($key)
4296 {
4297 if (count($this->language) < 1) {
4298 $this->setLanguage(); //Set the default language
4299 }
4300
4301 if (array_key_exists($key, $this->language)) {
4302 if ('smtp_connect_failed' === $key) {
4303 //Include a link to troubleshooting docs on SMTP connection failure.
4304 //This is by far the biggest cause of support questions
4305 //but it's usually not PHPMailer's fault.
4306 return $this->language[$key].' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting';
4307 }
4308
4309 return $this->language[$key];
4310 }
4311
4312 //Return the key as a fallback
4313 return $key;
4314 }
4315
4316 /**
4317 * Build an error message starting with a generic one and adding details if possible.
4318 *
4319 * @param string $base_key
4320 *
4321 * @return string
4322 */
4323 private function getSmtpErrorMessage($base_key)
4324 {
4325 $message = $this->lang($base_key);
4326 $error = $this->smtp->getError();
4327 if (!empty($error['error'])) {
4328 $message .= ' '.$error['error'];
4329 if (!empty($error['detail'])) {
4330 $message .= ' '.$error['detail'];
4331 }
4332 }
4333
4334 return $message;
4335 }
4336
4337 /**
4338 * Check if an error occurred.
4339 *
4340 * @return bool True if an error did occur
4341 */
4342 public function isError()
4343 {
4344 return $this->error_count > 0;
4345 }
4346
4347 /**
4348 * Add a custom header.
4349 * $name value can be overloaded to contain
4350 * both header name and value (name:value).
4351 *
4352 * @param string $name Custom header name
4353 * @param string|null $value Header value
4354 *
4355 * @return bool True if a header was set successfully
4356 * @throws Exception
4357 */
4358 public function addCustomHeader($name, $value = null)
4359 {
4360 if (null === $value && strpos($name, ':') !== false) {
4361 //Value passed in as name:value
4362 [$name, $value] = explode(':', $name, 2);
4363 }
4364 $name = trim($name);
4365 $value = (null === $value) ? '' : trim($value);
4366 //Ensure name is not empty, and that neither name nor value contain line breaks
4367 if (empty($name) || strpbrk($name.$value, "\r\n") !== false) {
4368 if ($this->exceptions) {
4369 throw new Exception($this->lang('invalid_header'));
4370 }
4371
4372 return false;
4373 }
4374 $this->CustomHeader[] = [$name, $value];
4375
4376 return true;
4377 }
4378
4379 /**
4380 * Returns all custom headers.
4381 *
4382 * @return array
4383 */
4384 public function getCustomHeaders()
4385 {
4386 return $this->CustomHeader;
4387 }
4388
4389 /**
4390 * Create a message body from an HTML string.
4391 * Automatically inlines images and creates a plain-text version by converting the HTML,
4392 * overwriting any existing values in Body and AltBody.
4393 * Do not source $message content from user input!
4394 * $basedir is prepended when handling relative URLs, e.g. <img src="/images/a.png"> and must not be empty
4395 * will look for an image file in $basedir/images/a.png and convert it to inline.
4396 * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email)
4397 * Converts data-uri images into embedded attachments.
4398 * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly.
4399 *
4400 * @param string $message HTML message string
4401 * @param string $basedir Absolute path to a base directory to prepend to relative paths to images
4402 * @param bool|callable $advanced Whether to use the internal HTML to text converter
4403 * or your own custom converter
4404 *
4405 * @return string The transformed message body
4406 *
4407 * @throws Exception
4408 *
4409 * @see PHPMailer::html2text()
4410 */
4411 public function msgHTML($message, $basedir = '', $advanced = false)
4412 {
4413 preg_match_all('/(?<!-)(src|background)=["\'](.*)["\']/Ui', $message, $images);
4414 if (array_key_exists(2, $images)) {
4415 if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
4416 //Ensure $basedir has a trailing /
4417 $basedir .= '/';
4418 }
4419 foreach ($images[2] as $imgindex => $url) {
4420 //Convert data URIs into embedded images
4421 //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
4422 $match = [];
4423 if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) {
4424 if (count($match) === 4 && static::ENCODING_BASE64 === $match[2]) {
4425 $data = base64_decode($match[3]);
4426 } elseif ('' === $match[2]) {
4427 $data = rawurldecode($match[3]);
4428 } else {
4429 //Not recognised so leave it alone
4430 continue;
4431 }
4432 //Hash the decoded data, not the URL, so that the same data-URI image used in multiple places
4433 //will only be embedded once, even if it used a different encoding
4434 $cid = substr(hash('sha256', $data), 0, 32).'@phpmailer.0'; //RFC2392 S 2
4435
4436 if (!$this->cidExists($cid)) {
4437 $this->addStringEmbeddedImage(
4438 $data,
4439 $cid,
4440 'embed'.$imgindex,
4441 static::ENCODING_BASE64,
4442 $match[1]
4443 );
4444 }
4445 $message = str_replace(
4446 $images[0][$imgindex],
4447 $images[1][$imgindex].'="cid:'.$cid.'"',
4448 $message
4449 );
4450 continue;
4451 }
4452 if (
4453 //Only process relative URLs if a basedir is provided (i.e. no absolute local paths)
4454 !empty($basedir)
4455 //Ignore URLs containing parent dir traversal (..)
4456 && (strpos($url, '..') === false)
4457 //Do not change urls that are already inline images
4458 && 0 !== strpos($url, 'cid:')
4459 //Do not change absolute URLs, including anonymous protocol
4460 && !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url)
4461 ) {
4462 $filename = static::mb_pathinfo($url, PATHINFO_BASENAME);
4463 $directory = dirname($url);
4464 if ('.' === $directory) {
4465 $directory = '';
4466 }
4467 //RFC2392 S 2
4468 $cid = substr(hash('sha256', $url), 0, 32).'@phpmailer.0';
4469 if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
4470 $basedir .= '/';
4471 }
4472 if (strlen($directory) > 1 && '/' !== substr($directory, -1)) {
4473 $directory .= '/';
4474 }
4475 if (
4476 $this->addEmbeddedImage(
4477 $basedir.$directory.$filename,
4478 $cid,
4479 $filename,
4480 static::ENCODING_BASE64,
4481 static::_mime_types((string)static::mb_pathinfo($filename, PATHINFO_EXTENSION))
4482 )
4483 ) {
4484 $message = preg_replace(
4485 '/'.$images[1][$imgindex].'=["\']'.preg_quote($url, '/').'["\']/Ui',
4486 $images[1][$imgindex].'="cid:'.$cid.'"',
4487 $message
4488 );
4489 }
4490 }
4491 }
4492 }
4493 $this->isHTML();
4494 //Convert all message body line breaks to LE, makes quoted-printable encoding work much better
4495 $this->Body = static::normalizeBreaks($message);
4496 $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced));
4497 if (!$this->alternativeExists()) {
4498 $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'
4499 .static::$LE;
4500 }
4501
4502 return $this->Body;
4503 }
4504
4505 /**
4506 * Convert an HTML string into plain text.
4507 * This is used by msgHTML().
4508 * Note - older versions of this function used a bundled advanced converter
4509 * which was removed for license reasons in #232.
4510 * Example usage:
4511 *
4512 * ```php
4513 * //Use default conversion
4514 * $plain = $mail->html2text($html);
4515 * //Use your own custom converter
4516 * $plain = $mail->html2text($html, function($html) {
4517 * $converter = new MyHtml2text($html);
4518 * return $converter->get_text();
4519 * });
4520 * ```
4521 *
4522 * @param string $html The HTML text to convert
4523 * @param bool|callable $advanced Any boolean value to use the internal converter,
4524 * or provide your own callable for custom conversion.
4525 * *Never* pass user-supplied data into this parameter
4526 *
4527 * @return string
4528 */
4529 public function html2text($html, $advanced = false)
4530 {
4531 if (is_callable($advanced)) {
4532 return call_user_func($advanced, $html);
4533 }
4534
4535 return html_entity_decode(
4536 trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
4537 ENT_QUOTES,
4538 $this->CharSet
4539 );
4540 }
4541
4542 /**
4543 * Get the MIME type for a file extension.
4544 *
4545 * @param string $ext File extension
4546 *
4547 * @return string MIME type of file
4548 */
4549 public static function _mime_types($ext = '')
4550 {
4551 $mimes = [
4552 'xl' => 'application/excel',
4553 'js' => 'application/javascript',
4554 'hqx' => 'application/mac-binhex40',
4555 'cpt' => 'application/mac-compactpro',
4556 'bin' => 'application/macbinary',
4557 'doc' => 'application/msword',
4558 'word' => 'application/msword',
4559 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
4560 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
4561 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
4562 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
4563 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
4564 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
4565 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
4566 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
4567 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
4568 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
4569 'class' => 'application/octet-stream',
4570 'dll' => 'application/octet-stream',
4571 'dms' => 'application/octet-stream',
4572 'exe' => 'application/octet-stream',
4573 'lha' => 'application/octet-stream',
4574 'lzh' => 'application/octet-stream',
4575 'psd' => 'application/octet-stream',
4576 'sea' => 'application/octet-stream',
4577 'so' => 'application/octet-stream',
4578 'oda' => 'application/oda',
4579 'pdf' => 'application/pdf',
4580 'ai' => 'application/postscript',
4581 'eps' => 'application/postscript',
4582 'ps' => 'application/postscript',
4583 'smi' => 'application/smil',
4584 'smil' => 'application/smil',
4585 'mif' => 'application/vnd.mif',
4586 'xls' => 'application/vnd.ms-excel',
4587 'ppt' => 'application/vnd.ms-powerpoint',
4588 'wbxml' => 'application/vnd.wap.wbxml',
4589 'wmlc' => 'application/vnd.wap.wmlc',
4590 'dcr' => 'application/x-director',
4591 'dir' => 'application/x-director',
4592 'dxr' => 'application/x-director',
4593 'dvi' => 'application/x-dvi',
4594 'gtar' => 'application/x-gtar',
4595 'php3' => 'application/x-httpd-php',
4596 'php4' => 'application/x-httpd-php',
4597 'php' => 'application/x-httpd-php',
4598 'phtml' => 'application/x-httpd-php',
4599 'phps' => 'application/x-httpd-php-source',
4600 'swf' => 'application/x-shockwave-flash',
4601 'sit' => 'application/x-stuffit',
4602 'tar' => 'application/x-tar',
4603 'tgz' => 'application/x-tar',
4604 'xht' => 'application/xhtml+xml',
4605 'xhtml' => 'application/xhtml+xml',
4606 'zip' => 'application/zip',
4607 'mid' => 'audio/midi',
4608 'midi' => 'audio/midi',
4609 'mp2' => 'audio/mpeg',
4610 'mp3' => 'audio/mpeg',
4611 'm4a' => 'audio/mp4',
4612 'mpga' => 'audio/mpeg',
4613 'aif' => 'audio/x-aiff',
4614 'aifc' => 'audio/x-aiff',
4615 'aiff' => 'audio/x-aiff',
4616 'ram' => 'audio/x-pn-realaudio',
4617 'rm' => 'audio/x-pn-realaudio',
4618 'rpm' => 'audio/x-pn-realaudio-plugin',
4619 'ra' => 'audio/x-realaudio',
4620 'wav' => 'audio/x-wav',
4621 'mka' => 'audio/x-matroska',
4622 'bmp' => 'image/bmp',
4623 'gif' => 'image/gif',
4624 'jpeg' => 'image/jpeg',
4625 'jpe' => 'image/jpeg',
4626 'jpg' => 'image/jpeg',
4627 'png' => 'image/png',
4628 'tiff' => 'image/tiff',
4629 'tif' => 'image/tiff',
4630 'webp' => 'image/webp',
4631 'avif' => 'image/avif',
4632 'heif' => 'image/heif',
4633 'heifs' => 'image/heif-sequence',
4634 'heic' => 'image/heic',
4635 'heics' => 'image/heic-sequence',
4636 'eml' => 'message/rfc822',
4637 'css' => 'text/css',
4638 'html' => 'text/html',
4639 'htm' => 'text/html',
4640 'shtml' => 'text/html',
4641 'log' => 'text/plain',
4642 'text' => 'text/plain',
4643 'txt' => 'text/plain',
4644 'rtx' => 'text/richtext',
4645 'rtf' => 'text/rtf',
4646 'vcf' => 'text/vcard',
4647 'vcard' => 'text/vcard',
4648 'ics' => 'text/calendar',
4649 'xml' => 'text/xml',
4650 'xsl' => 'text/xml',
4651 'csv' => 'text/csv',
4652 'wmv' => 'video/x-ms-wmv',
4653 'mpeg' => 'video/mpeg',
4654 'mpe' => 'video/mpeg',
4655 'mpg' => 'video/mpeg',
4656 'mp4' => 'video/mp4',
4657 'm4v' => 'video/mp4',
4658 'mov' => 'video/quicktime',
4659 'qt' => 'video/quicktime',
4660 'rv' => 'video/vnd.rn-realvideo',
4661 'avi' => 'video/x-msvideo',
4662 'movie' => 'video/x-sgi-movie',
4663 'webm' => 'video/webm',
4664 'mkv' => 'video/x-matroska',
4665 ];
4666 $ext = strtolower($ext);
4667 if (array_key_exists($ext, $mimes)) {
4668 return $mimes[$ext];
4669 }
4670
4671 return 'application/octet-stream';
4672 }
4673
4674 /**
4675 * Map a file name to a MIME type.
4676 * Defaults to 'application/octet-stream', i.e.. arbitrary binary data.
4677 *
4678 * @param string $filename A file name or full path, does not need to exist as a file
4679 *
4680 * @return string
4681 */
4682 public static function filenameToType($filename)
4683 {
4684 //In case the path is a URL, strip any query string before getting extension
4685 $qpos = strpos($filename, '?');
4686 if (false !== $qpos) {
4687 $filename = substr($filename, 0, $qpos);
4688 }
4689 $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION);
4690
4691 return static::_mime_types($ext);
4692 }
4693
4694 /**
4695 * Multi-byte-safe pathinfo replacement.
4696 * Drop-in replacement for pathinfo(), but multibyte- and cross-platform-safe.
4697 *
4698 * @see https://www.php.net/manual/en/function.pathinfo.php#107461
4699 *
4700 * @param string $path A filename or path, does not need to exist as a file
4701 * @param int|string $options Either a PATHINFO_* constant,
4702 * or a string name to return only the specified piece
4703 *
4704 * @return string|array
4705 */
4706 public static function mb_pathinfo($path, $options = null)
4707 {
4708 $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];
4709 $pathinfo = [];
4710 if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^.\\\\/]+?)|))[\\\\/.]*$#m', $path, $pathinfo)) {
4711 if (array_key_exists(1, $pathinfo)) {
4712 $ret['dirname'] = $pathinfo[1];
4713 }
4714 if (array_key_exists(2, $pathinfo)) {
4715 $ret['basename'] = $pathinfo[2];
4716 }
4717 if (array_key_exists(5, $pathinfo)) {
4718 $ret['extension'] = $pathinfo[5];
4719 }
4720 if (array_key_exists(3, $pathinfo)) {
4721 $ret['filename'] = $pathinfo[3];
4722 }
4723 }
4724 switch ($options) {
4725 case PATHINFO_DIRNAME:
4726 case 'dirname':
4727 return $ret['dirname'];
4728 case PATHINFO_BASENAME:
4729 case 'basename':
4730 return $ret['basename'];
4731 case PATHINFO_EXTENSION:
4732 case 'extension':
4733 return $ret['extension'];
4734 case PATHINFO_FILENAME:
4735 case 'filename':
4736 return $ret['filename'];
4737 default:
4738 return $ret;
4739 }
4740 }
4741
4742 /**
4743 * Set or reset instance properties.
4744 * You should avoid this function - it's more verbose, less efficient, more error-prone and
4745 * harder to debug than setting properties directly.
4746 * Usage Example:
4747 * `$mail->set('SMTPSecure', static::ENCRYPTION_STARTTLS);`
4748 * is the same as:
4749 * `$mail->SMTPSecure = static::ENCRYPTION_STARTTLS;`.
4750 *
4751 * @param string $name The property name to set
4752 * @param mixed $value The value to set the property to
4753 *
4754 * @return bool
4755 */
4756 public function set($name, $value = '')
4757 {
4758 if (property_exists($this, $name)) {
4759 $this->{$name} = $value;
4760
4761 return true;
4762 }
4763 $this->setError($this->lang('variable_set').$name);
4764
4765 return false;
4766 }
4767
4768 /**
4769 * Strip newlines to prevent header injection.
4770 *
4771 * @param string $str
4772 *
4773 * @return string
4774 */
4775 public function secureHeader($str)
4776 {
4777 return trim(str_replace(["\r", "\n"], '', $str));
4778 }
4779
4780 /**
4781 * Normalize line breaks in a string.
4782 * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format.
4783 * Defaults to CRLF (for message bodies) and preserves consecutive breaks.
4784 *
4785 * @param string $text
4786 * @param string $breaktype What kind of line break to use; defaults to static::$LE
4787 *
4788 * @return string
4789 */
4790 public static function normalizeBreaks($text, $breaktype = null)
4791 {
4792 if (null === $breaktype) {
4793 $breaktype = static::$LE;
4794 }
4795 //Normalise to \n
4796 $text = str_replace([self::CRLF, "\r"], "\n", $text);
4797 //Now convert LE as needed
4798 if ("\n" !== $breaktype) {
4799 $text = str_replace("\n", $breaktype, $text);
4800 }
4801
4802 return $text;
4803 }
4804
4805 /**
4806 * Remove trailing whitespace from a string.
4807 *
4808 * @param string $text
4809 *
4810 * @return string The text to remove whitespace from
4811 */
4812 public static function stripTrailingWSP($text)
4813 {
4814 return rtrim($text, " \r\n\t");
4815 }
4816
4817 /**
4818 * Strip trailing line breaks from a string.
4819 *
4820 * @param string $text
4821 *
4822 * @return string The text to remove breaks from
4823 */
4824 public static function stripTrailingBreaks($text)
4825 {
4826 return rtrim($text, "\r\n");
4827 }
4828
4829 /**
4830 * Return the current line break format string.
4831 *
4832 * @return string
4833 */
4834 public static function getLE()
4835 {
4836 return static::$LE;
4837 }
4838
4839 /**
4840 * Set the line break format string, e.g. "\r\n".
4841 *
4842 * @param string $le
4843 */
4844 protected static function setLE($le)
4845 {
4846 static::$LE = $le;
4847 }
4848
4849 /**
4850 * Set the public and private key files and password for S/MIME signing.
4851 *
4852 * @param string $cert_filename
4853 * @param string $key_filename
4854 * @param string $key_pass Password for private key
4855 * @param string $extracerts_filename Optional path to chain certificate
4856 */
4857 public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '')
4858 {
4859 $this->sign_cert_file = $cert_filename;
4860 $this->sign_key_file = $key_filename;
4861 $this->sign_key_pass = $key_pass;
4862 $this->sign_extracerts_file = $extracerts_filename;
4863 }
4864
4865 /**
4866 * Quoted-Printable-encode a DKIM header.
4867 *
4868 * @param string $txt
4869 *
4870 * @return string
4871 */
4872 public function DKIM_QP($txt)
4873 {
4874 $line = '';
4875 $len = strlen($txt);
4876 for ($i = 0; $i < $len; ++$i) {
4877 $ord = ord($txt[$i]);
4878 if (((0x21 <= $ord) && ($ord <= 0x3A)) || $ord === 0x3C || ((0x3E <= $ord) && ($ord <= 0x7E))) {
4879 $line .= $txt[$i];
4880 } else {
4881 $line .= '='.sprintf('%02X', $ord);
4882 }
4883 }
4884
4885 return $line;
4886 }
4887
4888 /**
4889 * Generate a DKIM signature.
4890 *
4891 * @param string $signHeader
4892 *
4893 * @return string The DKIM signature value
4894 * @throws Exception
4895 *
4896 */
4897 public function DKIM_Sign($signHeader)
4898 {
4899 if (!defined('PKCS7_TEXT')) {
4900 if ($this->exceptions) {
4901 throw new Exception($this->lang('extension_missing').'openssl');
4902 }
4903
4904 return '';
4905 }
4906 $privKeyStr = !empty($this->DKIM_private_string)
4907 ?
4908 $this->DKIM_private_string
4909 :
4910 file_get_contents($this->DKIM_private);
4911 if ('' !== $this->DKIM_passphrase) {
4912 $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase);
4913 } else {
4914 $privKey = openssl_pkey_get_private($privKeyStr);
4915 }
4916 if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) {
4917 if (\PHP_MAJOR_VERSION < 8) {
4918 openssl_pkey_free($privKey);
4919 }
4920
4921 return base64_encode($signature);
4922 }
4923 if (\PHP_MAJOR_VERSION < 8) {
4924 openssl_pkey_free($privKey);
4925 }
4926
4927 return '';
4928 }
4929
4930 /**
4931 * Generate a DKIM canonicalization header.
4932 * Uses the 'relaxed' algorithm from RFC6376 section 3.4.2.
4933 * Canonicalized headers should *always* use CRLF, regardless of mailer setting.
4934 *
4935 * @see https://tools.ietf.org/html/rfc6376#section-3.4.2
4936 *
4937 * @param string $signHeader Header
4938 *
4939 * @return string
4940 */
4941 public function DKIM_HeaderC($signHeader)
4942 {
4943 //Normalize breaks to CRLF (regardless of the mailer)
4944 $signHeader = static::normalizeBreaks($signHeader, self::CRLF);
4945 //Unfold header lines
4946 //Note PCRE \s is too broad a definition of whitespace; RFC5322 defines it as `[ \t]`
4947 //@see https://tools.ietf.org/html/rfc5322#section-2.2
4948 //That means this may break if you do something daft like put vertical tabs in your headers.
4949 $signHeader = preg_replace('/\r\n[ \t]+/', ' ', $signHeader);
4950 //Break headers out into an array
4951 $lines = explode(self::CRLF, $signHeader);
4952 foreach ($lines as $key => $line) {
4953 //If the header is missing a :, skip it as it's invalid
4954 //This is likely to happen because the explode() above will also split
4955 //on the trailing LE, leaving an empty line
4956 if (strpos($line, ':') === false) {
4957 continue;
4958 }
4959 [$heading, $value] = explode(':', $line, 2);
4960 //Lower-case header name
4961 $heading = strtolower($heading);
4962 //Collapse white space within the value, also convert WSP to space
4963 $value = preg_replace('/[ \t]+/', ' ', $value);
4964 //RFC6376 is slightly unclear here - it says to delete space at the *end* of each value
4965 //But then says to delete space before and after the colon.
4966 //Net result is the same as trimming both ends of the value.
4967 //By elimination, the same applies to the field name
4968 $lines[$key] = trim($heading, " \t").':'.trim($value, " \t");
4969 }
4970
4971 return implode(self::CRLF, $lines);
4972 }
4973
4974 /**
4975 * Generate a DKIM canonicalization body.
4976 * Uses the 'simple' algorithm from RFC6376 section 3.4.3.
4977 * Canonicalized bodies should *always* use CRLF, regardless of mailer setting.
4978 *
4979 * @see https://tools.ietf.org/html/rfc6376#section-3.4.3
4980 *
4981 * @param string $body Message Body
4982 *
4983 * @return string
4984 */
4985 public function DKIM_BodyC($body)
4986 {
4987 if (empty($body)) {
4988 return self::CRLF;
4989 }
4990 //Normalize line endings to CRLF
4991 $body = static::normalizeBreaks($body, self::CRLF);
4992
4993 //Reduce multiple trailing line breaks to a single one
4994 return static::stripTrailingBreaks($body).self::CRLF;
4995 }
4996
4997 /**
4998 * Create the DKIM header and body in a new message header.
4999 *
5000 * @param string $headers_line Header lines
5001 * @param string $subject Subject
5002 * @param string $body Body
5003 *
5004 * @return string
5005 * @throws Exception
5006 *
5007 */
5008 public function DKIM_Add($headers_line, $subject, $body)
5009 {
5010 $DKIMsignatureType = 'rsa-sha256'; //Signature & hash algorithms
5011 $DKIMcanonicalization = 'relaxed/simple'; //Canonicalization methods of header & body
5012 $DKIMquery = 'dns/txt'; //Query method
5013 $DKIMtime = time();
5014 //Always sign these headers without being asked
5015 //Recommended list from https://tools.ietf.org/html/rfc6376#section-5.4.1
5016 $autoSignHeaders = [
5017 'from',
5018 'to',
5019 'cc',
5020 'date',
5021 'subject',
5022 'reply-to',
5023 'message-id',
5024 'content-type',
5025 'mime-version',
5026 'x-mailer',
5027 ];
5028 if (stripos($headers_line, 'Subject') === false) {
5029 $headers_line .= 'Subject: '.$subject.static::$LE;
5030 }
5031 $headerLines = explode(static::$LE, $headers_line);
5032 $currentHeaderLabel = '';
5033 $currentHeaderValue = '';
5034 $parsedHeaders = [];
5035 $headerLineIndex = 0;
5036 $headerLineCount = count($headerLines);
5037 foreach ($headerLines as $headerLine) {
5038 $matches = [];
5039 if (preg_match('/^([^ \t]*?)(?::[ \t]*)(.*)$/', $headerLine, $matches)) {
5040 if ($currentHeaderLabel !== '') {
5041 //We were previously in another header; This is the start of a new header, so save the previous one
5042 $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
5043 }
5044 $currentHeaderLabel = $matches[1];
5045 $currentHeaderValue = $matches[2];
5046 } elseif (preg_match('/^[ \t]+(.*)$/', $headerLine, $matches)) {
5047 //This is a folded continuation of the current header, so unfold it
5048 $currentHeaderValue .= ' '.$matches[1];
5049 }
5050 ++$headerLineIndex;
5051 if ($headerLineIndex >= $headerLineCount) {
5052 //This was the last line, so finish off this header
5053 $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
5054 }
5055 }
5056 $copiedHeaders = [];
5057 $headersToSignKeys = [];
5058 $headersToSign = [];
5059 foreach ($parsedHeaders as $header) {
5060 //Is this header one that must be included in the DKIM signature?
5061 if (in_array(strtolower($header['label']), $autoSignHeaders, true)) {
5062 $headersToSignKeys[] = $header['label'];
5063 $headersToSign[] = $header['label'].': '.$header['value'];
5064 if ($this->DKIM_copyHeaderFields) {
5065 $copiedHeaders[] = $header['label'].':'. //Note no space after this, as per RFC
5066 str_replace('|', '=7C', $this->DKIM_QP($header['value']));
5067 }
5068 continue;
5069 }
5070 //Is this an extra custom header we've been asked to sign?
5071 if (in_array($header['label'], $this->DKIM_extraHeaders, true)) {
5072 //Find its value in custom headers
5073 foreach ($this->CustomHeader as $customHeader) {
5074 if ($customHeader[0] === $header['label']) {
5075 $headersToSignKeys[] = $header['label'];
5076 $headersToSign[] = $header['label'].': '.$header['value'];
5077 if ($this->DKIM_copyHeaderFields) {
5078 $copiedHeaders[] = $header['label'].':'. //Note no space after this, as per RFC
5079 str_replace('|', '=7C', $this->DKIM_QP($header['value']));
5080 }
5081 //Skip straight to the next header
5082 continue 2;
5083 }
5084 }
5085 }
5086 }
5087 $copiedHeaderFields = '';
5088 if ($this->DKIM_copyHeaderFields && count($copiedHeaders) > 0) {
5089 //Assemble a DKIM 'z' tag
5090 $copiedHeaderFields = ' z=';
5091 $first = true;
5092 foreach ($copiedHeaders as $copiedHeader) {
5093 if (!$first) {
5094 $copiedHeaderFields .= static::$LE.' |';
5095 }
5096 //Fold long values
5097 if (strlen($copiedHeader) > self::STD_LINE_LENGTH - 3) {
5098 $copiedHeaderFields .= substr(
5099 chunk_split($copiedHeader, self::STD_LINE_LENGTH - 3, static::$LE.self::FWS),
5100 0,
5101 -strlen(static::$LE.self::FWS)
5102 );
5103 } else {
5104 $copiedHeaderFields .= $copiedHeader;
5105 }
5106 $first = false;
5107 }
5108 $copiedHeaderFields .= ';'.static::$LE;
5109 }
5110 $headerKeys = ' h='.implode(':', $headersToSignKeys).';'.static::$LE;
5111 $headerValues = implode(static::$LE, $headersToSign);
5112 $body = $this->DKIM_BodyC($body);
5113 //Base64 of packed binary SHA-256 hash of body
5114 $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body)));
5115 $ident = '';
5116 if ('' !== $this->DKIM_identity) {
5117 $ident = ' i='.$this->DKIM_identity.';'.static::$LE;
5118 }
5119 //The DKIM-Signature header is included in the signature *except for* the value of the `b` tag
5120 //which is appended after calculating the signature
5121 //https://tools.ietf.org/html/rfc6376#section-3.5
5122 $dkimSignatureHeader = 'DKIM-Signature: v=1;'.
5123 ' d='.$this->DKIM_domain.';'.
5124 ' s='.$this->DKIM_selector.';'.static::$LE.
5125 ' a='.$DKIMsignatureType.';'.
5126 ' q='.$DKIMquery.';'.
5127 ' t='.$DKIMtime.';'.
5128 ' c='.$DKIMcanonicalization.';'.static::$LE.
5129 $headerKeys.
5130 $ident.
5131 $copiedHeaderFields.
5132 ' bh='.$DKIMb64.';'.static::$LE.
5133 ' b=';
5134 //Canonicalize the set of headers
5135 $canonicalizedHeaders = $this->DKIM_HeaderC(
5136 $headerValues.static::$LE.$dkimSignatureHeader
5137 );
5138 $signature = $this->DKIM_Sign($canonicalizedHeaders);
5139 $signature = trim(chunk_split($signature, self::STD_LINE_LENGTH - 3, static::$LE.self::FWS));
5140
5141 return static::normalizeBreaks($dkimSignatureHeader.$signature);
5142 }
5143
5144 /**
5145 * Detect if a string contains a line longer than the maximum line length
5146 * allowed by RFC 2822 section 2.1.1.
5147 *
5148 * @param string $str
5149 *
5150 * @return bool
5151 */
5152 public static function hasLineLongerThanMax($str)
5153 {
5154 return (bool)preg_match('/^(.{'.(self::MAX_LINE_LENGTH + strlen(static::$LE)).',})/m', $str);
5155 }
5156
5157 /**
5158 * If a string contains any "special" characters, double-quote the name,
5159 * and escape any double quotes with a backslash.
5160 *
5161 * @param string $str
5162 *
5163 * @return string
5164 *
5165 * @see RFC822 3.4.1
5166 */
5167 public static function quotedString($str)
5168 {
5169 if (preg_match('/[ ()<>@,;:"\/\[\]?=]/', $str)) {
5170 //If the string contains any of these chars, it must be double-quoted
5171 //and any double quotes must be escaped with a backslash
5172 return '"'.str_replace('"', '\\"', $str).'"';
5173 }
5174
5175 //Return the string untouched, it doesn't need quoting
5176 return $str;
5177 }
5178
5179 /**
5180 * Allows for public read access to 'to' property.
5181 * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
5182 *
5183 * @return array
5184 */
5185 public function getToAddresses()
5186 {
5187 return $this->to;
5188 }
5189
5190 /**
5191 * Allows for public read access to 'cc' property.
5192 * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
5193 *
5194 * @return array
5195 */
5196 public function getCcAddresses()
5197 {
5198 return $this->cc;
5199 }
5200
5201 /**
5202 * Allows for public read access to 'bcc' property.
5203 * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
5204 *
5205 * @return array
5206 */
5207 public function getBccAddresses()
5208 {
5209 return $this->bcc;
5210 }
5211
5212 /**
5213 * Allows for public read access to 'ReplyTo' property.
5214 * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
5215 *
5216 * @return array
5217 */
5218 public function getReplyToAddresses()
5219 {
5220 return $this->ReplyTo;
5221 }
5222
5223 /**
5224 * Allows for public read access to 'all_recipients' property.
5225 * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
5226 *
5227 * @return array
5228 */
5229 public function getAllRecipientAddresses()
5230 {
5231 return $this->all_recipients;
5232 }
5233
5234 /**
5235 * Perform a callback.
5236 *
5237 * @param bool $isSent
5238 * @param array $to
5239 * @param array $cc
5240 * @param array $bcc
5241 * @param string $subject
5242 * @param string $body
5243 * @param string $from
5244 * @param array $extra
5245 */
5246 protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from, $extra)
5247 {
5248 if (!empty($this->action_function) && is_callable($this->action_function)) {
5249 call_user_func($this->action_function, $isSent, $to, $cc, $bcc, $subject, $body, $from, $extra);
5250 }
5251 }
5252
5253 /**
5254 * Get the OAuthTokenProvider instance.
5255 *
5256 * @return OAuthTokenProvider
5257 */
5258 public function getOAuth()
5259 {
5260 return $this->oauth;
5261 }
5262
5263 /**
5264 * Set an OAuthTokenProvider instance.
5265 */
5266 public function setOAuth(OAuthTokenProvider $oauth)
5267 {
5268 $this->oauth = $oauth;
5269 }
5270 }
5271