1<?php 2/** vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */ 3// +----------------------------------------------------------------------+ 4// | PHP Version 5 and 7 | 5// +----------------------------------------------------------------------+ 6// | Copyright (c) 1997-2019 Jon Parise and Chuck Hagenbuch | 7// | All rights reserved. | 8// | | 9// | Redistribution and use in source and binary forms, with or without | 10// | modification, are permitted provided that the following conditions | 11// | are met: | 12// | | 13// | 1. Redistributions of source code must retain the above copyright | 14// | notice, this list of conditions and the following disclaimer. | 15// | | 16// | 2. Redistributions in binary form must reproduce the above copyright | 17// | notice, this list of conditions and the following disclaimer in | 18// | the documentation and/or other materials provided with the | 19// | distribution. | 20// | | 21// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | 22// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | 23// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS | 24// | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE | 25// | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, | 26// | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, | 27// | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | 28// | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | 29// | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | 30// | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN | 31// | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | 32// | POSSIBILITY OF SUCH DAMAGE. | 33// +----------------------------------------------------------------------+ 34// | Authors: Chuck Hagenbuch <chuck@horde.org> | 35// | Jon Parise <jon@php.net> | 36// | Damian Alejandro Fernandez Sosa <damlists@cnba.uba.ar> | 37// +----------------------------------------------------------------------+ 38 39require_once 'PEAR.php'; 40require_once 'Net/Socket.php'; 41 42/** 43 * Provides an implementation of the SMTP protocol using PEAR's 44 * Net_Socket class. 45 * 46 * @package Net_SMTP 47 * @author Chuck Hagenbuch <chuck@horde.org> 48 * @author Jon Parise <jon@php.net> 49 * @author Damian Alejandro Fernandez Sosa <damlists@cnba.uba.ar> 50 * @license http://opensource.org/licenses/bsd-license.php BSD-2-Clause 51 * 52 * @example basic.php A basic implementation of the Net_SMTP package. 53 */ 54class Net_SMTP 55{ 56 /** 57 * The server to connect to. 58 * @var string 59 */ 60 public $host = 'localhost'; 61 62 /** 63 * The port to connect to. 64 * @var int 65 */ 66 public $port = 25; 67 68 /** 69 * The value to give when sending EHLO or HELO. 70 * @var string 71 */ 72 public $localhost = 'localhost'; 73 74 /** 75 * List of supported authentication methods, in preferential order. 76 * @var array 77 */ 78 public $auth_methods = array(); 79 80 /** 81 * Use SMTP command pipelining (specified in RFC 2920) if the SMTP 82 * server supports it. 83 * 84 * When pipeling is enabled, rcptTo(), mailFrom(), sendFrom(), 85 * somlFrom() and samlFrom() do not wait for a response from the 86 * SMTP server but return immediately. 87 * 88 * @var bool 89 */ 90 public $pipelining = false; 91 92 /** 93 * Number of pipelined commands. 94 * @var int 95 */ 96 protected $pipelined_commands = 0; 97 98 /** 99 * Should debugging output be enabled? 100 * @var boolean 101 */ 102 protected $debug = false; 103 104 /** 105 * Debug output handler. 106 * @var callback 107 */ 108 protected $debug_handler = null; 109 110 /** 111 * The socket resource being used to connect to the SMTP server. 112 * @var resource 113 */ 114 protected $socket = null; 115 116 /** 117 * Array of socket options that will be passed to Net_Socket::connect(). 118 * @see stream_context_create() 119 * @var array 120 */ 121 protected $socket_options = null; 122 123 /** 124 * The socket I/O timeout value in seconds. 125 * @var int 126 */ 127 protected $timeout = 0; 128 129 /** 130 * The most recent server response code. 131 * @var int 132 */ 133 protected $code = -1; 134 135 /** 136 * The most recent server response arguments. 137 * @var array 138 */ 139 protected $arguments = array(); 140 141 /** 142 * Stores the SMTP server's greeting string. 143 * @var string 144 */ 145 protected $greeting = null; 146 147 /** 148 * Stores detected features of the SMTP server. 149 * @var array 150 */ 151 protected $esmtp = array(); 152 153 /** 154 * Instantiates a new Net_SMTP object, overriding any defaults 155 * with parameters that are passed in. 156 * 157 * If you have SSL support in PHP, you can connect to a server 158 * over SSL using an 'ssl://' prefix: 159 * 160 * // 465 is a common smtps port. 161 * $smtp = new Net_SMTP('ssl://mail.host.com', 465); 162 * $smtp->connect(); 163 * 164 * @param string $host The server to connect to. 165 * @param integer $port The port to connect to. 166 * @param string $localhost The value to give when sending EHLO or HELO. 167 * @param boolean $pipelining Use SMTP command pipelining 168 * @param integer $timeout Socket I/O timeout in seconds. 169 * @param array $socket_options Socket stream_context_create() options. 170 * @param string $gssapi_principal GSSAPI service principal name 171 * @param string $gssapi_cname GSSAPI credentials cache 172 * 173 * @since 1.0 174 */ 175 public function __construct($host = null, $port = null, $localhost = null, 176 $pipelining = false, $timeout = 0, $socket_options = null, 177 $gssapi_principal=null, $gssapi_cname=null 178 ) { 179 if (isset($host)) { 180 $this->host = $host; 181 } 182 if (isset($port)) { 183 $this->port = $port; 184 } 185 if (isset($localhost)) { 186 $this->localhost = $localhost; 187 } 188 189 $this->pipelining = $pipelining; 190 $this->socket = new Net_Socket(); 191 $this->socket_options = $socket_options; 192 $this->timeout = $timeout; 193 $this->gssapi_principal = $gssapi_principal; 194 $this->gssapi_cname = $gssapi_cname; 195 196 /* If PHP krb5 extension is loaded, we enable GSSAPI method. */ 197 if (extension_loaded('krb5')) { 198 $this->setAuthMethod('GSSAPI', array($this, 'authGSSAPI')); 199 } 200 201 /* Include the Auth_SASL package. If the package is available, we 202 * enable the authentication methods that depend upon it. */ 203 if (@include_once 'Auth/SASL.php') { 204 $this->setAuthMethod('CRAM-MD5', array($this, 'authCramMD5')); 205 $this->setAuthMethod('DIGEST-MD5', array($this, 'authDigestMD5')); 206 } 207 208 /* These standard authentication methods are always available. */ 209 $this->setAuthMethod('LOGIN', array($this, 'authLogin'), false); 210 $this->setAuthMethod('PLAIN', array($this, 'authPlain'), false); 211 $this->setAuthMethod('XOAUTH2', array($this, 'authXOAuth2'), false); 212 } 213 214 /** 215 * Set the socket I/O timeout value in seconds plus microseconds. 216 * 217 * @param integer $seconds Timeout value in seconds. 218 * @param integer $microseconds Additional value in microseconds. 219 * 220 * @since 1.5.0 221 */ 222 public function setTimeout($seconds, $microseconds = 0) 223 { 224 return $this->socket->setTimeout($seconds, $microseconds); 225 } 226 227 /** 228 * Set the value of the debugging flag. 229 * 230 * @param boolean $debug New value for the debugging flag. 231 * @param callback $handler Debug handler callback 232 * 233 * @since 1.1.0 234 */ 235 public function setDebug($debug, $handler = null) 236 { 237 $this->debug = $debug; 238 $this->debug_handler = $handler; 239 } 240 241 /** 242 * Write the given debug text to the current debug output handler. 243 * 244 * @param string $message Debug mesage text. 245 * 246 * @since 1.3.3 247 */ 248 protected function debug($message) 249 { 250 if ($this->debug) { 251 if ($this->debug_handler) { 252 call_user_func_array( 253 $this->debug_handler, array(&$this, $message) 254 ); 255 } else { 256 echo "DEBUG: $message\n"; 257 } 258 } 259 } 260 261 /** 262 * Send the given string of data to the server. 263 * 264 * @param string $data The string of data to send. 265 * 266 * @return mixed The number of bytes that were actually written, 267 * or a PEAR_Error object on failure. 268 * 269 * @since 1.1.0 270 */ 271 protected function send($data) 272 { 273 $this->debug("Send: $data"); 274 275 $result = $this->socket->write($data); 276 if (!$result || PEAR::isError($result)) { 277 $msg = $result ? $result->getMessage() : "unknown error"; 278 return PEAR::raiseError("Failed to write to socket: $msg"); 279 } 280 281 return $result; 282 } 283 284 /** 285 * Send a command to the server with an optional string of 286 * arguments. A carriage return / linefeed (CRLF) sequence will 287 * be appended to each command string before it is sent to the 288 * SMTP server - an error will be thrown if the command string 289 * already contains any newline characters. Use send() for 290 * commands that must contain newlines. 291 * 292 * @param string $command The SMTP command to send to the server. 293 * @param string $args A string of optional arguments to append 294 * to the command. 295 * 296 * @return mixed The result of the send() call. 297 * 298 * @since 1.1.0 299 */ 300 protected function put($command, $args = '') 301 { 302 if (!empty($args)) { 303 $command .= ' ' . $args; 304 } 305 306 if (strcspn($command, "\r\n") !== strlen($command)) { 307 return PEAR::raiseError('Commands cannot contain newlines'); 308 } 309 310 return $this->send($command . "\r\n"); 311 } 312 313 /** 314 * Read a reply from the SMTP server. The reply consists of a response 315 * code and a response message. 316 * 317 * @param mixed $valid The set of valid response codes. These 318 * may be specified as an array of integer 319 * values or as a single integer value. 320 * @param bool $later Do not parse the response now, but wait 321 * until the last command in the pipelined 322 * command group 323 * 324 * @return mixed True if the server returned a valid response code or 325 * a PEAR_Error object is an error condition is reached. 326 * 327 * @since 1.1.0 328 * 329 * @see getResponse 330 */ 331 protected function parseResponse($valid, $later = false) 332 { 333 $this->code = -1; 334 $this->arguments = array(); 335 336 if ($later) { 337 $this->pipelined_commands++; 338 return true; 339 } 340 341 for ($i = 0; $i <= $this->pipelined_commands; $i++) { 342 while ($line = $this->socket->readLine()) { 343 $this->debug("Recv: $line"); 344 345 /* If we receive an empty line, the connection was closed. */ 346 if (empty($line)) { 347 $this->disconnect(); 348 return PEAR::raiseError('Connection was closed'); 349 } 350 351 /* Read the code and store the rest in the arguments array. */ 352 $code = substr($line, 0, 3); 353 $this->arguments[] = trim(substr($line, 4)); 354 355 /* Check the syntax of the response code. */ 356 if (is_numeric($code)) { 357 $this->code = (int)$code; 358 } else { 359 $this->code = -1; 360 break; 361 } 362 363 /* If this is not a multiline response, we're done. */ 364 if (substr($line, 3, 1) != '-') { 365 break; 366 } 367 } 368 } 369 370 $this->pipelined_commands = 0; 371 372 /* Compare the server's response code with the valid code/codes. */ 373 if (is_int($valid) && ($this->code === $valid)) { 374 return true; 375 } elseif (is_array($valid) && in_array($this->code, $valid, true)) { 376 return true; 377 } 378 379 return PEAR::raiseError('Invalid response code received from server', $this->code); 380 } 381 382 /** 383 * Issue an SMTP command and verify its response. 384 * 385 * @param string $command The SMTP command string or data. 386 * @param mixed $valid The set of valid response codes. These 387 * may be specified as an array of integer 388 * values or as a single integer value. 389 * 390 * @return mixed True on success or a PEAR_Error object on failure. 391 * 392 * @since 1.6.0 393 */ 394 public function command($command, $valid) 395 { 396 if (PEAR::isError($error = $this->put($command))) { 397 return $error; 398 } 399 if (PEAR::isError($error = $this->parseResponse($valid))) { 400 return $error; 401 } 402 403 return true; 404 } 405 406 /** 407 * Return a 2-tuple containing the last response from the SMTP server. 408 * 409 * @return array A two-element array: the first element contains the 410 * response code as an integer and the second element 411 * contains the response's arguments as a string. 412 * 413 * @since 1.1.0 414 */ 415 public function getResponse() 416 { 417 return array($this->code, join("\n", $this->arguments)); 418 } 419 420 /** 421 * Return the SMTP server's greeting string. 422 * 423 * @return string A string containing the greeting string, or null if 424 * a greeting has not been received. 425 * 426 * @since 1.3.3 427 */ 428 public function getGreeting() 429 { 430 return $this->greeting; 431 } 432 433 /** 434 * Attempt to connect to the SMTP server. 435 * 436 * @param int $timeout The timeout value (in seconds) for the 437 * socket connection attempt. 438 * @param bool $persistent Should a persistent socket connection 439 * be used? 440 * 441 * @return mixed Returns a PEAR_Error with an error message on any 442 * kind of failure, or true on success. 443 * @since 1.0 444 */ 445 public function connect($timeout = null, $persistent = false) 446 { 447 $this->greeting = null; 448 449 $result = $this->socket->connect( 450 $this->host, $this->port, $persistent, $timeout, $this->socket_options 451 ); 452 453 if (PEAR::isError($result)) { 454 return PEAR::raiseError( 455 'Failed to connect socket: ' . $result->getMessage() 456 ); 457 } 458 459 /* 460 * Now that we're connected, reset the socket's timeout value for 461 * future I/O operations. This allows us to have different socket 462 * timeout values for the initial connection (our $timeout parameter) 463 * and all other socket operations. 464 */ 465 if ($this->timeout > 0) { 466 if (PEAR::isError($error = $this->setTimeout($this->timeout))) { 467 return $error; 468 } 469 } 470 471 if (PEAR::isError($error = $this->parseResponse(220))) { 472 return $error; 473 } 474 475 /* Extract and store a copy of the server's greeting string. */ 476 list(, $this->greeting) = $this->getResponse(); 477 478 if (PEAR::isError($error = $this->negotiate())) { 479 return $error; 480 } 481 482 return true; 483 } 484 485 /** 486 * Attempt to disconnect from the SMTP server. 487 * 488 * @return mixed Returns a PEAR_Error with an error message on any 489 * kind of failure, or true on success. 490 * @since 1.0 491 */ 492 public function disconnect() 493 { 494 if (PEAR::isError($error = $this->put('QUIT'))) { 495 return $error; 496 } 497 if (PEAR::isError($error = $this->parseResponse(221))) { 498 return $error; 499 } 500 if (PEAR::isError($error = $this->socket->disconnect())) { 501 return PEAR::raiseError( 502 'Failed to disconnect socket: ' . $error->getMessage() 503 ); 504 } 505 506 return true; 507 } 508 509 /** 510 * Attempt to send the EHLO command and obtain a list of ESMTP 511 * extensions available, and failing that just send HELO. 512 * 513 * @return mixed Returns a PEAR_Error with an error message on any 514 * kind of failure, or true on success. 515 * 516 * @since 1.1.0 517 */ 518 protected function negotiate() 519 { 520 if (PEAR::isError($error = $this->put('EHLO', $this->localhost))) { 521 return $error; 522 } 523 524 if (PEAR::isError($this->parseResponse(250))) { 525 /* If the EHLO failed, try the simpler HELO command. */ 526 if (PEAR::isError($error = $this->put('HELO', $this->localhost))) { 527 return $error; 528 } 529 if (PEAR::isError($this->parseResponse(250))) { 530 return PEAR::raiseError('HELO was not accepted', $this->code); 531 } 532 533 return true; 534 } 535 536 foreach ($this->arguments as $argument) { 537 $verb = strtok($argument, ' '); 538 $len = strlen($verb); 539 $arguments = substr($argument, $len + 1, strlen($argument) - $len - 1); 540 $this->esmtp[$verb] = $arguments; 541 } 542 543 if (!isset($this->esmtp['PIPELINING'])) { 544 $this->pipelining = false; 545 } 546 547 return true; 548 } 549 550 /** 551 * Returns the name of the best authentication method that the server 552 * has advertised. 553 * 554 * @return mixed Returns a string containing the name of the best 555 * supported authentication method or a PEAR_Error object 556 * if a failure condition is encountered. 557 * @since 1.1.0 558 */ 559 protected function getBestAuthMethod() 560 { 561 $available_methods = explode(' ', $this->esmtp['AUTH']); 562 563 foreach ($this->auth_methods as $method => $callback) { 564 if (in_array($method, $available_methods)) { 565 return $method; 566 } 567 } 568 569 return PEAR::raiseError('No supported authentication methods'); 570 } 571 572 /** 573 * Attempt to do SMTP authentication. 574 * 575 * @param string $uid The userid to authenticate as. 576 * @param string $pwd The password to authenticate with. 577 * @param string $method The requested authentication method. If none is 578 * specified, the best supported method will be used. 579 * @param bool $tls Flag indicating whether or not TLS should be attempted. 580 * @param string $authz An optional authorization identifier. If specified, this 581 * identifier will be used as the authorization proxy. 582 * 583 * @return mixed Returns a PEAR_Error with an error message on any 584 * kind of failure, or true on success. 585 * @since 1.0 586 */ 587 public function auth($uid, $pwd , $method = '', $tls = true, $authz = '') 588 { 589 /* We can only attempt a TLS connection if one has been requested, 590 * we're running PHP 5.1.0 or later, have access to the OpenSSL 591 * extension, are connected to an SMTP server which supports the 592 * STARTTLS extension, and aren't already connected over a secure 593 * (SSL) socket connection. */ 594 if ($tls && version_compare(PHP_VERSION, '5.1.0', '>=') 595 && extension_loaded('openssl') && isset($this->esmtp['STARTTLS']) 596 && strncasecmp($this->host, 'ssl://', 6) !== 0 597 ) { 598 /* Start the TLS connection attempt. */ 599 if (PEAR::isError($result = $this->put('STARTTLS'))) { 600 return $result; 601 } 602 if (PEAR::isError($result = $this->parseResponse(220))) { 603 return $result; 604 } 605 if (isset($this->socket_options['ssl']['crypto_method'])) { 606 $crypto_method = $this->socket_options['ssl']['crypto_method']; 607 } else { 608 /* STREAM_CRYPTO_METHOD_TLS_ANY_CLIENT constant does not exist 609 * and STREAM_CRYPTO_METHOD_SSLv23_CLIENT constant is 610 * inconsistent across PHP versions. */ 611 $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT 612 | @STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT 613 | @STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; 614 } 615 if (PEAR::isError($result = $this->socket->enableCrypto(true, $crypto_method))) { 616 return $result; 617 } elseif ($result !== true) { 618 return PEAR::raiseError('STARTTLS failed'); 619 } 620 621 /* Send EHLO again to recieve the AUTH string from the 622 * SMTP server. */ 623 $this->negotiate(); 624 } 625 626 if (empty($this->esmtp['AUTH'])) { 627 return PEAR::raiseError('SMTP server does not support authentication'); 628 } 629 630 /* If no method has been specified, get the name of the best 631 * supported method advertised by the SMTP server. */ 632 if (empty($method)) { 633 if (PEAR::isError($method = $this->getBestAuthMethod())) { 634 /* Return the PEAR_Error object from _getBestAuthMethod(). */ 635 return $method; 636 } 637 } else { 638 $method = strtoupper($method); 639 if (!array_key_exists($method, $this->auth_methods)) { 640 return PEAR::raiseError("$method is not a supported authentication method"); 641 } 642 } 643 644 if (!isset($this->auth_methods[$method])) { 645 return PEAR::raiseError("$method is not a supported authentication method"); 646 } 647 648 if (!is_callable($this->auth_methods[$method], false)) { 649 return PEAR::raiseError("$method authentication method cannot be called"); 650 } 651 652 if (is_array($this->auth_methods[$method])) { 653 list($object, $method) = $this->auth_methods[$method]; 654 $result = $object->{$method}($uid, $pwd, $authz, $this); 655 } else { 656 $func = $this->auth_methods[$method]; 657 $result = $func($uid, $pwd, $authz, $this); 658 } 659 660 /* If an error was encountered, return the PEAR_Error object. */ 661 if (PEAR::isError($result)) { 662 return $result; 663 } 664 665 return true; 666 } 667 668 /** 669 * Add a new authentication method. 670 * 671 * @param string $name The authentication method name (e.g. 'PLAIN') 672 * @param mixed $callback The authentication callback (given as the name of a 673 * function or as an (object, method name) array). 674 * @param bool $prepend Should the new method be prepended to the list of 675 * available methods? This is the default behavior, 676 * giving the new method the highest priority. 677 * 678 * @return mixed True on success or a PEAR_Error object on failure. 679 * 680 * @since 1.6.0 681 */ 682 public function setAuthMethod($name, $callback, $prepend = true) 683 { 684 if (!is_string($name)) { 685 return PEAR::raiseError('Method name is not a string'); 686 } 687 688 if (!is_string($callback) && !is_array($callback)) { 689 return PEAR::raiseError('Method callback must be string or array'); 690 } 691 692 if (is_array($callback)) { 693 if (!is_object($callback[0]) || !is_string($callback[1])) { 694 return PEAR::raiseError('Bad mMethod callback array'); 695 } 696 } 697 698 if ($prepend) { 699 $this->auth_methods = array_merge( 700 array($name => $callback), $this->auth_methods 701 ); 702 } else { 703 $this->auth_methods[$name] = $callback; 704 } 705 706 return true; 707 } 708 709 /** 710 * Authenticates the user using the DIGEST-MD5 method. 711 * 712 * @param string $uid The userid to authenticate as. 713 * @param string $pwd The password to authenticate with. 714 * @param string $authz The optional authorization proxy identifier. 715 * 716 * @return mixed Returns a PEAR_Error with an error message on any 717 * kind of failure, or true on success. 718 * @since 1.1.0 719 */ 720 protected function authDigestMD5($uid, $pwd, $authz = '') 721 { 722 if (PEAR::isError($error = $this->put('AUTH', 'DIGEST-MD5'))) { 723 return $error; 724 } 725 /* 334: Continue authentication request */ 726 if (PEAR::isError($error = $this->parseResponse(334))) { 727 /* 503: Error: already authenticated */ 728 if ($this->code === 503) { 729 return true; 730 } 731 return $error; 732 } 733 734 $auth_sasl = new Auth_SASL; 735 $digest = $auth_sasl->factory('digest-md5'); 736 $challenge = base64_decode($this->arguments[0]); 737 $auth_str = base64_encode( 738 $digest->getResponse($uid, $pwd, $challenge, $this->host, "smtp", $authz) 739 ); 740 741 if (PEAR::isError($error = $this->put($auth_str))) { 742 return $error; 743 } 744 /* 334: Continue authentication request */ 745 if (PEAR::isError($error = $this->parseResponse(334))) { 746 return $error; 747 } 748 749 /* We don't use the protocol's third step because SMTP doesn't 750 * allow subsequent authentication, so we just silently ignore 751 * it. */ 752 if (PEAR::isError($error = $this->put(''))) { 753 return $error; 754 } 755 /* 235: Authentication successful */ 756 if (PEAR::isError($error = $this->parseResponse(235))) { 757 return $error; 758 } 759 } 760 761 /** 762 * Authenticates the user using the CRAM-MD5 method. 763 * 764 * @param string $uid The userid to authenticate as. 765 * @param string $pwd The password to authenticate with. 766 * @param string $authz The optional authorization proxy identifier. 767 * 768 * @return mixed Returns a PEAR_Error with an error message on any 769 * kind of failure, or true on success. 770 * @since 1.1.0 771 */ 772 protected function authCRAMMD5($uid, $pwd, $authz = '') 773 { 774 if (PEAR::isError($error = $this->put('AUTH', 'CRAM-MD5'))) { 775 return $error; 776 } 777 /* 334: Continue authentication request */ 778 if (PEAR::isError($error = $this->parseResponse(334))) { 779 /* 503: Error: already authenticated */ 780 if ($this->code === 503) { 781 return true; 782 } 783 return $error; 784 } 785 786 $auth_sasl = new Auth_SASL; 787 $challenge = base64_decode($this->arguments[0]); 788 $cram = $auth_sasl->factory('cram-md5'); 789 $auth_str = base64_encode($cram->getResponse($uid, $pwd, $challenge)); 790 791 if (PEAR::isError($error = $this->put($auth_str))) { 792 return $error; 793 } 794 795 /* 235: Authentication successful */ 796 if (PEAR::isError($error = $this->parseResponse(235))) { 797 return $error; 798 } 799 } 800 801 /** 802 * Authenticates the user using the LOGIN method. 803 * 804 * @param string $uid The userid to authenticate as. 805 * @param string $pwd The password to authenticate with. 806 * @param string $authz The optional authorization proxy identifier. 807 * 808 * @return mixed Returns a PEAR_Error with an error message on any 809 * kind of failure, or true on success. 810 * @since 1.1.0 811 */ 812 protected function authLogin($uid, $pwd, $authz = '') 813 { 814 if (PEAR::isError($error = $this->put('AUTH', 'LOGIN'))) { 815 return $error; 816 } 817 /* 334: Continue authentication request */ 818 if (PEAR::isError($error = $this->parseResponse(334))) { 819 /* 503: Error: already authenticated */ 820 if ($this->code === 503) { 821 return true; 822 } 823 return $error; 824 } 825 826 if (PEAR::isError($error = $this->put(base64_encode($uid)))) { 827 return $error; 828 } 829 /* 334: Continue authentication request */ 830 if (PEAR::isError($error = $this->parseResponse(334))) { 831 return $error; 832 } 833 834 if (PEAR::isError($error = $this->put(base64_encode($pwd)))) { 835 return $error; 836 } 837 838 /* 235: Authentication successful */ 839 if (PEAR::isError($error = $this->parseResponse(235))) { 840 return $error; 841 } 842 843 return true; 844 } 845 846 /** 847 * Authenticates the user using the PLAIN method. 848 * 849 * @param string $uid The userid to authenticate as. 850 * @param string $pwd The password to authenticate with. 851 * @param string $authz The optional authorization proxy identifier. 852 * 853 * @return mixed Returns a PEAR_Error with an error message on any 854 * kind of failure, or true on success. 855 * @since 1.1.0 856 */ 857 protected function authPlain($uid, $pwd, $authz = '') 858 { 859 if (PEAR::isError($error = $this->put('AUTH', 'PLAIN'))) { 860 return $error; 861 } 862 /* 334: Continue authentication request */ 863 if (PEAR::isError($error = $this->parseResponse(334))) { 864 /* 503: Error: already authenticated */ 865 if ($this->code === 503) { 866 return true; 867 } 868 return $error; 869 } 870 871 $auth_str = base64_encode($authz . chr(0) . $uid . chr(0) . $pwd); 872 873 if (PEAR::isError($error = $this->put($auth_str))) { 874 return $error; 875 } 876 877 /* 235: Authentication successful */ 878 if (PEAR::isError($error = $this->parseResponse(235))) { 879 return $error; 880 } 881 882 return true; 883 } 884 885 /** 886 * Authenticates the user using the GSSAPI method. 887 * 888 * PHP krb5 extension is required, 889 * service principal and credentials cache must be set. 890 * 891 * @param string $uid The userid to authenticate as. 892 * @param string $pwd The password to authenticate with. 893 * @param string $authz The optional authorization proxy identifier. 894 * 895 * @return mixed Returns a PEAR_Error with an error message on any 896 * kind of failure, or true on success. 897 */ 898 protected function authGSSAPI($uid, $pwd, $authz = '') 899 { 900 if (PEAR::isError($error = $this->put('AUTH', 'GSSAPI'))) { 901 return $error; 902 } 903 /* 334: Continue authentication request */ 904 if (PEAR::isError($error = $this->parseResponse(334))) { 905 /* 503: Error: already authenticated */ 906 if ($this->code === 503) { 907 return true; 908 } 909 return $error; 910 } 911 912 if (!$this->gssapi_principal) { 913 return PEAR::raiseError('No Kerberos service principal set', 2); 914 } 915 916 if (!empty($this->gssapi_cname)) { 917 putenv('KRB5CCNAME=' . $this->gssapi_cname); 918 } 919 920 try { 921 $ccache = new KRB5CCache(); 922 if (!empty($this->gssapi_cname)) { 923 $ccache->open($this->gssapi_cname); 924 } 925 926 $gssapicontext = new GSSAPIContext(); 927 $gssapicontext->acquireCredentials($ccache); 928 929 $token = ''; 930 $success = $gssapicontext->initSecContext($this->gssapi_principal, null, null, null, $token); 931 $token = base64_encode($token); 932 } 933 catch (Exception $e) { 934 return PEAR::raiseError('GSSAPI authentication failed: ' . $e->getMessage()); 935 } 936 937 if (PEAR::isError($error = $this->put($token))) { 938 return $error; 939 } 940 941 /* 334: Continue authentication request */ 942 if (PEAR::isError($error = $this->parseResponse(334))) { 943 return $error; 944 } 945 946 $response = $this->arguments[0]; 947 948 try { 949 $challenge = base64_decode($response); 950 $gssapicontext->unwrap($challenge, $challenge); 951 $gssapicontext->wrap($challenge, $challenge, true); 952 } 953 catch (Exception $e) { 954 return PEAR::raiseError('GSSAPI authentication failed: ' . $e->getMessage()); 955 } 956 957 if (PEAR::isError($error = $this->put(base64_encode($challenge)))) { 958 return $error; 959 } 960 961 /* 235: Authentication successful */ 962 if (PEAR::isError($error = $this->parseResponse(235))) { 963 return $error; 964 } 965 966 return true; 967 } 968 969 /** 970 * Authenticates the user using the XOAUTH2 method. 971 * 972 * @param string $uid The userid to authenticate as. 973 * @param string $token The access token to authenticate with. 974 * @param string $authz The optional authorization proxy identifier. 975 * 976 * @return mixed Returns a PEAR_Error with an error message on any 977 * kind of failure, or true on success. 978 * @since 1.9.0 979 */ 980 public function authXOAuth2($uid, $token, $authz, $conn) 981 { 982 $auth = base64_encode("user=$uid\1auth=$token\1\1"); 983 if (PEAR::isError($error = $this->put('AUTH', 'XOAUTH2 ' . $auth))) { 984 return $error; 985 } 986 987 /* 235: Authentication successful or 334: Continue authentication */ 988 if (PEAR::isError($error = $this->parseResponse([235, 334]))) { 989 return $error; 990 } 991 992 /* 334: Continue authentication request */ 993 if ($this->code === 334) { 994 /* Send an empty line as response to 334 */ 995 if (PEAR::isError($error = $this->put(''))) { 996 return $error; 997 } 998 999 /* Expect 235: Authentication successful */ 1000 if (PEAR::isError($error = $this->parseResponse(235))) { 1001 return $error; 1002 } 1003 } 1004 1005 return true; 1006 } 1007 1008 /** 1009 * Send the HELO command. 1010 * 1011 * @param string $domain The domain name to say we are. 1012 * 1013 * @return mixed Returns a PEAR_Error with an error message on any 1014 * kind of failure, or true on success. 1015 * @since 1.0 1016 */ 1017 public function helo($domain) 1018 { 1019 if (PEAR::isError($error = $this->put('HELO', $domain))) { 1020 return $error; 1021 } 1022 if (PEAR::isError($error = $this->parseResponse(250))) { 1023 return $error; 1024 } 1025 1026 return true; 1027 } 1028 1029 /** 1030 * Return the list of SMTP service extensions advertised by the server. 1031 * 1032 * @return array The list of SMTP service extensions. 1033 * @since 1.3 1034 */ 1035 public function getServiceExtensions() 1036 { 1037 return $this->esmtp; 1038 } 1039 1040 /** 1041 * Send the MAIL FROM: command. 1042 * 1043 * @param string $sender The sender (reverse path) to set. 1044 * @param string $params String containing additional MAIL parameters, 1045 * such as the NOTIFY flags defined by RFC 1891 1046 * or the VERP protocol. 1047 * 1048 * If $params is an array, only the 'verp' option 1049 * is supported. If 'verp' is true, the XVERP 1050 * parameter is appended to the MAIL command. 1051 * If the 'verp' value is a string, the full 1052 * XVERP=value parameter is appended. 1053 * 1054 * @return mixed Returns a PEAR_Error with an error message on any 1055 * kind of failure, or true on success. 1056 * @since 1.0 1057 */ 1058 public function mailFrom($sender, $params = null) 1059 { 1060 $args = "FROM:<$sender>"; 1061 1062 /* Support the deprecated array form of $params. */ 1063 if (is_array($params) && isset($params['verp'])) { 1064 if ($params['verp'] === true) { 1065 $args .= ' XVERP'; 1066 } elseif (trim($params['verp'])) { 1067 $args .= ' XVERP=' . $params['verp']; 1068 } 1069 } elseif (is_string($params) && !empty($params)) { 1070 $args .= ' ' . $params; 1071 } 1072 1073 if (PEAR::isError($error = $this->put('MAIL', $args))) { 1074 return $error; 1075 } 1076 if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) { 1077 return $error; 1078 } 1079 1080 return true; 1081 } 1082 1083 /** 1084 * Send the RCPT TO: command. 1085 * 1086 * @param string $recipient The recipient (forward path) to add. 1087 * @param string $params String containing additional RCPT parameters, 1088 * such as the NOTIFY flags defined by RFC 1891. 1089 * 1090 * @return mixed Returns a PEAR_Error with an error message on any 1091 * kind of failure, or true on success. 1092 * 1093 * @since 1.0 1094 */ 1095 public function rcptTo($recipient, $params = null) 1096 { 1097 $args = "TO:<$recipient>"; 1098 if (is_string($params)) { 1099 $args .= ' ' . $params; 1100 } 1101 1102 if (PEAR::isError($error = $this->put('RCPT', $args))) { 1103 return $error; 1104 } 1105 if (PEAR::isError($error = $this->parseResponse(array(250, 251), $this->pipelining))) { 1106 return $error; 1107 } 1108 1109 return true; 1110 } 1111 1112 /** 1113 * Quote the data so that it meets SMTP standards. 1114 * 1115 * This is provided as a separate public function to facilitate 1116 * easier overloading for the cases where it is desirable to 1117 * customize the quoting behavior. 1118 * 1119 * @param string &$data The message text to quote. The string must be passed 1120 * by reference, and the text will be modified in place. 1121 * 1122 * @since 1.2 1123 */ 1124 public function quotedata(&$data) 1125 { 1126 /* Because a single leading period (.) signifies an end to the 1127 * data, legitimate leading periods need to be "doubled" ('..'). */ 1128 $data = preg_replace('/^\./m', '..', $data); 1129 1130 /* Change Unix (\n) and Mac (\r) linefeeds into CRLF's (\r\n). */ 1131 $data = preg_replace('/(?:\r\n|\n|\r(?!\n))/', "\r\n", $data); 1132 } 1133 1134 /** 1135 * Send the DATA command. 1136 * 1137 * @param mixed $data The message data, either as a string or an open 1138 * file resource. 1139 * @param string $headers The message headers. If $headers is provided, 1140 * $data is assumed to contain only body data. 1141 * 1142 * @return mixed Returns a PEAR_Error with an error message on any 1143 * kind of failure, or true on success. 1144 * @since 1.0 1145 */ 1146 public function data($data, $headers = null) 1147 { 1148 /* Verify that $data is a supported type. */ 1149 if (!is_string($data) && !is_resource($data)) { 1150 return PEAR::raiseError('Expected a string or file resource'); 1151 } 1152 1153 /* Start by considering the size of the optional headers string. We 1154 * also account for the addition 4 character "\r\n\r\n" separator 1155 * sequence. */ 1156 $size = $headers_size = (is_null($headers)) ? 0 : strlen($headers) + 4; 1157 1158 if (is_resource($data)) { 1159 $stat = fstat($data); 1160 if ($stat === false) { 1161 return PEAR::raiseError('Failed to get file size'); 1162 } 1163 $size += $stat['size']; 1164 } else { 1165 $size += strlen($data); 1166 } 1167 1168 /* RFC 1870, section 3, subsection 3 states "a value of zero indicates 1169 * that no fixed maximum message size is in force". Furthermore, it 1170 * says that if "the parameter is omitted no information is conveyed 1171 * about the server's fixed maximum message size". */ 1172 $limit = (isset($this->esmtp['SIZE'])) ? $this->esmtp['SIZE'] : 0; 1173 if ($limit > 0 && $size >= $limit) { 1174 return PEAR::raiseError('Message size exceeds server limit'); 1175 } 1176 1177 /* Initiate the DATA command. */ 1178 if (PEAR::isError($error = $this->put('DATA'))) { 1179 return $error; 1180 } 1181 if (PEAR::isError($error = $this->parseResponse(354))) { 1182 return $error; 1183 } 1184 1185 /* If we have a separate headers string, send it first. */ 1186 if (!is_null($headers)) { 1187 $this->quotedata($headers); 1188 if (PEAR::isError($result = $this->send($headers . "\r\n\r\n"))) { 1189 return $result; 1190 } 1191 1192 /* Subtract the headers size now that they've been sent. */ 1193 $size -= $headers_size; 1194 } 1195 1196 /* Now we can send the message body data. */ 1197 if (is_resource($data)) { 1198 /* Stream the contents of the file resource out over our socket 1199 * connection, line by line. Each line must be run through the 1200 * quoting routine. */ 1201 while (strlen($line = fread($data, 8192)) > 0) { 1202 /* If the last character is an newline, we need to grab the 1203 * next character to check to see if it is a period. */ 1204 while (!feof($data)) { 1205 $char = fread($data, 1); 1206 $line .= $char; 1207 if ($char != "\n") { 1208 break; 1209 } 1210 } 1211 $this->quotedata($line); 1212 if (PEAR::isError($result = $this->send($line))) { 1213 return $result; 1214 } 1215 } 1216 1217 $last = $line; 1218 } else { 1219 /* 1220 * Break up the data by sending one chunk (up to 512k) at a time. 1221 * This approach reduces our peak memory usage. 1222 */ 1223 for ($offset = 0; $offset < $size;) { 1224 $end = $offset + 512000; 1225 1226 /* 1227 * Ensure we don't read beyond our data size or span multiple 1228 * lines. quotedata() can't properly handle character data 1229 * that's split across two line break boundaries. 1230 */ 1231 if ($end >= $size) { 1232 $end = $size; 1233 } else { 1234 for (; $end < $size; $end++) { 1235 if ($data[$end] != "\n") { 1236 break; 1237 } 1238 } 1239 } 1240 1241 /* Extract our chunk and run it through the quoting routine. */ 1242 $chunk = substr($data, $offset, $end - $offset); 1243 $this->quotedata($chunk); 1244 1245 /* If we run into a problem along the way, abort. */ 1246 if (PEAR::isError($result = $this->send($chunk))) { 1247 return $result; 1248 } 1249 1250 /* Advance the offset to the end of this chunk. */ 1251 $offset = $end; 1252 } 1253 1254 $last = $chunk; 1255 } 1256 1257 /* Don't add another CRLF sequence if it's already in the data */ 1258 $terminator = (substr($last, -2) == "\r\n" ? '' : "\r\n") . ".\r\n"; 1259 1260 /* Finally, send the DATA terminator sequence. */ 1261 if (PEAR::isError($result = $this->send($terminator))) { 1262 return $result; 1263 } 1264 1265 /* Verify that the data was successfully received by the server. */ 1266 if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) { 1267 return $error; 1268 } 1269 1270 return true; 1271 } 1272 1273 /** 1274 * Send the SEND FROM: command. 1275 * 1276 * @param string $path The reverse path to send. 1277 * 1278 * @return mixed Returns a PEAR_Error with an error message on any 1279 * kind of failure, or true on success. 1280 * @since 1.2.6 1281 */ 1282 public function sendFrom($path) 1283 { 1284 if (PEAR::isError($error = $this->put('SEND', "FROM:<$path>"))) { 1285 return $error; 1286 } 1287 if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) { 1288 return $error; 1289 } 1290 1291 return true; 1292 } 1293 1294 /** 1295 * Send the SOML FROM: command. 1296 * 1297 * @param string $path The reverse path to send. 1298 * 1299 * @return mixed Returns a PEAR_Error with an error message on any 1300 * kind of failure, or true on success. 1301 * @since 1.2.6 1302 */ 1303 public function somlFrom($path) 1304 { 1305 if (PEAR::isError($error = $this->put('SOML', "FROM:<$path>"))) { 1306 return $error; 1307 } 1308 if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) { 1309 return $error; 1310 } 1311 1312 return true; 1313 } 1314 1315 /** 1316 * Send the SAML FROM: command. 1317 * 1318 * @param string $path The reverse path to send. 1319 * 1320 * @return mixed Returns a PEAR_Error with an error message on any 1321 * kind of failure, or true on success. 1322 * @since 1.2.6 1323 */ 1324 public function samlFrom($path) 1325 { 1326 if (PEAR::isError($error = $this->put('SAML', "FROM:<$path>"))) { 1327 return $error; 1328 } 1329 if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) { 1330 return $error; 1331 } 1332 1333 return true; 1334 } 1335 1336 /** 1337 * Send the RSET command. 1338 * 1339 * @return mixed Returns a PEAR_Error with an error message on any 1340 * kind of failure, or true on success. 1341 * @since 1.0 1342 */ 1343 public function rset() 1344 { 1345 if (PEAR::isError($error = $this->put('RSET'))) { 1346 return $error; 1347 } 1348 if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) { 1349 return $error; 1350 } 1351 1352 return true; 1353 } 1354 1355 /** 1356 * Send the VRFY command. 1357 * 1358 * @param string $string The string to verify 1359 * 1360 * @return mixed Returns a PEAR_Error with an error message on any 1361 * kind of failure, or true on success. 1362 * @since 1.0 1363 */ 1364 public function vrfy($string) 1365 { 1366 /* Note: 251 is also a valid response code */ 1367 if (PEAR::isError($error = $this->put('VRFY', $string))) { 1368 return $error; 1369 } 1370 if (PEAR::isError($error = $this->parseResponse(array(250, 252)))) { 1371 return $error; 1372 } 1373 1374 return true; 1375 } 1376 1377 /** 1378 * Send the NOOP command. 1379 * 1380 * @return mixed Returns a PEAR_Error with an error message on any 1381 * kind of failure, or true on success. 1382 * @since 1.0 1383 */ 1384 public function noop() 1385 { 1386 if (PEAR::isError($error = $this->put('NOOP'))) { 1387 return $error; 1388 } 1389 if (PEAR::isError($error = $this->parseResponse(250))) { 1390 return $error; 1391 } 1392 1393 return true; 1394 } 1395 1396 /** 1397 * Backwards-compatibility method. identifySender()'s functionality is 1398 * now handled internally. 1399 * 1400 * @return boolean This method always return true. 1401 * 1402 * @since 1.0 1403 */ 1404 public function identifySender() 1405 { 1406 return true; 1407 } 1408} 1409