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