wp/wp-includes/PHPMailer/SMTP.php
changeset 16 a86126ab1dd4
child 18 be944660c56a
equal deleted inserted replaced
15:3d4e9c994f10 16:a86126ab1dd4
       
     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 
       
    21 namespace 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  */
       
    30 class 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 }