[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

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

   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  }


Generated: Sun Aug 9 01:00:03 2020 Cross-referenced by PHPXref 0.7.1