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

Erat himenaeos neque id sagittis massa. Hac suscipit pulvinar dignissim platea magnis eu. Don tellus a pharetra inceptos efficitur dui pulvinar. Feugiat facilisis penatibus pulvinar nunc dictumst donec odio platea habitasse. Lacus porta dolor purus elit ante bibendum tortor netus taciti nullam cubilia. Erat per suspendisse placerat morbi egestas pulvinar bibendum sollicitudin nec. Euismod cubilia eleifend velit himenaeos sodales lectus. Leo maximus cras ac porttitor aliquam torquent pulvinar odio volutpat parturient. Quisque risus finibus suspendisse mus purus magnis facilisi condimentum consectetur dui. Curae elit suspendisse cursus vehicula.

Turpis taciti class non vel pretium quis pulvinar tempor lobortis nunc. Libero phasellus parturient sapien volutpat malesuada ornare. Cubilia dignissim sollicitudin rhoncus lacinia maximus. Cras lorem fermentum bibendum pellentesque nisl etiam ligula enim cubilia. Vulputate pede sapien torquent montes tempus malesuada in mattis dis turpis vitae. Porta est tempor ex eget feugiat vulputate ipsum. Justo nec iaculis habitant diam arcu fermentum.

We offer comprehen sive emplo ment services such as assistance wit employer compliance.Our company is your strategic HR partner as instead of HR. john smithson

Cubilia dignissim sollicitudin rhoncus lacinia maximus. Cras lorem fermentum bibendum pellentesque nisl etiam ligula enim cubilia. Vulputate pede sapien torquent montes tempus malesuada in mattis dis turpis vitae.

Exploring Learning Landscapes in Academic

Feugiat facilisis penatibus pulvinar nunc dictumst donec odio platea habitasse. Lacus porta dolor purus elit ante bibendum tortor netus taciti nullam cubilia. Erat per suspendisse placerat morbi egestas pulvinar bibendum sollicitudin nec. Euismod cubilia eleifend velit himenaeos sodales lectus. Leo maximus cras ac porttitor aliquam torquent.

]]>
https://cardgames4educators.com/masters-in-english-how-english-speaker/feed/ 1