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 | 346 +++++++++++++++++++++++++++++++++++++++++---------------- 1 files changed, 248 insertions(+), 98 deletions(-) diff --git a/program/lib/Net/SMTP.php b/program/lib/Net/SMTP.php index 0602e10..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,21 @@ * @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. + * @var int + * @access private + */ + var $_timeout = 0; /** * The most recent server response code. @@ -148,11 +161,14 @@ * @param integer $port The port to connect to. * @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) + function Net_SMTP($host = null, $port = null, $localhost = null, + $pipelining = false, $timeout = 0, $socket_options = null) { if (isset($host)) { $this->host = $host; @@ -166,16 +182,32 @@ $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); + } + + /** + * Set the socket I/O timeout value in seconds plus microseconds. + * + * @param integer $seconds Timeout value in seconds. + * @param integer $microseconds Additional value in microseconds. + * + * @access public + * @since 1.5.0 + */ + function setTimeout($seconds, $microseconds = 0) { + return $this->_socket->setTimeout($seconds, $microseconds); } /** @@ -217,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 @@ -226,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; } /** @@ -259,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"); @@ -298,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. */ @@ -333,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; } /** @@ -369,7 +430,7 @@ * Attempt to connect to the SMTP server. * * @param int $timeout The timeout value (in seconds) for the - * socket connection. + * socket connection attempt. * @param bool $persistent Should a persistent socket connection * be used? * @@ -382,10 +443,23 @@ { $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()); + } + + /* + * Now that we're connected, reset the socket's timeout value for + * future I/O operations. This allows us to have different socket + * timeout values for the initial connection (our $timeout parameter) + * and all other socket operations. + */ + if ($this->_timeout > 0) { + if (PEAR::isError($error = $this->setTimeout($this->_timeout))) { + return $error; + } } if (PEAR::isError($error = $this->_parseResponse(220))) { @@ -453,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; @@ -487,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); } /** @@ -504,13 +580,15 @@ * @param string The requested authentication method. If none is * specified, the best supported method will be used. * @param bool Flag indicating whether or not TLS should be attempted. + * @param string An optional authorization identifier. If specified, this + * identifier will be used as the authorization proxy. * * @return mixed Returns a PEAR_Error with an error message on any * kind of failure, or true on success. * @access public * @since 1.0 */ - function auth($uid, $pwd , $method = '', $tls = true) + function auth($uid, $pwd , $method = '', $tls = true, $authz = '') { /* We can only attempt a TLS connection if one has been requested, * we're running PHP 5.1.0 or later, have access to the OpenSSL @@ -551,32 +629,26 @@ } } 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); - 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); - 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)) { @@ -587,17 +659,58 @@ } /** + * 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; + } + + /** * Authenticates the user using the DIGEST-MD5 method. * * @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 _authDigest_MD5($uid, $pwd) + function _authDigest_MD5($uid, $pwd, $authz = '') { if (PEAR::isError($error = $this->_put('AUTH', 'DIGEST-MD5'))) { return $error; @@ -612,9 +725,10 @@ } $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")); + $this->host, "smtp", + $authz)); if (PEAR::isError($error = $this->_put($auth_str))) { return $error; @@ -641,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; @@ -662,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))) { @@ -680,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; @@ -725,13 +841,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 _authPlain($uid, $pwd) + function _authPlain($uid, $pwd, $authz = '') { if (PEAR::isError($error = $this->_put('AUTH', 'PLAIN'))) { return $error; @@ -745,7 +862,7 @@ return $error; } - $auth_str = base64_encode(chr(0) . $uid . chr(0) . $pwd); + $auth_str = base64_encode($authz . chr(0) . $uid . chr(0) . $pwd); if (PEAR::isError($error = $this->_put($auth_str))) { return $error; @@ -826,7 +943,7 @@ } elseif (trim($params['verp'])) { $args .= ' XVERP=' . $params['verp']; } - } elseif (is_string($params)) { + } elseif (is_string($params) && !empty($params)) { $args .= ' ' . $params; } @@ -885,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); } /** @@ -915,31 +1030,29 @@ return PEAR::raiseError('Expected a string or file resource'); } - /* RFC 1870, section 3, subsection 3 states "a value of zero - * indicates that no fixed maximum message size is in force". - * Furthermore, it says that if "the parameter is omitted no - * information is conveyed about the server's fixed maximum - * message size". */ - if (isset($this->_esmtp['SIZE']) && ($this->_esmtp['SIZE'] > 0)) { - /* Start by considering the size of the optional headers string. - * We also account for the addition 4 character "\r\n\r\n" - * separator sequence. */ - $size = (is_null($headers)) ? 0 : strlen($headers) + 4; + /* Start by considering the size of the optional headers string. We + * also account for the addition 4 character "\r\n\r\n" separator + * sequence. */ + $size = (is_null($headers)) ? 0 : strlen($headers) + 4; - if (is_resource($data)) { - $stat = fstat($data); - if ($stat === false) { - return PEAR::raiseError('Failed to get file size'); - } - $size += $stat['size']; - } else { - $size += strlen($data); + if (is_resource($data)) { + $stat = fstat($data); + if ($stat === false) { + return PEAR::raiseError('Failed to get file size'); } + $size += $stat['size']; + } else { + $size += strlen($data); + } - if ($size >= $this->_esmtp['SIZE']) { - $this->disconnect(); - return PEAR::raiseError('Message size exceeds server limit'); - } + /* RFC 1870, section 3, subsection 3 states "a value of zero indicates + * that no fixed maximum message size is in force". Furthermore, it + * says that if "the parameter is omitted no information is conveyed + * about the server's fixed maximum message size". */ + $limit = (isset($this->_esmtp['SIZE'])) ? $this->_esmtp['SIZE'] : 0; + if ($limit > 0 && $size >= $limit) { + $this->disconnect(); + return PEAR::raiseError('Message size exceeds server limit'); } /* Initiate the DATA command. */ @@ -963,26 +1076,63 @@ /* 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; } } - - /* Finally, send the DATA terminator sequence. */ - if (PEAR::isError($result = $this->_send("\r\n.\r\n"))) { - return $result; - } } else { - /* Just send the entire quoted string followed by the DATA - * terminator. */ - $this->quotedata($data); - if (PEAR::isError($result = $this->_send($data . "\r\n.\r\n"))) { - return $result; + /* + * Break up the data by sending one chunk (up to 512k) at a time. + * This approach reduces our peak memory usage. + */ + for ($offset = 0; $offset < $size;) { + $end = $offset + 512000; + + /* + * Ensure we don't read beyond our data size or span multiple + * lines. quotedata() can't properly handle character data + * that's split across two line break boundaries. + */ + if ($end >= $size) { + $end = $size; + } else { + for (; $end < $size; $end++) { + if ($data[$end] != "\n") { + break; + } + } + } + + /* Extract our chunk and run it through the quoting routine. */ + $chunk = substr($data, $offset, $end - $offset); + $this->quotedata($chunk); + + /* If we run into a problem along the way, abort. */ + if (PEAR::isError($result = $this->_send($chunk))) { + return $result; + } + + /* Advance the offset to the end of this chunk. */ + $offset = $end; } } + /* Finally, send the DATA terminator sequence. */ + if (PEAR::isError($result = $this->_send("\r\n.\r\n"))) { + return $result; + } + /* Verify that the data was successfully received by the server. */ if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { return $error; -- Gitblit v1.9.1