egroupware/emailadmin/inc/class.emailadmin_sieve.inc.php

672 lines
22 KiB
PHP

<?php
/**
* EGroupware EMailAdmin: Support for Sieve scripts
*
* @link http://www.egroupware.org
* @package emailadmin
* @author Ralf Becker <rb@stylite.de>
* @author Klaus Leithoff <kl@stylite.de>
* @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);
}
}