[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

title

Body

[close]

/wp-includes/ -> class-smtp.php (source)

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


Generated: Fri Oct 25 08:20:01 2019 Cross-referenced by PHPXref 0.7