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