[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/PHPMailer/ -> SMTP.php (source)

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


Generated : Sat Jan 31 08:20:07 2026 Cross-referenced by PHPXref