"Fossies" - the Fresh Open Source Software Archive

Member "PHPMailer-6.4.1/src/SMTP.php" (29 Apr 2021, 47228 Bytes) of package /linux/www/PHPMailer-6.4.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "SMTP.php" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 6.4.0_vs_6.4.1.

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