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