WordPress.org

Make WordPress Core

Ticket #41750: 41750.6.diff

File 41750.6.diff, 410.7 KB (added by desrosj, 16 months ago)
  • phpcs.xml.dist

    diff --git a/phpcs.xml.dist b/phpcs.xml.dist
    index 3718c5aab1..746bb09319 100644
    a b  
    112112        <exclude-pattern>/src/wp-includes/class-IXR\.php</exclude-pattern>
    113113        <exclude-pattern>/src/wp-includes/class-json\.php</exclude-pattern>
    114114        <exclude-pattern>/src/wp-includes/class-phpass\.php</exclude-pattern>
    115         <exclude-pattern>/src/wp-includes/class-phpmailer\.php</exclude-pattern>
    116115        <exclude-pattern>/src/wp-includes/class-pop3\.php</exclude-pattern>
    117116        <exclude-pattern>/src/wp-includes/class-requests\.php</exclude-pattern>
    118117        <exclude-pattern>/src/wp-includes/class-simplepie\.php</exclude-pattern>
    119         <exclude-pattern>/src/wp-includes/class-smtp\.php</exclude-pattern>
     118        <exclude-pattern>/src/wp-includes/PHPMailer/Exception\.php</exclude-pattern>
     119        <exclude-pattern>/src/wp-includes/PHPMailer/PHPMailer\.php</exclude-pattern>
     120        <exclude-pattern>/src/wp-includes/PHPMailer/SMTP\.php</exclude-pattern>
    120121        <exclude-pattern>/src/wp-includes/class-snoopy\.php</exclude-pattern>
    121122        <exclude-pattern>/src/wp-includes/class-wp-block-parser\.php</exclude-pattern>
    122123        <exclude-pattern>/src/wp-includes/deprecated\.php</exclude-pattern>
  • new file src/wp-includes/PHPMailer/Exception.php

    diff --git a/src/wp-includes/PHPMailer/Exception.php b/src/wp-includes/PHPMailer/Exception.php
    new file mode 100644
    index 0000000000..b1e552f50b
    - +  
     1<?php
     2/**
     3 * PHPMailer Exception class.
     4 * PHP Version 5.5.
     5 *
     6 * @see       https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
     7 *
     8 * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
     9 * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>
     10 * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
     11 * @author    Brent R. Matzelle (original founder)
     12 * @copyright 2012 - 2017 Marcus Bointon
     13 * @copyright 2010 - 2012 Jim Jagielski
     14 * @copyright 2004 - 2009 Andy Prevost
     15 * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
     16 * @note      This program is distributed in the hope that it will be useful - WITHOUT
     17 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
     18 * FITNESS FOR A PARTICULAR PURPOSE.
     19 */
     20
     21namespace PHPMailer\PHPMailer;
     22
     23/**
     24 * PHPMailer exception handler.
     25 *
     26 * @author Marcus Bointon <phpmailer@synchromedia.co.uk>
     27 */
     28class Exception extends \Exception
     29{
     30    /**
     31     * Prettify error message output.
     32     *
     33     * @return string
     34     */
     35    public function errorMessage()
     36    {
     37        return '<strong>' . htmlspecialchars($this->getMessage()) . "</strong><br />\n";
     38    }
     39}
  • new file src/wp-includes/PHPMailer/PHPMailer.php

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

    diff --git a/src/wp-includes/PHPMailer/SMTP.php b/src/wp-includes/PHPMailer/SMTP.php
    new file mode 100644
    index 0000000000..aa5555149a
    - +  
     1<?php
     2/**
     3 * PHPMailer RFC821 SMTP email transport class.
     4 * PHP Version 5.5.
     5 *
     6 * @see       https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
     7 *
     8 * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
     9 * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>
     10 * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
     11 * @author    Brent R. Matzelle (original founder)
     12 * @copyright 2012 - 2019 Marcus Bointon
     13 * @copyright 2010 - 2012 Jim Jagielski
     14 * @copyright 2004 - 2009 Andy Prevost
     15 * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
     16 * @note      This program is distributed in the hope that it will be useful - WITHOUT
     17 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
     18 * FITNESS FOR A PARTICULAR PURPOSE.
     19 */
     20
     21namespace PHPMailer\PHPMailer;
     22
     23/**
     24 * PHPMailer RFC821 SMTP email transport class.
     25 * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
     26 *
     27 * @author Chris Ryan
     28 * @author Marcus Bointon <phpmailer@synchromedia.co.uk>
     29 */
     30class SMTP
     31{
     32    /**
     33     * The PHPMailer SMTP version number.
     34     *
     35     * @var string
     36     */
     37    const VERSION = '6.1.6';
     38
     39    /**
     40     * SMTP line break constant.
     41     *
     42     * @var string
     43     */
     44    const LE = "\r\n";
     45
     46    /**
     47     * The SMTP port to use if one is not specified.
     48     *
     49     * @var int
     50     */
     51    const DEFAULT_PORT = 25;
     52
     53    /**
     54     * The maximum line length allowed by RFC 5321 section 4.5.3.1.6,
     55     * *excluding* a trailing CRLF break.
     56     *
     57     * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.6
     58     *
     59     * @var int
     60     */
     61    const MAX_LINE_LENGTH = 998;
     62
     63    /**
     64     * The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5,
     65     * *including* a trailing CRLF line break.
     66     *
     67     * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.5
     68     *
     69     * @var int
     70     */
     71    const MAX_REPLY_LENGTH = 512;
     72
     73    /**
     74     * Debug level for no output.
     75     *
     76     * @var int
     77     */
     78    const DEBUG_OFF = 0;
     79
     80    /**
     81     * Debug level to show client -> server messages.
     82     *
     83     * @var int
     84     */
     85    const DEBUG_CLIENT = 1;
     86
     87    /**
     88     * Debug level to show client -> server and server -> client messages.
     89     *
     90     * @var int
     91     */
     92    const DEBUG_SERVER = 2;
     93
     94    /**
     95     * Debug level to show connection status, client -> server and server -> client messages.
     96     *
     97     * @var int
     98     */
     99    const DEBUG_CONNECTION = 3;
     100
     101    /**
     102     * Debug level to show all messages.
     103     *
     104     * @var int
     105     */
     106    const DEBUG_LOWLEVEL = 4;
     107
     108    /**
     109     * Debug output level.
     110     * Options:
     111     * * self::DEBUG_OFF (`0`) No debug output, default
     112     * * self::DEBUG_CLIENT (`1`) Client commands
     113     * * self::DEBUG_SERVER (`2`) Client commands and server responses
     114     * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status
     115     * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages.
     116     *
     117     * @var int
     118     */
     119    public $do_debug = self::DEBUG_OFF;
     120
     121    /**
     122     * How to handle debug output.
     123     * Options:
     124     * * `echo` Output plain-text as-is, appropriate for CLI
     125     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
     126     * * `error_log` Output to error log as configured in php.ini
     127     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
     128     *
     129     * ```php
     130     * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
     131     * ```
     132     *
     133     * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
     134     * level output is used:
     135     *
     136     * ```php
     137     * $mail->Debugoutput = new myPsr3Logger;
     138     * ```
     139     *
     140     * @var string|callable|\Psr\Log\LoggerInterface
     141     */
     142    public $Debugoutput = 'echo';
     143
     144    /**
     145     * Whether to use VERP.
     146     *
     147     * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path
     148     * @see http://www.postfix.org/VERP_README.html Info on VERP
     149     *
     150     * @var bool
     151     */
     152    public $do_verp = false;
     153
     154    /**
     155     * The timeout value for connection, in seconds.
     156     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
     157     * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
     158     *
     159     * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2
     160     *
     161     * @var int
     162     */
     163    public $Timeout = 300;
     164
     165    /**
     166     * How long to wait for commands to complete, in seconds.
     167     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
     168     *
     169     * @var int
     170     */
     171    public $Timelimit = 300;
     172
     173    /**
     174     * Patterns to extract an SMTP transaction id from reply to a DATA command.
     175     * The first capture group in each regex will be used as the ID.
     176     * MS ESMTP returns the message ID, which may not be correct for internal tracking.
     177     *
     178     * @var string[]
     179     */
     180    protected $smtp_transaction_id_patterns = [
     181        'exim' => '/[\d]{3} OK id=(.*)/',
     182        'sendmail' => '/[\d]{3} 2.0.0 (.*) Message/',
     183        'postfix' => '/[\d]{3} 2.0.0 Ok: queued as (.*)/',
     184        'Microsoft_ESMTP' => '/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/',
     185        'Amazon_SES' => '/[\d]{3} Ok (.*)/',
     186        'SendGrid' => '/[\d]{3} Ok: queued as (.*)/',
     187        'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/',
     188    ];
     189
     190    /**
     191     * The last transaction ID issued in response to a DATA command,
     192     * if one was detected.
     193     *
     194     * @var string|bool|null
     195     */
     196    protected $last_smtp_transaction_id;
     197
     198    /**
     199     * The socket for the server connection.
     200     *
     201     * @var ?resource
     202     */
     203    protected $smtp_conn;
     204
     205    /**
     206     * Error information, if any, for the last SMTP command.
     207     *
     208     * @var array
     209     */
     210    protected $error = [
     211        'error' => '',
     212        'detail' => '',
     213        'smtp_code' => '',
     214        'smtp_code_ex' => '',
     215    ];
     216
     217    /**
     218     * The reply the server sent to us for HELO.
     219     * If null, no HELO string has yet been received.
     220     *
     221     * @var string|null
     222     */
     223    protected $helo_rply;
     224
     225    /**
     226     * The set of SMTP extensions sent in reply to EHLO command.
     227     * Indexes of the array are extension names.
     228     * Value at index 'HELO' or 'EHLO' (according to command that was sent)
     229     * represents the server name. In case of HELO it is the only element of the array.
     230     * Other values can be boolean TRUE or an array containing extension options.
     231     * If null, no HELO/EHLO string has yet been received.
     232     *
     233     * @var array|null
     234     */
     235    protected $server_caps;
     236
     237    /**
     238     * The most recent reply received from the server.
     239     *
     240     * @var string
     241     */
     242    protected $last_reply = '';
     243
     244    /**
     245     * Output debugging info via a user-selected method.
     246     *
     247     * @param string $str   Debug string to output
     248     * @param int    $level The debug level of this message; see DEBUG_* constants
     249     *
     250     * @see SMTP::$Debugoutput
     251     * @see SMTP::$do_debug
     252     */
     253    protected function edebug($str, $level = 0)
     254    {
     255        if ($level > $this->do_debug) {
     256            return;
     257        }
     258        //Is this a PSR-3 logger?
     259        if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
     260            $this->Debugoutput->debug($str);
     261
     262            return;
     263        }
     264        //Avoid clash with built-in function names
     265        if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
     266            call_user_func($this->Debugoutput, $str, $level);
     267
     268            return;
     269        }
     270        switch ($this->Debugoutput) {
     271            case 'error_log':
     272                //Don't output, just log
     273                error_log($str);
     274                break;
     275            case 'html':
     276                //Cleans up output a bit for a better looking, HTML-safe output
     277                echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(
     278                    preg_replace('/[\r\n]+/', '', $str),
     279                    ENT_QUOTES,
     280                    'UTF-8'
     281                ), "<br>\n";
     282                break;
     283            case 'echo':
     284            default:
     285                //Normalize line breaks
     286                $str = preg_replace('/\r\n|\r/m', "\n", $str);
     287                echo gmdate('Y-m-d H:i:s'),
     288                "\t",
     289                    //Trim trailing space
     290                trim(
     291                    //Indent for readability, except for trailing break
     292                    str_replace(
     293                        "\n",
     294                        "\n                   \t                  ",
     295                        trim($str)
     296                    )
     297                ),
     298                "\n";
     299        }
     300    }
     301
     302    /**
     303     * Connect to an SMTP server.
     304     *
     305     * @param string $host    SMTP server IP or host name
     306     * @param int    $port    The port number to connect to
     307     * @param int    $timeout How long to wait for the connection to open
     308     * @param array  $options An array of options for stream_context_create()
     309     *
     310     * @return bool
     311     */
     312    public function connect($host, $port = null, $timeout = 30, $options = [])
     313    {
     314        static $streamok;
     315        //This is enabled by default since 5.0.0 but some providers disable it
     316        //Check this once and cache the result
     317        if (null === $streamok) {
     318            $streamok = function_exists('stream_socket_client');
     319        }
     320        // Clear errors to avoid confusion
     321        $this->setError('');
     322        // Make sure we are __not__ connected
     323        if ($this->connected()) {
     324            // Already connected, generate error
     325            $this->setError('Already connected to a server');
     326
     327            return false;
     328        }
     329        if (empty($port)) {
     330            $port = self::DEFAULT_PORT;
     331        }
     332        // Connect to the SMTP server
     333        $this->edebug(
     334            "Connection: opening to $host:$port, timeout=$timeout, options=" .
     335            (count($options) > 0 ? var_export($options, true) : 'array()'),
     336            self::DEBUG_CONNECTION
     337        );
     338        $errno = 0;
     339        $errstr = '';
     340        if ($streamok) {
     341            $socket_context = stream_context_create($options);
     342            set_error_handler([$this, 'errorHandler']);
     343            $this->smtp_conn = stream_socket_client(
     344                $host . ':' . $port,
     345                $errno,
     346                $errstr,
     347                $timeout,
     348                STREAM_CLIENT_CONNECT,
     349                $socket_context
     350            );
     351            restore_error_handler();
     352        } else {
     353            //Fall back to fsockopen which should work in more places, but is missing some features
     354            $this->edebug(
     355                'Connection: stream_socket_client not available, falling back to fsockopen',
     356                self::DEBUG_CONNECTION
     357            );
     358            set_error_handler([$this, 'errorHandler']);
     359            $this->smtp_conn = fsockopen(
     360                $host,
     361                $port,
     362                $errno,
     363                $errstr,
     364                $timeout
     365            );
     366            restore_error_handler();
     367        }
     368        // Verify we connected properly
     369        if (!is_resource($this->smtp_conn)) {
     370            $this->setError(
     371                'Failed to connect to server',
     372                '',
     373                (string) $errno,
     374                $errstr
     375            );
     376            $this->edebug(
     377                'SMTP ERROR: ' . $this->error['error']
     378                . ": $errstr ($errno)",
     379                self::DEBUG_CLIENT
     380            );
     381
     382            return false;
     383        }
     384        $this->edebug('Connection: opened', self::DEBUG_CONNECTION);
     385        // SMTP server can take longer to respond, give longer timeout for first read
     386        // Windows does not have support for this timeout function
     387        if (strpos(PHP_OS, 'WIN') !== 0) {
     388            $max = (int) ini_get('max_execution_time');
     389            // Don't bother if unlimited
     390            if (0 !== $max && $timeout > $max) {
     391                @set_time_limit($timeout);
     392            }
     393            stream_set_timeout($this->smtp_conn, $timeout, 0);
     394        }
     395        // Get any announcement
     396        $announce = $this->get_lines();
     397        $this->edebug('SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER);
     398
     399        return true;
     400    }
     401
     402    /**
     403     * Initiate a TLS (encrypted) session.
     404     *
     405     * @return bool
     406     */
     407    public function startTLS()
     408    {
     409        if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
     410            return false;
     411        }
     412
     413        //Allow the best TLS version(s) we can
     414        $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
     415
     416        //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
     417        //so add them back in manually if we can
     418        if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
     419            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
     420            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
     421        }
     422
     423        // Begin encrypted connection
     424        set_error_handler([$this, 'errorHandler']);
     425        $crypto_ok = stream_socket_enable_crypto(
     426            $this->smtp_conn,
     427            true,
     428            $crypto_method
     429        );
     430        restore_error_handler();
     431
     432        return (bool) $crypto_ok;
     433    }
     434
     435    /**
     436     * Perform SMTP authentication.
     437     * Must be run after hello().
     438     *
     439     * @see    hello()
     440     *
     441     * @param string $username The user name
     442     * @param string $password The password
     443     * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)
     444     * @param OAuth  $OAuth    An optional OAuth instance for XOAUTH2 authentication
     445     *
     446     * @return bool True if successfully authenticated
     447     */
     448    public function authenticate(
     449        $username,
     450        $password,
     451        $authtype = null,
     452        $OAuth = null
     453    ) {
     454        if (!$this->server_caps) {
     455            $this->setError('Authentication is not allowed before HELO/EHLO');
     456
     457            return false;
     458        }
     459
     460        if (array_key_exists('EHLO', $this->server_caps)) {
     461            // SMTP extensions are available; try to find a proper authentication method
     462            if (!array_key_exists('AUTH', $this->server_caps)) {
     463                $this->setError('Authentication is not allowed at this stage');
     464                // 'at this stage' means that auth may be allowed after the stage changes
     465                // e.g. after STARTTLS
     466
     467                return false;
     468            }
     469
     470            $this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL);
     471            $this->edebug(
     472                'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
     473                self::DEBUG_LOWLEVEL
     474            );
     475
     476            //If we have requested a specific auth type, check the server supports it before trying others
     477            if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) {
     478                $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL);
     479                $authtype = null;
     480            }
     481
     482            if (empty($authtype)) {
     483                //If no auth mechanism is specified, attempt to use these, in this order
     484                //Try CRAM-MD5 first as it's more secure than the others
     485                foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) {
     486                    if (in_array($method, $this->server_caps['AUTH'], true)) {
     487                        $authtype = $method;
     488                        break;
     489                    }
     490                }
     491                if (empty($authtype)) {
     492                    $this->setError('No supported authentication methods found');
     493
     494                    return false;
     495                }
     496                $this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
     497            }
     498
     499            if (!in_array($authtype, $this->server_caps['AUTH'], true)) {
     500                $this->setError("The requested authentication method \"$authtype\" is not supported by the server");
     501
     502                return false;
     503            }
     504        } elseif (empty($authtype)) {
     505            $authtype = 'LOGIN';
     506        }
     507        switch ($authtype) {
     508            case 'PLAIN':
     509                // Start authentication
     510                if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
     511                    return false;
     512                }
     513                // Send encoded username and password
     514                if (!$this->sendCommand(
     515                    'User & Password',
     516                    base64_encode("\0" . $username . "\0" . $password),
     517                    235
     518                )
     519                ) {
     520                    return false;
     521                }
     522                break;
     523            case 'LOGIN':
     524                // Start authentication
     525                if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
     526                    return false;
     527                }
     528                if (!$this->sendCommand('Username', base64_encode($username), 334)) {
     529                    return false;
     530                }
     531                if (!$this->sendCommand('Password', base64_encode($password), 235)) {
     532                    return false;
     533                }
     534                break;
     535            case 'CRAM-MD5':
     536                // Start authentication
     537                if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
     538                    return false;
     539                }
     540                // Get the challenge
     541                $challenge = base64_decode(substr($this->last_reply, 4));
     542
     543                // Build the response
     544                $response = $username . ' ' . $this->hmac($challenge, $password);
     545
     546                // send encoded credentials
     547                return $this->sendCommand('Username', base64_encode($response), 235);
     548            case 'XOAUTH2':
     549                //The OAuth instance must be set up prior to requesting auth.
     550                if (null === $OAuth) {
     551                    return false;
     552                }
     553                $oauth = $OAuth->getOauth64();
     554
     555                // Start authentication
     556                if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
     557                    return false;
     558                }
     559                break;
     560            default:
     561                $this->setError("Authentication method \"$authtype\" is not supported");
     562
     563                return false;
     564        }
     565
     566        return true;
     567    }
     568
     569    /**
     570     * Calculate an MD5 HMAC hash.
     571     * Works like hash_hmac('md5', $data, $key)
     572     * in case that function is not available.
     573     *
     574     * @param string $data The data to hash
     575     * @param string $key  The key to hash with
     576     *
     577     * @return string
     578     */
     579    protected function hmac($data, $key)
     580    {
     581        if (function_exists('hash_hmac')) {
     582            return hash_hmac('md5', $data, $key);
     583        }
     584
     585        // The following borrowed from
     586        // http://php.net/manual/en/function.mhash.php#27225
     587
     588        // RFC 2104 HMAC implementation for php.
     589        // Creates an md5 HMAC.
     590        // Eliminates the need to install mhash to compute a HMAC
     591        // by Lance Rushing
     592
     593        $bytelen = 64; // byte length for md5
     594        if (strlen($key) > $bytelen) {
     595            $key = pack('H*', md5($key));
     596        }
     597        $key = str_pad($key, $bytelen, chr(0x00));
     598        $ipad = str_pad('', $bytelen, chr(0x36));
     599        $opad = str_pad('', $bytelen, chr(0x5c));
     600        $k_ipad = $key ^ $ipad;
     601        $k_opad = $key ^ $opad;
     602
     603        return md5($k_opad . pack('H*', md5($k_ipad . $data)));
     604    }
     605
     606    /**
     607     * Check connection state.
     608     *
     609     * @return bool True if connected
     610     */
     611    public function connected()
     612    {
     613        if (is_resource($this->smtp_conn)) {
     614            $sock_status = stream_get_meta_data($this->smtp_conn);
     615            if ($sock_status['eof']) {
     616                // The socket is valid but we are not connected
     617                $this->edebug(
     618                    'SMTP NOTICE: EOF caught while checking if connected',
     619                    self::DEBUG_CLIENT
     620                );
     621                $this->close();
     622
     623                return false;
     624            }
     625
     626            return true; // everything looks good
     627        }
     628
     629        return false;
     630    }
     631
     632    /**
     633     * Close the socket and clean up the state of the class.
     634     * Don't use this function without first trying to use QUIT.
     635     *
     636     * @see quit()
     637     */
     638    public function close()
     639    {
     640        $this->setError('');
     641        $this->server_caps = null;
     642        $this->helo_rply = null;
     643        if (is_resource($this->smtp_conn)) {
     644            // close the connection and cleanup
     645            fclose($this->smtp_conn);
     646            $this->smtp_conn = null; //Makes for cleaner serialization
     647            $this->edebug('Connection: closed', self::DEBUG_CONNECTION);
     648        }
     649    }
     650
     651    /**
     652     * Send an SMTP DATA command.
     653     * Issues a data command and sends the msg_data to the server,
     654     * finializing the mail transaction. $msg_data is the message
     655     * that is to be send with the headers. Each header needs to be
     656     * on a single line followed by a <CRLF> with the message headers
     657     * and the message body being separated by an additional <CRLF>.
     658     * Implements RFC 821: DATA <CRLF>.
     659     *
     660     * @param string $msg_data Message data to send
     661     *
     662     * @return bool
     663     */
     664    public function data($msg_data)
     665    {
     666        //This will use the standard timelimit
     667        if (!$this->sendCommand('DATA', 'DATA', 354)) {
     668            return false;
     669        }
     670
     671        /* The server is ready to accept data!
     672         * According to rfc821 we should not send more than 1000 characters on a single line (including the LE)
     673         * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
     674         * smaller lines to fit within the limit.
     675         * We will also look for lines that start with a '.' and prepend an additional '.'.
     676         * NOTE: this does not count towards line-length limit.
     677         */
     678
     679        // Normalize line breaks before exploding
     680        $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data));
     681
     682        /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
     683         * of the first line (':' separated) does not contain a space then it _should_ be a header and we will
     684         * process all lines before a blank line as headers.
     685         */
     686
     687        $field = substr($lines[0], 0, strpos($lines[0], ':'));
     688        $in_headers = false;
     689        if (!empty($field) && strpos($field, ' ') === false) {
     690            $in_headers = true;
     691        }
     692
     693        foreach ($lines as $line) {
     694            $lines_out = [];
     695            if ($in_headers && $line === '') {
     696                $in_headers = false;
     697            }
     698            //Break this line up into several smaller lines if it's too long
     699            //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
     700            while (isset($line[self::MAX_LINE_LENGTH])) {
     701                //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
     702                //so as to avoid breaking in the middle of a word
     703                $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
     704                //Deliberately matches both false and 0
     705                if (!$pos) {
     706                    //No nice break found, add a hard break
     707                    $pos = self::MAX_LINE_LENGTH - 1;
     708                    $lines_out[] = substr($line, 0, $pos);
     709                    $line = substr($line, $pos);
     710                } else {
     711                    //Break at the found point
     712                    $lines_out[] = substr($line, 0, $pos);
     713                    //Move along by the amount we dealt with
     714                    $line = substr($line, $pos + 1);
     715                }
     716                //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
     717                if ($in_headers) {
     718                    $line = "\t" . $line;
     719                }
     720            }
     721            $lines_out[] = $line;
     722
     723            //Send the lines to the server
     724            foreach ($lines_out as $line_out) {
     725                //RFC2821 section 4.5.2
     726                if (!empty($line_out) && $line_out[0] === '.') {
     727                    $line_out = '.' . $line_out;
     728                }
     729                $this->client_send($line_out . static::LE, 'DATA');
     730            }
     731        }
     732
     733        //Message data has been sent, complete the command
     734        //Increase timelimit for end of DATA command
     735        $savetimelimit = $this->Timelimit;
     736        $this->Timelimit *= 2;
     737        $result = $this->sendCommand('DATA END', '.', 250);
     738        $this->recordLastTransactionID();
     739        //Restore timelimit
     740        $this->Timelimit = $savetimelimit;
     741
     742        return $result;
     743    }
     744
     745    /**
     746     * Send an SMTP HELO or EHLO command.
     747     * Used to identify the sending server to the receiving server.
     748     * This makes sure that client and server are in a known state.
     749     * Implements RFC 821: HELO <SP> <domain> <CRLF>
     750     * and RFC 2821 EHLO.
     751     *
     752     * @param string $host The host name or IP to connect to
     753     *
     754     * @return bool
     755     */
     756    public function hello($host = '')
     757    {
     758        //Try extended hello first (RFC 2821)
     759        return $this->sendHello('EHLO', $host) or $this->sendHello('HELO', $host);
     760    }
     761
     762    /**
     763     * Send an SMTP HELO or EHLO command.
     764     * Low-level implementation used by hello().
     765     *
     766     * @param string $hello The HELO string
     767     * @param string $host  The hostname to say we are
     768     *
     769     * @return bool
     770     *
     771     * @see hello()
     772     */
     773    protected function sendHello($hello, $host)
     774    {
     775        $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
     776        $this->helo_rply = $this->last_reply;
     777        if ($noerror) {
     778            $this->parseHelloFields($hello);
     779        } else {
     780            $this->server_caps = null;
     781        }
     782
     783        return $noerror;
     784    }
     785
     786    /**
     787     * Parse a reply to HELO/EHLO command to discover server extensions.
     788     * In case of HELO, the only parameter that can be discovered is a server name.
     789     *
     790     * @param string $type `HELO` or `EHLO`
     791     */
     792    protected function parseHelloFields($type)
     793    {
     794        $this->server_caps = [];
     795        $lines = explode("\n", $this->helo_rply);
     796
     797        foreach ($lines as $n => $s) {
     798            //First 4 chars contain response code followed by - or space
     799            $s = trim(substr($s, 4));
     800            if (empty($s)) {
     801                continue;
     802            }
     803            $fields = explode(' ', $s);
     804            if (!empty($fields)) {
     805                if (!$n) {
     806                    $name = $type;
     807                    $fields = $fields[0];
     808                } else {
     809                    $name = array_shift($fields);
     810                    switch ($name) {
     811                        case 'SIZE':
     812                            $fields = ($fields ? $fields[0] : 0);
     813                            break;
     814                        case 'AUTH':
     815                            if (!is_array($fields)) {
     816                                $fields = [];
     817                            }
     818                            break;
     819                        default:
     820                            $fields = true;
     821                    }
     822                }
     823                $this->server_caps[$name] = $fields;
     824            }
     825        }
     826    }
     827
     828    /**
     829     * Send an SMTP MAIL command.
     830     * Starts a mail transaction from the email address specified in
     831     * $from. Returns true if successful or false otherwise. If True
     832     * the mail transaction is started and then one or more recipient
     833     * commands may be called followed by a data command.
     834     * Implements RFC 821: MAIL <SP> FROM:<reverse-path> <CRLF>.
     835     *
     836     * @param string $from Source address of this message
     837     *
     838     * @return bool
     839     */
     840    public function mail($from)
     841    {
     842        $useVerp = ($this->do_verp ? ' XVERP' : '');
     843
     844        return $this->sendCommand(
     845            'MAIL FROM',
     846            'MAIL FROM:<' . $from . '>' . $useVerp,
     847            250
     848        );
     849    }
     850
     851    /**
     852     * Send an SMTP QUIT command.
     853     * Closes the socket if there is no error or the $close_on_error argument is true.
     854     * Implements from RFC 821: QUIT <CRLF>.
     855     *
     856     * @param bool $close_on_error Should the connection close if an error occurs?
     857     *
     858     * @return bool
     859     */
     860    public function quit($close_on_error = true)
     861    {
     862        $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
     863        $err = $this->error; //Save any error
     864        if ($noerror || $close_on_error) {
     865            $this->close();
     866            $this->error = $err; //Restore any error from the quit command
     867        }
     868
     869        return $noerror;
     870    }
     871
     872    /**
     873     * Send an SMTP RCPT command.
     874     * Sets the TO argument to $toaddr.
     875     * Returns true if the recipient was accepted false if it was rejected.
     876     * Implements from RFC 821: RCPT <SP> TO:<forward-path> <CRLF>.
     877     *
     878     * @param string $address The address the message is being sent to
     879     * @param string $dsn     Comma separated list of DSN notifications. NEVER, SUCCESS, FAILURE
     880     *                        or DELAY. If you specify NEVER all other notifications are ignored.
     881     *
     882     * @return bool
     883     */
     884    public function recipient($address, $dsn = '')
     885    {
     886        if (empty($dsn)) {
     887            $rcpt = 'RCPT TO:<' . $address . '>';
     888        } else {
     889            $dsn = strtoupper($dsn);
     890            $notify = [];
     891
     892            if (strpos($dsn, 'NEVER') !== false) {
     893                $notify[] = 'NEVER';
     894            } else {
     895                foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) {
     896                    if (strpos($dsn, $value) !== false) {
     897                        $notify[] = $value;
     898                    }
     899                }
     900            }
     901
     902            $rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . implode(',', $notify);
     903        }
     904
     905        return $this->sendCommand(
     906            'RCPT TO',
     907            $rcpt,
     908            [250, 251]
     909        );
     910    }
     911
     912    /**
     913     * Send an SMTP RSET command.
     914     * Abort any transaction that is currently in progress.
     915     * Implements RFC 821: RSET <CRLF>.
     916     *
     917     * @return bool True on success
     918     */
     919    public function reset()
     920    {
     921        return $this->sendCommand('RSET', 'RSET', 250);
     922    }
     923
     924    /**
     925     * Send a command to an SMTP server and check its return code.
     926     *
     927     * @param string    $command       The command name - not sent to the server
     928     * @param string    $commandstring The actual command to send
     929     * @param int|array $expect        One or more expected integer success codes
     930     *
     931     * @return bool True on success
     932     */
     933    protected function sendCommand($command, $commandstring, $expect)
     934    {
     935        if (!$this->connected()) {
     936            $this->setError("Called $command without being connected");
     937
     938            return false;
     939        }
     940        //Reject line breaks in all commands
     941        if ((strpos($commandstring, "\n") !== false) || (strpos($commandstring, "\r") !== false)) {
     942            $this->setError("Command '$command' contained line breaks");
     943
     944            return false;
     945        }
     946        $this->client_send($commandstring . static::LE, $command);
     947
     948        $this->last_reply = $this->get_lines();
     949        // Fetch SMTP code and possible error code explanation
     950        $matches = [];
     951        if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) {
     952            $code = (int) $matches[1];
     953            $code_ex = (count($matches) > 2 ? $matches[2] : null);
     954            // Cut off error code from each response line
     955            $detail = preg_replace(
     956                "/{$code}[ -]" .
     957                ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m',
     958                '',
     959                $this->last_reply
     960            );
     961        } else {
     962            // Fall back to simple parsing if regex fails
     963            $code = (int) substr($this->last_reply, 0, 3);
     964            $code_ex = null;
     965            $detail = substr($this->last_reply, 4);
     966        }
     967
     968        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
     969
     970        if (!in_array($code, (array) $expect, true)) {
     971            $this->setError(
     972                "$command command failed",
     973                $detail,
     974                $code,
     975                $code_ex
     976            );
     977            $this->edebug(
     978                'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
     979                self::DEBUG_CLIENT
     980            );
     981
     982            return false;
     983        }
     984
     985        $this->setError('');
     986
     987        return true;
     988    }
     989
     990    /**
     991     * Send an SMTP SAML command.
     992     * Starts a mail transaction from the email address specified in $from.
     993     * Returns true if successful or false otherwise. If True
     994     * the mail transaction is started and then one or more recipient
     995     * commands may be called followed by a data command. This command
     996     * will send the message to the users terminal if they are logged
     997     * in and send them an email.
     998     * Implements RFC 821: SAML <SP> FROM:<reverse-path> <CRLF>.
     999     *
     1000     * @param string $from The address the message is from
     1001     *
     1002     * @return bool
     1003     */
     1004    public function sendAndMail($from)
     1005    {
     1006        return $this->sendCommand('SAML', "SAML FROM:$from", 250);
     1007    }
     1008
     1009    /**
     1010     * Send an SMTP VRFY command.
     1011     *
     1012     * @param string $name The name to verify
     1013     *
     1014     * @return bool
     1015     */
     1016    public function verify($name)
     1017    {
     1018        return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
     1019    }
     1020
     1021    /**
     1022     * Send an SMTP NOOP command.
     1023     * Used to keep keep-alives alive, doesn't actually do anything.
     1024     *
     1025     * @return bool
     1026     */
     1027    public function noop()
     1028    {
     1029        return $this->sendCommand('NOOP', 'NOOP', 250);
     1030    }
     1031
     1032    /**
     1033     * Send an SMTP TURN command.
     1034     * This is an optional command for SMTP that this class does not support.
     1035     * This method is here to make the RFC821 Definition complete for this class
     1036     * and _may_ be implemented in future.
     1037     * Implements from RFC 821: TURN <CRLF>.
     1038     *
     1039     * @return bool
     1040     */
     1041    public function turn()
     1042    {
     1043        $this->setError('The SMTP TURN command is not implemented');
     1044        $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
     1045
     1046        return false;
     1047    }
     1048
     1049    /**
     1050     * Send raw data to the server.
     1051     *
     1052     * @param string $data    The data to send
     1053     * @param string $command Optionally, the command this is part of, used only for controlling debug output
     1054     *
     1055     * @return int|bool The number of bytes sent to the server or false on error
     1056     */
     1057    public function client_send($data, $command = '')
     1058    {
     1059        //If SMTP transcripts are left enabled, or debug output is posted online
     1060        //it can leak credentials, so hide credentials in all but lowest level
     1061        if (self::DEBUG_LOWLEVEL > $this->do_debug &&
     1062            in_array($command, ['User & Password', 'Username', 'Password'], true)) {
     1063            $this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT);
     1064        } else {
     1065            $this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT);
     1066        }
     1067        set_error_handler([$this, 'errorHandler']);
     1068        $result = fwrite($this->smtp_conn, $data);
     1069        restore_error_handler();
     1070
     1071        return $result;
     1072    }
     1073
     1074    /**
     1075     * Get the latest error.
     1076     *
     1077     * @return array
     1078     */
     1079    public function getError()
     1080    {
     1081        return $this->error;
     1082    }
     1083
     1084    /**
     1085     * Get SMTP extensions available on the server.
     1086     *
     1087     * @return array|null
     1088     */
     1089    public function getServerExtList()
     1090    {
     1091        return $this->server_caps;
     1092    }
     1093
     1094    /**
     1095     * Get metadata about the SMTP server from its HELO/EHLO response.
     1096     * The method works in three ways, dependent on argument value and current state:
     1097     *   1. HELO/EHLO has not been sent - returns null and populates $this->error.
     1098     *   2. HELO has been sent -
     1099     *     $name == 'HELO': returns server name
     1100     *     $name == 'EHLO': returns boolean false
     1101     *     $name == any other string: returns null and populates $this->error
     1102     *   3. EHLO has been sent -
     1103     *     $name == 'HELO'|'EHLO': returns the server name
     1104     *     $name == any other string: if extension $name exists, returns True
     1105     *       or its options (e.g. AUTH mechanisms supported). Otherwise returns False.
     1106     *
     1107     * @param string $name Name of SMTP extension or 'HELO'|'EHLO'
     1108     *
     1109     * @return string|bool|null
     1110     */
     1111    public function getServerExt($name)
     1112    {
     1113        if (!$this->server_caps) {
     1114            $this->setError('No HELO/EHLO was sent');
     1115
     1116            return;
     1117        }
     1118
     1119        if (!array_key_exists($name, $this->server_caps)) {
     1120            if ('HELO' === $name) {
     1121                return $this->server_caps['EHLO'];
     1122            }
     1123            if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) {
     1124                return false;
     1125            }
     1126            $this->setError('HELO handshake was used; No information about server extensions available');
     1127
     1128            return;
     1129        }
     1130
     1131        return $this->server_caps[$name];
     1132    }
     1133
     1134    /**
     1135     * Get the last reply from the server.
     1136     *
     1137     * @return string
     1138     */
     1139    public function getLastReply()
     1140    {
     1141        return $this->last_reply;
     1142    }
     1143
     1144    /**
     1145     * Read the SMTP server's response.
     1146     * Either before eof or socket timeout occurs on the operation.
     1147     * With SMTP we can tell if we have more lines to read if the
     1148     * 4th character is '-' symbol. If it is a space then we don't
     1149     * need to read anything else.
     1150     *
     1151     * @return string
     1152     */
     1153    protected function get_lines()
     1154    {
     1155        // If the connection is bad, give up straight away
     1156        if (!is_resource($this->smtp_conn)) {
     1157            return '';
     1158        }
     1159        $data = '';
     1160        $endtime = 0;
     1161        stream_set_timeout($this->smtp_conn, $this->Timeout);
     1162        if ($this->Timelimit > 0) {
     1163            $endtime = time() + $this->Timelimit;
     1164        }
     1165        $selR = [$this->smtp_conn];
     1166        $selW = null;
     1167        while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
     1168            //Must pass vars in here as params are by reference
     1169            if (!stream_select($selR, $selW, $selW, $this->Timelimit)) {
     1170                $this->edebug(
     1171                    'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit . ' sec)',
     1172                    self::DEBUG_LOWLEVEL
     1173                );
     1174                break;
     1175            }
     1176            //Deliberate noise suppression - errors are handled afterwards
     1177            $str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH);
     1178            $this->edebug('SMTP INBOUND: "' . trim($str) . '"', self::DEBUG_LOWLEVEL);
     1179            $data .= $str;
     1180            // If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
     1181            // or 4th character is a space or a line break char, we are done reading, break the loop.
     1182            // String array access is a significant micro-optimisation over strlen
     1183            if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") {
     1184                break;
     1185            }
     1186            // Timed-out? Log and break
     1187            $info = stream_get_meta_data($this->smtp_conn);
     1188            if ($info['timed_out']) {
     1189                $this->edebug(
     1190                    'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)',
     1191                    self::DEBUG_LOWLEVEL
     1192                );
     1193                break;
     1194            }
     1195            // Now check if reads took too long
     1196            if ($endtime && time() > $endtime) {
     1197                $this->edebug(
     1198                    'SMTP -> get_lines(): timelimit reached (' .
     1199                    $this->Timelimit . ' sec)',
     1200                    self::DEBUG_LOWLEVEL
     1201                );
     1202                break;
     1203            }
     1204        }
     1205
     1206        return $data;
     1207    }
     1208
     1209    /**
     1210     * Enable or disable VERP address generation.
     1211     *
     1212     * @param bool $enabled
     1213     */
     1214    public function setVerp($enabled = false)
     1215    {
     1216        $this->do_verp = $enabled;
     1217    }
     1218
     1219    /**
     1220     * Get VERP address generation mode.
     1221     *
     1222     * @return bool
     1223     */
     1224    public function getVerp()
     1225    {
     1226        return $this->do_verp;
     1227    }
     1228
     1229    /**
     1230     * Set error messages and codes.
     1231     *
     1232     * @param string $message      The error message
     1233     * @param string $detail       Further detail on the error
     1234     * @param string $smtp_code    An associated SMTP error code
     1235     * @param string $smtp_code_ex Extended SMTP code
     1236     */
     1237    protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
     1238    {
     1239        $this->error = [
     1240            'error' => $message,
     1241            'detail' => $detail,
     1242            'smtp_code' => $smtp_code,
     1243            'smtp_code_ex' => $smtp_code_ex,
     1244        ];
     1245    }
     1246
     1247    /**
     1248     * Set debug output method.
     1249     *
     1250     * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it
     1251     */
     1252    public function setDebugOutput($method = 'echo')
     1253    {
     1254        $this->Debugoutput = $method;
     1255    }
     1256
     1257    /**
     1258     * Get debug output method.
     1259     *
     1260     * @return string
     1261     */
     1262    public function getDebugOutput()
     1263    {
     1264        return $this->Debugoutput;
     1265    }
     1266
     1267    /**
     1268     * Set debug output level.
     1269     *
     1270     * @param int $level
     1271     */
     1272    public function setDebugLevel($level = 0)
     1273    {
     1274        $this->do_debug = $level;
     1275    }
     1276
     1277    /**
     1278     * Get debug output level.
     1279     *
     1280     * @return int
     1281     */
     1282    public function getDebugLevel()
     1283    {
     1284        return $this->do_debug;
     1285    }
     1286
     1287    /**
     1288     * Set SMTP timeout.
     1289     *
     1290     * @param int $timeout The timeout duration in seconds
     1291     */
     1292    public function setTimeout($timeout = 0)
     1293    {
     1294        $this->Timeout = $timeout;
     1295    }
     1296
     1297    /**
     1298     * Get SMTP timeout.
     1299     *
     1300     * @return int
     1301     */
     1302    public function getTimeout()
     1303    {
     1304        return $this->Timeout;
     1305    }
     1306
     1307    /**
     1308     * Reports an error number and string.
     1309     *
     1310     * @param int    $errno   The error number returned by PHP
     1311     * @param string $errmsg  The error message returned by PHP
     1312     * @param string $errfile The file the error occurred in
     1313     * @param int    $errline The line number the error occurred on
     1314     */
     1315    protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
     1316    {
     1317        $notice = 'Connection failed.';
     1318        $this->setError(
     1319            $notice,
     1320            $errmsg,
     1321            (string) $errno
     1322        );
     1323        $this->edebug(
     1324            "$notice Error #$errno: $errmsg [$errfile line $errline]",
     1325            self::DEBUG_CONNECTION
     1326        );
     1327    }
     1328
     1329    /**
     1330     * Extract and return the ID of the last SMTP transaction based on
     1331     * a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
     1332     * Relies on the host providing the ID in response to a DATA command.
     1333     * If no reply has been received yet, it will return null.
     1334     * If no pattern was matched, it will return false.
     1335     *
     1336     * @return bool|string|null
     1337     */
     1338    protected function recordLastTransactionID()
     1339    {
     1340        $reply = $this->getLastReply();
     1341
     1342        if (empty($reply)) {
     1343            $this->last_smtp_transaction_id = null;
     1344        } else {
     1345            $this->last_smtp_transaction_id = false;
     1346            foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
     1347                $matches = [];
     1348                if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
     1349                    $this->last_smtp_transaction_id = trim($matches[1]);
     1350                    break;
     1351                }
     1352            }
     1353        }
     1354
     1355        return $this->last_smtp_transaction_id;
     1356    }
     1357
     1358    /**
     1359     * Get the queue/transaction ID of the last SMTP transaction
     1360     * If no reply has been received yet, it will return null.
     1361     * If no pattern was matched, it will return false.
     1362     *
     1363     * @return bool|string|null
     1364     *
     1365     * @see recordLastTransactionID()
     1366     */
     1367    public function getLastTransactionID()
     1368    {
     1369        return $this->last_smtp_transaction_id;
     1370    }
     1371}
  • src/wp-includes/class-phpmailer.php

    diff --git a/src/wp-includes/class-phpmailer.php b/src/wp-includes/class-phpmailer.php
    index 30754a4e6c..b82f7ff613 100644
    a b