From 3875eb68139d878fba2f00bdcaae3c34ebb53da7 Mon Sep 17 00:00:00 2001 From: alecpl Date: Thu, 24 Nov 2011 10:54:59 +0000 Subject: - Add possibility to add SASL mechanisms for SMTP in smtp_connect hook (#1487937) --- CHANGELOG | 1 + program/include/rcube_smtp.php | 15 +++- program/lib/Net/SMTP.php | 167 ++++++++++++++++++++++++++++++----------- 3 files changed, 135 insertions(+), 48 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bc7f092b5..b0b2c8c85 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ CHANGELOG Roundcube Webmail =========================== +- Add possibility to add SASL mechanisms for SMTP in smtp_connect hook (#1487937) - Mark (with different color) folders with recent messages (#1486234) - Fix possible infinite redirect on attachment preview (#1488199) - Improved clickjacking protection for browsers which don't support X-Frame-Options headers diff --git a/program/include/rcube_smtp.php b/program/include/rcube_smtp.php index 5c2dd92d2..e1a6f3af0 100644 --- a/program/include/rcube_smtp.php +++ b/program/include/rcube_smtp.php @@ -50,13 +50,13 @@ class rcube_smtp public function connect($host=null, $port=null, $user=null, $pass=null) { $RCMAIL = rcmail::get_instance(); - + // disconnect/destroy $this->conn $this->disconnect(); - + // reset error/response var $this->error = $this->response = null; - + // let plugins alter smtp connection config $CONFIG = $RCMAIL->plugins->exec_hook('smtp_connect', array( 'smtp_server' => $host ? $host : $RCMAIL->config->get('smtp_server'), @@ -68,6 +68,7 @@ class rcube_smtp 'smtp_auth_type' => $RCMAIL->config->get('smtp_auth_type'), 'smtp_helo_host' => $RCMAIL->config->get('smtp_helo_host'), 'smtp_timeout' => $RCMAIL->config->get('smtp_timeout'), + 'smtp_auth_callbacks' => array(), )); $smtp_host = rcube_parse_host($CONFIG['smtp_server']); @@ -108,6 +109,14 @@ class rcube_smtp if ($RCMAIL->config->get('smtp_debug')) $this->conn->setDebug(true, array($this, 'debug_handler')); + // register authentication methods + if (!empty($CONFIG['smtp_auth_callbacks']) && method_exists($this->conn, 'setAuthMethod')) { + foreach ($CONFIG['smtp_auth_callbacks'] as $callback) { + $this->conn->setAuthMethod($callback['name'], $callback['function'], + isset($callback['prepend']) ? $callback['prepend'] : true); + } + } + // try to connect to server and exit on failure $result = $this->conn->connect($smtp_timeout); diff --git a/program/lib/Net/SMTP.php b/program/lib/Net/SMTP.php index 0463758b3..4e04f9191 100644 --- a/program/lib/Net/SMTP.php +++ b/program/lib/Net/SMTP.php @@ -62,7 +62,7 @@ class Net_SMTP * @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 @@ -187,15 +187,16 @@ class Net_SMTP $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') === true) { + $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); } /** @@ -250,7 +251,8 @@ class Net_SMTP * * @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 @@ -259,13 +261,14 @@ class Net_SMTP { $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; } /** @@ -292,7 +295,8 @@ class Net_SMTP } 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"); @@ -331,10 +335,11 @@ class Net_SMTP 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. */ @@ -366,7 +371,32 @@ class Net_SMTP } 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; } /** @@ -499,7 +529,8 @@ class Net_SMTP 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; @@ -533,13 +564,14 @@ class Net_SMTP { $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); } /** @@ -599,33 +631,27 @@ class Net_SMTP } } 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; + if (!isset($this->auth_methods[$method])) { + return PEAR::raiseError("$method is not a supported authentication method"); + } - default: - $result = PEAR::raiseError("$method is not a supported authentication method"); - break; + 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; @@ -634,6 +660,46 @@ class Net_SMTP 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; + } + /** * Authenticates the user using the DIGEST-MD5 method. * @@ -691,13 +757,14 @@ class Net_SMTP * * @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; @@ -730,13 +797,14 @@ class Net_SMTP * * @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; @@ -1012,7 +1080,16 @@ class Net_SMTP /* 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; -- cgit v1.2.3