* @author Klaus Leithoff * @author Lars Kneschke * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ include_once('Net/Sieve.php'); /** * Support for Sieve scripts * * Class can be switched to use exceptions by calling * * PEAR::setErrorHandling(PEAR_ERROR_EXCEPTION); * * In which case constructor and setters will throw exceptions for connection, login or other errors. * * retriveRules and getters will not throw an exception, if there's no script currently. * * Most methods incl. constructor accept a script-name, but by default current active script is used * and if theres no script emailadmin_sieve::DEFAULT_SCRIPT_NAME. */ class emailadmin_sieve extends Net_Sieve { /** * reference to emailadmin_imap object * * @var emailadmin_imap */ var $icServer; /** * @var string name of active script queried from Sieve server */ var $scriptName; /** * @var $rules containing the rules */ var $rules; /** * @var $vacation containing the vacation */ var $vacation; /** * @var $emailNotification containing the emailNotification */ var $emailNotification; /** * @var object $error the last PEAR error object */ var $error; /** * The timeout for the connection to the SIEVE server. * @var int */ var $_timeout = 10; /** * Switch on some error_log debug messages * * @var boolean */ var $debug = false; /** * Default script name used if no active script found on server */ const DEFAULT_SCRIPT_NAME = 'mail'; /** * Constructor * * @param emailadmin_imap $_icServer * @param string $_euser effictive user, if given the Cyrus admin account is used to login on behalf of $euser * @param string $_scriptName */ function __construct(emailadmin_imap $_icServer=null, $_euser='', $_scriptName=null) { parent::Net_Sieve(); if ($_scriptName) $this->scriptName = $_scriptName; // TODO: since we seem to have major problems authenticating via DIGEST-MD5 and CRAM-MD5 in SIEVE, we skip MD5-METHODS for now if (!is_null($_icServer)) { $_icServer->supportedAuthMethods = array('PLAIN' , 'LOGIN'); $_icServer->supportedSASLAuthMethods=array(); } else { $this->supportedAuthMethods = array('PLAIN' , 'LOGIN'); $this->supportedSASLAuthMethods=array(); } $this->displayCharset = translation::charset(); if (!is_null($_icServer) && $this->_connect($_icServer, $_euser) === 'die') { die('Sieve not activated'); } } /** * Open connection to the sieve server * * @param emailadmin_imap $_icServer * @param string $euser effictive user, if given the Cyrus admin account is used to login on behalf of $euser * @return mixed 'die' = sieve not enabled, false=connect or login failure, true=success */ function _connect(emailadmin_imap $_icServer, $euser='') { static $isConError = null; static $sieveAuthMethods = null; $_icServerID = $_icServer->acc_id; if (is_null($isConError)) { $isConError = egw_cache::getCache(egw_cache::INSTANCE, 'email', 'icServerSIEVE_connectionError' . trim($GLOBALS['egw_info']['user']['account_id'])); } if ( isset($isConError[$_icServerID]) ) { $this->error = new PEAR_Error($isConError[$_icServerID]); return false; } if ($this->debug) { error_log(__METHOD__ . array2string($euser)); } if($_icServer->acc_sieve_enabled) { if ($_icServer->acc_sieve_host) { $sieveHost = $_icServer->acc_sieve_host; } else { $sieveHost = $_icServer->acc_imap_host; } //error_log(__METHOD__.__LINE__.'->'.$sieveHost); $sievePort = $_icServer->acc_sieve_port; $useTLS = false; switch($_icServer->acc_sieve_ssl & ~emailadmin_account::SSL_VERIFY) { case emailadmin_account::SSL_SSL: $sieveHost = 'ssl://'.$sieveHost; break; case emailadmin_account::SSL_TLS: $sieveHost = 'tls://'.$sieveHost; break; case emailadmin_account::SSL_STARTTLS: $useTLS = true; } // disable certificate validation, if not explicitly enabled (not possible in current UI, as not supported by Horde_Imap_Client) $options = array( 'ssl' => array( 'verify_peer' => (bool)($_icServer->acc_sieve_ssl & emailadmin_account::SSL_VERIFY), 'verify_peer_name' => (bool)($_icServer->acc_sieve_ssl & emailadmin_account::SSL_VERIFY), ), ); if ($euser) { $username = $_icServer->acc_imap_admin_username; $password = $_icServer->acc_imap_admin_password; } else { $username = $_icServer->acc_imap_username; $password = $_icServer->acc_imap_password; } $this->icServer = $_icServer; } else { egw_cache::setCache(egw_cache::INSTANCE,'email','icServerSIEVE_connectionError'.trim($GLOBALS['egw_info']['user']['account_id']),$isConError,$expiration=60*15); return 'die'; } $this->_timeout = 10; // socket::connect sets the/this timeout on connection $timeout = emailadmin_imap::getTimeOut('SIEVE'); if ($timeout > $this->_timeout) { $this->_timeout = $timeout; } if(self::isError($this->error = $this->connect($sieveHost , $sievePort, $options, $useTLS) ) ) { if ($this->debug) { error_log(__METHOD__ . ": error in connect($sieveHost,$sievePort, " . array2string($options) . ", $useTLS): " . $this->error->getMessage()); } $isConError[$_icServerID] = $this->error->getMessage(); egw_cache::setCache(egw_cache::INSTANCE,'email','icServerSIEVE_connectionError'.trim($GLOBALS['egw_info']['user']['account_id']),$isConError,$expiration=60*15); return false; } // we cache the supported AuthMethods during session, to be able to speed up login. if (is_null($sieveAuthMethods)) { $sieveAuthMethods = & egw_cache::getSession('email', 'sieve_supportedAuthMethods'); } if (isset($sieveAuthMethods[$_icServerID])) { $this->supportedAuthMethods = $sieveAuthMethods[$_icServerID]; } if(self::isError($this->error = $this->login($username, $password, 'LOGIN', $euser) ) ) { if ($this->debug) { error_log(__METHOD__ . ": error in login($username,$password,null,$euser): " . $this->error->getMessage()); } $isConError[$_icServerID] = $this->error->getMessage(); egw_cache::setCache(egw_cache::INSTANCE,'email','icServerSIEVE_connectionError'.trim($GLOBALS['egw_info']['user']['account_id']),$isConError,$expiration=60*15); return false; } // query active script from Sieve server if (empty($this->scriptName)) { try { $this->scriptName = $this->getActive(); } catch(Exception $e) { unset($e); // ignore NOTEXISTS exception } if (empty($this->scriptName)) { $this->scriptName = self::DEFAULT_SCRIPT_NAME; } } //error_log(__METHOD__.__LINE__.array2string($this->_capability)); return true; } /** * Handles connecting to the server and checks the response validity. * overwritten function from Net_Sieve to respect timeout * * @param string $host Hostname of server. * @param string $port Port of server. * @param array $options List of options to pass to * stream_context_create(). * @param boolean $useTLS Use TLS if available. * * @return boolean True on success, PEAR_Error otherwise. */ function connect($host, $port, $options = null, $useTLS = true) { if ($this->debug) { error_log(__METHOD__ . __LINE__ . "$host, $port, " . array2string($options) . ", $useTLS"); } $this->_data['host'] = $host; $this->_data['port'] = $port; $this->_useTLS = $useTLS; if (is_array($options)) { $this->_options = array_merge((array)$this->_options, $options); } if (NET_SIEVE_STATE_DISCONNECTED != $this->_state) { return PEAR::raiseError('Not currently in DISCONNECTED state', 1); } if (self::isError($res = $this->_sock->connect($host, $port, false, ($this->_timeout?$this->_timeout:10), $options))) { return $res; } if ($this->_bypassAuth) { $this->_state = NET_SIEVE_STATE_TRANSACTION; } else { $this->_state = NET_SIEVE_STATE_AUTHORISATION; if (self::isError($res = $this->_doCmd())) { return $res; } } // Explicitly ask for the capabilities in case the connection is // picked up from an existing connection. if (self::isError($res = $this->_cmdCapability())) { return PEAR::raiseError( 'Failed to connect, server said: ' . $res->getMessage(), 2 ); } // Check if we can enable TLS via STARTTLS. if ($useTLS && !empty($this->_capability['starttls']) && function_exists('stream_socket_enable_crypto') ) { if (self::isError($res = $this->_startTLS())) { return $res; } } return true; } /** * Own _getBestAuthMethod as Net/Sieve.php assumes SASLMethods to be working * Returns the name of the best authentication method that the server * has advertised. * * @param string if !=null,check this one first if reported as serverMethod. * if so, return as bestauthmethod * @return mixed Returns a string containing the name of the best * supported authentication method or a PEAR_Error object * if a failure condition is encountered. */ function _getBestAuthMethod($userMethod = null) { //error_log(__METHOD__.__LINE__.'->'.$userMethod.'<->'.array2string($this->_capability['sasl'])); if( isset($this->_capability['sasl']) ){ $serverMethods=$this->_capability['sasl']; }else{ // if the server don't send an sasl capability fallback to login auth //return 'LOGIN'; return PEAR::raiseError("This server don't support any Auth methods SASL problem?"); } $methods = array(); if($userMethod != null ){ $methods[] = $userMethod; foreach ( $this->supportedAuthMethods as $method ) { $methods[]=$method; } }else{ $methods = $this->supportedAuthMethods; } if( ($methods != null) && ($serverMethods != null)){ foreach ( $methods as $method ) { if ( in_array( $method , $serverMethods ) ) { return $method; } } $serverMethods=implode(',' , $serverMethods ); $myMethods=implode(',' ,$this->supportedAuthMethods); return PEAR::raiseError("$method NOT supported authentication method!. This server " . "supports these methods= $serverMethods, but I support $myMethods"); }else{ return PEAR::raiseError("This server don't support any Auth methods"); } } /** * Handles the authentication using any known method * overwritten function from Net_Sieve to support fallback * * @param string $uid The userid to authenticate as. * @param string $pwd The password to authenticate with. * @param string $userMethod The method to use ( if $userMethod == '' then the class chooses the best method (the stronger is the best ) ) * @param string $euser The effective uid to authenticate as. * * @return mixed string or PEAR_Error * * @access private * @since 1.0 */ function _cmdAuthenticate($uid , $pwd , $userMethod = null , $euser = '' ) { if ( self::isError( $method = $this->_getBestAuthMethod($userMethod) ) ) { return $method; } //error_log(__METHOD__.__LINE__.' using AuthMethod: '.$method); switch ($method) { case 'DIGEST-MD5': $result = $this->_authDigestMD5( $uid , $pwd , $euser ); if (!self::isError($result)) { break; } $res = $this->_doCmd(); unset($this->_error); $this->supportedAuthMethods = array_diff($this->supportedAuthMethods,array($method,'CRAM-MD5')); return $this->_cmdAuthenticate($uid , $pwd, null, $euser); case 'CRAM-MD5': $result = $this->_authCRAMMD5( $uid , $pwd, $euser); if (!self::isError($result)) { break; } $res = $this->_doCmd(); unset($this->_error); $this->supportedAuthMethods = array_diff($this->supportedAuthMethods,array($method,'DIGEST-MD5')); return $this->_cmdAuthenticate($uid , $pwd, null, $euser); case 'LOGIN': $result = $this->_authLOGIN( $uid , $pwd , $euser ); if (!self::isError($result)) { break; } $res = $this->_doCmd(); unset($this->_error); $this->supportedAuthMethods = array_diff($this->supportedAuthMethods,array($method)); return $this->_cmdAuthenticate($uid , $pwd, null, $euser); case 'PLAIN': $result = $this->_authPLAIN( $uid , $pwd , $euser ); break; default : $result = new PEAR_Error( "$method is not a supported authentication method" ); break; } if (self::isError($result)) { return $result; } if (self::isError($res = $this->_doCmd())) { return $res; } // Query the server capabilities again now that we are authenticated. if (self::isError($res = $this->_cmdCapability())) { return PEAR::raiseError( 'Failed to connect, server said: ' . $res->getMessage(), 2 ); } return $result; } /** * Send a command and retrieves a response from the server. * * @param string $cmd The command to send. * @param boolean $auth Whether this is an authentication command. * * @return string|PEAR_Error Reponse string if an OK response, PEAR_Error * if a NO response. */ function _doCmd($cmd = '', $auth = false) { $referralCount = 0; while ($referralCount < $this->_maxReferralCount) { if (strlen($cmd)) { $error = $this->_sendCmd($cmd); if (is_a($error, 'PEAR_Error')) { return $error; } } $response = ''; while (true) { $line = $this->_recvLn(); if (is_a($line, 'PEAR_Error')) { return $line; } if (preg_match('/^(OK|NO)/i', $line, $tag)) { // Check for string literal message. // ServerResponse may send {nm} (nm representing a number) // dbmail (in some versions) sends: {nm+} thus breaking RFC5804 rules (Section 4 Formal Syntax) // {nm+} may only be used in communicating from client TO server; (not Server to Client) // we work around this bug (allowing +) using a patch introduced with roundcube 2 years ago. //if (preg_match('/{([0-9]+)}$/', $line, $matches)) { //original if (preg_match('/{([0-9]+)\+?}$/', $line, $matches)) { //patched to cope with dbmail $line = substr($line, 0, -(strlen($matches[1]) + 2)) . str_replace( "\r\n", ' ', $this->_recvBytes($matches[1] + 2) ); } if ('OK' == $this->_toUpper($tag[1])) { $response .= $line; return rtrim($response); } return $this->_pear->raiseError(trim($response . substr($line, 2)), 3); } if (preg_match('/^BYE/i', $line)) { $error = $this->disconnect(false); if (is_a($error, 'PEAR_Error')) { return $this->_pear->raiseError( 'Cannot handle BYE, the error was: ' . $error->getMessage(), 4 ); } // Check for referral, then follow it. Otherwise, carp an // error. if (preg_match('/^bye \(referral "(sieve:\/\/)?([^"]+)/i', $line, $matches)) { // Replace the old host with the referral host // preserving any protocol prefix. $this->_data['host'] = preg_replace( '/\w+(?!(\w|\:\/\/)).*/', $matches[2], $this->_data['host'] ); $error = $this->_handleConnectAndLogin(); if (is_a($error, 'PEAR_Error')) { return $this->_pear->raiseError( 'Cannot follow referral to ' . $this->_data['host'] . ', the error was: ' . $error->getMessage(), 5 ); } break; } return $this->_pear->raiseError(trim($response . $line), 6); } // ServerResponse may send {nm} (nm representing a number) // dbmail (in some versions) sends: {nm+} thus breaking RFC5804 rules (Section 4 Formal Syntax) // {nm+} may only be used in communicating from client TO server; (not Server to Client) // we work around this bug (allowing +) using a patch introduced with roundcube 2 years ago. // although roundcube suggested only the change in line //if (preg_match('/^{([0-9]+)}/', $line, $matches)) { //original if (preg_match('/^{([0-9]+)\+?}/', $line, $matches)) { //patched to cope with dbmail // Matches literal string responses. $line = $this->_recvBytes($matches[1] + 2); if (!$auth) { // Receive the pending OK only if we aren't // authenticating since string responses during // authentication don't need an OK. $this->_recvLn(); } return $line; } if ($auth) { // String responses during authentication don't need an // OK. $response .= $line; return rtrim($response); } $response .= $line . "\r\n"; $referralCount++; } } return $this->_pear->raiseError('Max referral count (' . $referralCount . ') reached. Cyrus murder loop error?', 7); } function getRules() { if (!isset($this->rules)) $this->retrieveRules(); return $this->rules; } function getVacation() { if (!isset($this->rules)) $this->retrieveRules(); return $this->vacation; } function getEmailNotification() { if (!isset($this->rules)) $this->retrieveRules(); return $this->emailNotification; } /** * Set email notifications * * @param array $_rules * @param string $_scriptName */ function setRules(array $_rules, $_scriptName=null) { $script = $this->retrieveRules($_scriptName); $script->debug = $this->debug; $script->rules = $_rules; $ret = $script->updateScript($this); $this->error = $script->errstr; return $ret; } /** * Set email notifications * * @param array $_vacation * @param string $_scriptName */ function setVacation(array $_vacation, $_scriptName=null) { if ($this->debug) { error_log(__METHOD__ . "($_scriptName," . print_r($_vacation, true) . ')'); } $script = $this->retrieveRules($_scriptName); $script->debug = $this->debug; $script->vacation = $_vacation; $ret = $script->updateScript($this); $this->error = $script->errstr; return $ret; } /** * Set email notifications * * @param array $_emailNotification * @param string $_scriptName * @return emailadmin_script */ function setEmailNotification(array $_emailNotification, $_scriptName=null) { if ($_emailNotification['externalEmail'] == '' || !preg_match("/\@/",$_emailNotification['externalEmail'])) { $_emailNotification['status'] = 'off'; $_emailNotification['externalEmail'] = ''; } $script = $this->retrieveRules($_scriptName); $script->emailNotification = $_emailNotification; $ret = $script->updateScript($this); $this->error = $script->errstr; return $ret; } /** * Retrive rules, vacation, notifications and return emailadmin_script object to update them * * @param string $_scriptName * @return emailadmin_script */ function retrieveRules($_scriptName=null) { if (!$_scriptName) { $_scriptName = $this->scriptName; } $script = new emailadmin_script($_scriptName); try { $script->retrieveRules($this); } catch (Exception $e) { unset($e); // ignore not found script exception } $this->rules =& $script->rules; $this->vacation =& $script->vacation; $this->emailNotification =& $script->emailNotification; // Added email notifications return $script; } /** * Tell whether a value is a PEAR error. * * Implemented here to get arround: PHP Deprecated: Non-static method self::isError() should not be called statically * * @param mixed $data the value to test * @param int $code if $data is an error object, return true * only if $code is a string and * $obj->getMessage() == $code or * $code is an integer and $obj->getCode() == $code * @access public * @return bool true if parameter is an error */ protected static function isError($data, $code = null) { static $pear=null; if (!isset($pear)) $pear = new PEAR(); return $pear->isError($data, $code); } }