From a963a2b38cf571b29543d17edadc46f91caba3aa Mon Sep 17 00:00:00 2001 From: Aleksander Machniak <alec@alec.pl> Date: Fri, 24 May 2013 14:08:20 -0400 Subject: [PATCH] Fix connecting when host is specified with protocol prefix e.g. ssl:// --- program/lib/Net/SMTP.php | 208 ++++++++++++++++++++++++++++++++++++--------------- 1 files changed, 147 insertions(+), 61 deletions(-) diff --git a/program/lib/Net/SMTP.php b/program/lib/Net/SMTP.php index fef8076..2c1ef5c 100644 --- a/program/lib/Net/SMTP.php +++ b/program/lib/Net/SMTP.php @@ -17,8 +17,6 @@ // | Jon Parise <jon@php.net> | // | Damian Alejandro Fernandez Sosa <damlists@cnba.uba.ar> | // +----------------------------------------------------------------------+ -// -// $Id$ require_once 'PEAR.php'; require_once 'Net/Socket.php'; @@ -62,7 +60,7 @@ * @var array * @access public */ - var $auth_methods = array('DIGEST-MD5', 'CRAM-MD5', 'LOGIN', 'PLAIN'); + var $auth_methods = array(); /** * Use SMTP command pipelining (specified in RFC 2920) if the SMTP @@ -104,6 +102,14 @@ * @access private */ var $_socket = null; + + /** + * Array of socket options that will be passed to Net_Socket::connect(). + * @see stream_context_create() + * @var array + * @access private + */ + var $_socket_options = null; /** * The socket I/O timeout value in seconds. @@ -156,12 +162,13 @@ * @param string $localhost The value to give when sending EHLO or HELO. * @param boolean $pipeling Use SMTP command pipelining * @param integer $timeout Socket I/O timeout in seconds. + * @param array $socket_options Socket stream_context_create() options. * * @access public * @since 1.0 */ function Net_SMTP($host = null, $port = null, $localhost = null, - $pipelining = false, $timeout = 0) + $pipelining = false, $timeout = 0, $socket_options = null) { if (isset($host)) { $this->host = $host; @@ -175,17 +182,19 @@ $this->pipelining = $pipelining; $this->_socket = new Net_Socket(); + $this->_socket_options = $socket_options; $this->_timeout = $timeout; - /* Include the Auth_SASL package. If the package is not - * available, we disable the authentication methods that - * depend upon it. */ - if ((@include_once 'Auth/SASL.php') === false) { - $pos = array_search('DIGEST-MD5', $this->auth_methods); - unset($this->auth_methods[$pos]); - $pos = array_search('CRAM-MD5', $this->auth_methods); - unset($this->auth_methods[$pos]); + /* Include the Auth_SASL package. If the package is available, we + * enable the authentication methods that depend upon it. */ + if (@include_once 'Auth/SASL.php') { + $this->setAuthMethod('CRAM-MD5', array($this, '_authCram_MD5')); + $this->setAuthMethod('DIGEST-MD5', array($this, '_authDigest_MD5')); } + + /* These standard authentication methods are always available. */ + $this->setAuthMethod('LOGIN', array($this, '_authLogin'), false); + $this->setAuthMethod('PLAIN', array($this, '_authPlain'), false); } /** @@ -240,7 +249,8 @@ * * @param string $data The string of data to send. * - * @return mixed True on success or a PEAR_Error object on failure. + * @return mixed The number of bytes that were actually written, + * or a PEAR_Error object on failure. * * @access private * @since 1.1.0 @@ -249,13 +259,14 @@ { $this->_debug("Send: $data"); - $error = $this->_socket->write($data); - if ($error === false || PEAR::isError($error)) { - $msg = ($error) ? $error->getMessage() : "unknown error"; - return PEAR::raiseError("Failed to write to socket: $msg"); + $result = $this->_socket->write($data); + if (!$result || PEAR::isError($result)) { + $msg = ($result) ? $result->getMessage() : "unknown error"; + return PEAR::raiseError("Failed to write to socket: $msg", + null, PEAR_ERROR_RETURN); } - return true; + return $result; } /** @@ -282,7 +293,8 @@ } if (strcspn($command, "\r\n") !== strlen($command)) { - return PEAR::raiseError('Commands cannot contain newlines'); + return PEAR::raiseError('Commands cannot contain newlines', + null, PEAR_ERROR_RETURN); } return $this->_send($command . "\r\n"); @@ -321,10 +333,11 @@ while ($line = $this->_socket->readLine()) { $this->_debug("Recv: $line"); - /* If we receive an empty line, the connection has been closed. */ + /* If we receive an empty line, the connection was closed. */ if (empty($line)) { $this->disconnect(); - return PEAR::raiseError('Connection was unexpectedly closed'); + return PEAR::raiseError('Connection was closed', + null, PEAR_ERROR_RETURN); } /* Read the code and store the rest in the arguments array. */ @@ -356,7 +369,32 @@ } return PEAR::raiseError('Invalid response code received from server', - $this->_code); + $this->_code, PEAR_ERROR_RETURN); + } + + /** + * Issue an SMTP command and verify its response. + * + * @param string $command The SMTP command string or data. + * @param mixed $valid The set of valid response codes. These + * may be specified as an array of integer + * values or as a single integer value. + * + * @return mixed True on success or a PEAR_Error object on failure. + * + * @access public + * @since 1.6.0 + */ + function command($command, $valid) + { + if (PEAR::isError($error = $this->_put($command))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse($valid))) { + return $error; + } + + return true; } /** @@ -405,7 +443,8 @@ { $this->_greeting = null; $result = $this->_socket->connect($this->host, $this->port, - $persistent, $timeout); + $persistent, $timeout, + $this->_socket_options); if (PEAR::isError($result)) { return PEAR::raiseError('Failed to connect socket: ' . $result->getMessage()); @@ -417,8 +456,10 @@ * timeout values for the initial connection (our $timeout parameter) * and all other socket operations. */ - if (PEAR::isError($error = $this->setTimeout($this->_timeout))) { - return $error; + if ($this->_timeout > 0) { + if (PEAR::isError($error = $this->setTimeout($this->_timeout))) { + return $error; + } } if (PEAR::isError($error = $this->_parseResponse(220))) { @@ -486,7 +527,8 @@ return $error; } if (PEAR::isError($this->_parseResponse(250))) { - return PEAR::raiseError('HELO was not accepted: ', $this->_code); + return PEAR::raiseError('HELO was not accepted: ', $this->_code, + PEAR_ERROR_RETURN); } return true; @@ -520,13 +562,14 @@ { $available_methods = explode(' ', $this->_esmtp['AUTH']); - foreach ($this->auth_methods as $method) { + foreach ($this->auth_methods as $method => $callback) { if (in_array($method, $available_methods)) { return $method; } } - return PEAR::raiseError('No supported authentication methods'); + return PEAR::raiseError('No supported authentication methods', + null, PEAR_ERROR_RETURN); } /** @@ -586,36 +629,70 @@ } } else { $method = strtoupper($method); - if (!in_array($method, $this->auth_methods)) { + if (!array_key_exists($method, $this->auth_methods)) { return PEAR::raiseError("$method is not a supported authentication method"); } } - switch ($method) { - case 'DIGEST-MD5': - $result = $this->_authDigest_MD5($uid, $pwd, $authz); - break; - - case 'CRAM-MD5': - $result = $this->_authCRAM_MD5($uid, $pwd); - break; - - case 'LOGIN': - $result = $this->_authLogin($uid, $pwd); - break; - - case 'PLAIN': - $result = $this->_authPlain($uid, $pwd, $authz); - break; - - default: - $result = PEAR::raiseError("$method is not a supported authentication method"); - break; + if (!isset($this->auth_methods[$method])) { + return PEAR::raiseError("$method is not a supported authentication method"); } + + if (!is_callable($this->auth_methods[$method], false)) { + return PEAR::raiseError("$method authentication method cannot be called"); + } + + if (is_array($this->auth_methods[$method])) { + list($object, $method) = $this->auth_methods[$method]; + $result = $object->{$method}($uid, $pwd, $authz, $this); + } else { + $func = $this->auth_methods[$method]; + $result = $func($uid, $pwd, $authz, $this); + } /* If an error was encountered, return the PEAR_Error object. */ if (PEAR::isError($result)) { return $result; + } + + return true; + } + + /** + * Add a new authentication method. + * + * @param string The authentication method name (e.g. 'PLAIN') + * @param mixed The authentication callback (given as the name of a + * function or as an (object, method name) array). + * @param bool Should the new method be prepended to the list of + * available methods? This is the default behavior, + * giving the new method the highest priority. + * + * @return mixed True on success or a PEAR_Error object on failure. + * + * @access public + * @since 1.6.0 + */ + function setAuthMethod($name, $callback, $prepend = true) + { + if (!is_string($name)) { + return PEAR::raiseError('Method name is not a string'); + } + + if (!is_string($callback) && !is_array($callback)) { + return PEAR::raiseError('Method callback must be string or array'); + } + + if (is_array($callback)) { + if (!is_object($callback[0]) || !is_string($callback[1])) + return PEAR::raiseError('Bad mMethod callback array'); + } + + if ($prepend) { + $this->auth_methods = array_merge(array($name => $callback), + $this->auth_methods); + } else { + $this->auth_methods[$name] = $callback; } return true; @@ -648,7 +725,7 @@ } $challenge = base64_decode($this->_arguments[0]); - $digest = &Auth_SASL::factory('digestmd5'); + $digest = &Auth_SASL::factory('digest-md5'); $auth_str = base64_encode($digest->getResponse($uid, $pwd, $challenge, $this->host, "smtp", $authz)); @@ -678,13 +755,14 @@ * * @param string The userid to authenticate as. * @param string The password to authenticate with. + * @param string The optional authorization proxy identifier. * * @return mixed Returns a PEAR_Error with an error message on any * kind of failure, or true on success. * @access private * @since 1.1.0 */ - function _authCRAM_MD5($uid, $pwd) + function _authCRAM_MD5($uid, $pwd, $authz = '') { if (PEAR::isError($error = $this->_put('AUTH', 'CRAM-MD5'))) { return $error; @@ -699,7 +777,7 @@ } $challenge = base64_decode($this->_arguments[0]); - $cram = &Auth_SASL::factory('crammd5'); + $cram = &Auth_SASL::factory('cram-md5'); $auth_str = base64_encode($cram->getResponse($uid, $pwd, $challenge)); if (PEAR::isError($error = $this->_put($auth_str))) { @@ -717,13 +795,14 @@ * * @param string The userid to authenticate as. * @param string The password to authenticate with. + * @param string The optional authorization proxy identifier. * * @return mixed Returns a PEAR_Error with an error message on any * kind of failure, or true on success. * @access private * @since 1.1.0 */ - function _authLogin($uid, $pwd) + function _authLogin($uid, $pwd, $authz = '') { if (PEAR::isError($error = $this->_put('AUTH', 'LOGIN'))) { return $error; @@ -923,14 +1002,12 @@ */ function quotedata(&$data) { - /* Change Unix (\n) and Mac (\r) linefeeds into - * Internet-standard CRLF (\r\n) linefeeds. */ - $data = preg_replace(array('/(?<!\r)\n/','/\r(?!\n)/'), "\r\n", $data); - /* Because a single leading period (.) signifies an end to the - * data, legitimate leading periods need to be "doubled" - * (e.g. '..'). */ - $data = str_replace("\n.", "\n..", $data); + * data, legitimate leading periods need to be "doubled" ('..'). */ + $data = preg_replace('/^\./m', '..', $data); + + /* Change Unix (\n) and Mac (\r) linefeeds into CRLF's (\r\n). */ + $data = preg_replace('/(?:\r\n|\n|\r(?!\n))/', "\r\n", $data); } /** @@ -999,7 +1076,16 @@ /* Stream the contents of the file resource out over our socket * connection, line by line. Each line must be run through the * quoting routine. */ - while ($line = fgets($data, 1024)) { + while (strlen($line = fread($data, 8192)) > 0) { + /* If the last character is an newline, we need to grab the + * next character to check to see if it is a period. */ + while (!feof($data)) { + $char = fread($data, 1); + $line .= $char; + if ($char != "\n") { + break; + } + } $this->quotedata($line); if (PEAR::isError($result = $this->_send($line))) { return $result; -- Gitblit v1.9.1