diff --git a/plugins/managesieve/Changelog b/plugins/managesieve/Changelog new file mode 100644 index 000000000..21ed307a4 --- /dev/null +++ b/plugins/managesieve/Changelog @@ -0,0 +1,59 @@ +* version 1.0 [2009-05-21] +----------------------------------------------------------- +Rewritten using plugin API +Added hu_HU localization (Tamas Tevesz) + +* version beta7 (svn-r2300) [2009-03-01] +----------------------------------------------------------- +Added SquirrelMail script auto-import (Jonathan Ernst) +Added 'vacation' support (Jonathan Ernst & alec) +Added 'stop' support (Jonathan Ernst) +Added option for extensions disabling (Jonathan Ernst & alec) +Added fi_FI, nl_NL, bg_BG localization +Small style fixes + +* version 0.2-stable1 (svn-r2205) [2009-01-03] +----------------------------------------------------------- +Fix moving down filter row +Fixes for compressed js files in stable release package +Created patch for svn version r2205 + +* version 0.2-stable [2008-12-31] +----------------------------------------------------------- +Added ru_RU, fr_FR, zh_CN translation +Fixes for Roundcube 0.2-stable + +* version rc0.2beta [2008-09-21] +----------------------------------------------------------- +Small css fixes for IE +Fixes for Roundcube 0.2-beta + +* version beta6 [2008-08-08] +----------------------------------------------------------- +Added de_DE translation +Fix for Roundcube r1634 + +* version beta5 [2008-06-10] +----------------------------------------------------------- +Fixed 'exists' operators +Fixed 'not*' operators for custom headers +Fixed filters deleting + +* version beta4 [2008-06-09] +----------------------------------------------------------- +Fix for Roundcube r1490 + +* version beta3 [2008-05-22] +----------------------------------------------------------- +Fixed textarea error class setting +Added pagetitle setting +Added option 'managesieve_replace_delimiter' +Fixed errors on IE (still need some css fixes) + +* version beta2 [2008-05-20] +----------------------------------------------------------- +Use 'if' only for first filter and 'elsif' for the rest + +* version beta1 [2008-05-15] +----------------------------------------------------------- +Initial version for Roundcube r1388. diff --git a/plugins/managesieve/lib/Net/Sieve.php b/plugins/managesieve/lib/Net/Sieve.php new file mode 100644 index 000000000..bc0bcc8f2 --- /dev/null +++ b/plugins/managesieve/lib/Net/Sieve.php @@ -0,0 +1,1159 @@ + | +// | Co-Author: Damian Fernandez Sosa | +// | Co-Author: Anish Mistry | +// +-----------------------------------------------------------------------+ + +require_once('Net/Socket.php'); + +/** +* TODO +* +* o supportsAuthMech() +*/ + +/** +* Disconnected state +* @const NET_SIEVE_STATE_DISCONNECTED +*/ +define('NET_SIEVE_STATE_DISCONNECTED', 1, true); + +/** +* Authorisation state +* @const NET_SIEVE_STATE_AUTHORISATION +*/ +define('NET_SIEVE_STATE_AUTHORISATION', 2, true); + +/** +* Transaction state +* @const NET_SIEVE_STATE_TRANSACTION +*/ +define('NET_SIEVE_STATE_TRANSACTION', 3, true); + +/** +* A class for talking to the timsieved server which +* comes with Cyrus IMAP. +* +* SIEVE: RFC3028 http://www.ietf.org/rfc/rfc3028.txt +* MANAGE-SIEVE: http://www.ietf.org/internet-drafts/draft-martin-managesieve-07.txt +* +* @author Richard Heyes +* @author Damian Fernandez Sosa +* @author Anish Mistry +* @access public +* @version 1.2.0 +* @package Net_Sieve +*/ + +class Net_Sieve +{ + /** + * The socket object + * @var object + */ + var $_sock; + + /** + * Info about the connect + * @var array + */ + var $_data; + + /** + * Current state of the connection + * @var integer + */ + var $_state; + + /** + * Constructor error is any + * @var object + */ + var $_error; + + /** + * To allow class debuging + * @var boolean + */ + var $_debug = false; + + /** + * Allows picking up of an already established connection + * @var boolean + */ + var $_bypassAuth = false; + + /** + * Whether to use TLS if available + * @var boolean + */ + var $_useTLS = true; + + /** + * The auth methods this class support + * @var array + */ + var $supportedAuthMethods=array('DIGEST-MD5', 'CRAM-MD5', 'PLAIN' , 'LOGIN'); + //if you have problems using DIGEST-MD5 authentication please comment the line above and uncomment the following line + //var $supportedAuthMethods=array( 'CRAM-MD5', 'PLAIN' , 'LOGIN'); + + //var $supportedAuthMethods=array( 'PLAIN' , 'LOGIN'); + + /** + * The auth methods this class support + * @var array + */ + var $supportedSASLAuthMethods=array('DIGEST-MD5', 'CRAM-MD5'); + + /** + * Handles posible referral loops + * @var array + */ + var $_maxReferralCount = 15; + + /** + * Constructor + * Sets up the object, connects to the server and logs in. stores + * any generated error in $this->_error, which can be retrieved + * using the getError() method. + * + * @param string $user Login username + * @param string $pass Login password + * @param string $host Hostname of server + * @param string $port Port of server + * @param string $logintype Type of login to perform + * @param string $euser Effective User (if $user=admin, login as $euser) + * @param string $bypassAuth Skip the authentication phase. Useful if the socket + is already open. + * @param boolean $useTLS Use TLS if available + */ + function Net_Sieve($user = null , $pass = null , $host = 'localhost', $port = 2000, $logintype = '', $euser = '', $debug = false, $bypassAuth = false, $useTLS = true) + { + $this->_state = NET_SIEVE_STATE_DISCONNECTED; + $this->_data['user'] = $user; + $this->_data['pass'] = $pass; + $this->_data['host'] = $host; + $this->_data['port'] = $port; + $this->_data['logintype'] = $logintype; + $this->_data['euser'] = $euser; + $this->_sock = &new Net_Socket(); + $this->_debug = $debug; + $this->_bypassAuth = $bypassAuth; + $this->_useTLS = $useTLS; + /* + * 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) { + if($this->_debug){ + echo "AUTH_SASL NOT PRESENT!\n"; + } + foreach($this->supportedSASLAuthMethods as $SASLMethod){ + $pos = array_search( $SASLMethod, $this->supportedAuthMethods ); + if($this->_debug){ + echo "DISABLING METHOD $SASLMethod\n"; + } + unset($this->supportedAuthMethods[$pos]); + } + } + if( ($user != null) && ($pass != null) ){ + $this->_error = $this->_handleConnectAndLogin(); + } + } + + /** + * Handles the errors the class can find + * on the server + * + * @access private + * @param mixed $msg Text error message or PEAR error object + * @param integer $code Numeric error code + * @return PEAR_Error + */ + function _raiseError($msg, $code) + { + include_once 'PEAR.php'; + return PEAR::raiseError($msg, $code); + } + + /** + * Handles connect and login. + * on the server + * + * @access private + * @return mixed Indexed array of scriptnames or PEAR_Error on failure + */ + function _handleConnectAndLogin() + { + if (PEAR::isError($res = $this->connect($this->_data['host'] , $this->_data['port'], null, $this->_useTLS ))) { + return $res; + } + if($this->_bypassAuth === false) { + if (PEAR::isError($res = $this->login($this->_data['user'], $this->_data['pass'], $this->_data['logintype'] , $this->_data['euser'] , $this->_bypassAuth) ) ) { + return $res; + } + } + return true; + } + + /** + * Returns an indexed array of scripts currently + * on the server + * + * @return mixed Indexed array of scriptnames or PEAR_Error on failure + */ + function listScripts() + { + if (is_array($scripts = $this->_cmdListScripts())) { + $this->_active = $scripts[1]; + return $scripts[0]; + } else { + return $scripts; + } + } + + /** + * Returns the active script + * + * @return mixed The active scriptname or PEAR_Error on failure + */ + function getActive() + { + if (!empty($this->_active)) { + return $this->_active; + + } elseif (is_array($scripts = $this->_cmdListScripts())) { + $this->_active = $scripts[1]; + return $scripts[1]; + } + } + + /** + * Sets the active script + * + * @param string $scriptname The name of the script to be set as active + * @return mixed true on success, PEAR_Error on failure + */ + function setActive($scriptname) + { + return $this->_cmdSetActive($scriptname); + } + + /** + * Retrieves a script + * + * @param string $scriptname The name of the script to be retrieved + * @return mixed The script on success, PEAR_Error on failure + */ + function getScript($scriptname) + { + return $this->_cmdGetScript($scriptname); + } + + /** + * Adds a script to the server + * + * @param string $scriptname Name of the script + * @param string $script The script + * @param boolean $makeactive Whether to make this the active script + * @return mixed true on success, PEAR_Error on failure + */ + function installScript($scriptname, $script, $makeactive = false) + { + if (PEAR::isError($res = $this->_cmdPutScript($scriptname, $script))) { + return $res; + + } elseif ($makeactive) { + return $this->_cmdSetActive($scriptname); + + } else { + return true; + } + } + + /** + * Removes a script from the server + * + * @param string $scriptname Name of the script + * @return mixed True on success, PEAR_Error on failure + */ + function removeScript($scriptname) + { + return $this->_cmdDeleteScript($scriptname); + } + + /** + * Returns any error that may have been generated in the + * constructor + * + * @return mixed False if no error, PEAR_Error otherwise + */ + function getError() + { + return PEAR::isError($this->_error) ? $this->_error : false; + } + + /** + * Handles connecting to the server and checking the + * response is valid. + * + * @access private + * @param string $host Hostname of server + * @param string $port Port of server + * @param array $options List of options to pass to connect + * @param boolean $useTLS Use TLS if available + * @return mixed True on success, PEAR_Error otherwise + */ + function connect($host, $port, $options = null, $useTLS = true) + { + if (NET_SIEVE_STATE_DISCONNECTED != $this->_state) { + $msg='Not currently in DISCONNECTED state'; + $code=1; + return $this->_raiseError($msg,$code); + } + + if (PEAR::isError($res = $this->_sock->connect($host, $port, false, 5, $options))) { + return $res; + } + + if($this->_bypassAuth === false) { + $this->_state = NET_SIEVE_STATE_AUTHORISATION; + if (PEAR::isError($res = $this->_doCmd())) { + return $res; + } + } else { + $this->_state = NET_SIEVE_STATE_TRANSACTION; + } + + // Explicitly ask for the capabilities in case the connection + // is picked up from an existing connection. + if(PEAR::isError($res = $this->_cmdCapability() )) { + $msg='Failed to connect, server said: ' . $res->getMessage(); + $code=2; + return $this->_raiseError($msg,$code); + } + + // Get logon greeting/capability and parse + $this->_parseCapability($res); + + if($useTLS === true) { + // check if we can enable TLS via STARTTLS + if(isset($this->_capability['starttls']) && function_exists('stream_socket_enable_crypto') === true) { + if (PEAR::isError($res = $this->_startTLS())) { + return $res; + } + } + } + + return true; + } + + /** + * Logs into server. + * + * @param string $user Login username + * @param string $pass Login password + * @param string $logintype Type of login method to use + * @param string $euser Effective UID (perform on behalf of $euser) + * @param boolean $bypassAuth Do not perform authentication + * @return mixed True on success, PEAR_Error otherwise + */ + function login($user, $pass, $logintype = null , $euser = '', $bypassAuth = false) + { + if (NET_SIEVE_STATE_AUTHORISATION != $this->_state) { + $msg='Not currently in AUTHORISATION state'; + $code=1; + return $this->_raiseError($msg,$code); + } + + if( $bypassAuth === false ){ + if(PEAR::isError($res=$this->_cmdAuthenticate($user , $pass , $logintype, $euser ) ) ){ + return $res; + } + } + $this->_state = NET_SIEVE_STATE_TRANSACTION; + return true; + } + + /** + * Handles the authentication using any known method + * + * @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 ( PEAR::isError( $method = $this->_getBestAuthMethod($userMethod) ) ) { + return $method; + } + switch ($method) { + case 'DIGEST-MD5': + $result = $this->_authDigest_MD5( $uid , $pwd , $euser ); + return $result; + break; + case 'CRAM-MD5': + $result = $this->_authCRAM_MD5( $uid , $pwd, $euser); + break; + case 'LOGIN': + $result = $this->_authLOGIN( $uid , $pwd , $euser ); + break; + case 'PLAIN': + $result = $this->_authPLAIN( $uid , $pwd , $euser ); + break; + default : + $result = new PEAR_Error( "$method is not a supported authentication method" ); + break; + } + + if (PEAR::isError($res = $this->_doCmd() )) { + return $res; + } + return $result; + } + + /** + * Authenticates the user using the PLAIN method. + * + * @param string $user The userid to authenticate as. + * @param string $pass The password to authenticate with. + * @param string $euser The effective uid to authenticate as. + * + * @return array Returns an array containing the response + * + * @access private + * @since 1.0 + */ + function _authPLAIN($user, $pass , $euser ) + { + if ($euser != '') { + $cmd=sprintf('AUTHENTICATE "PLAIN" "%s"', base64_encode($euser . chr(0) . $user . chr(0) . $pass ) ) ; + } else { + $cmd=sprintf('AUTHENTICATE "PLAIN" "%s"', base64_encode( chr(0) . $user . chr(0) . $pass ) ); + } + return $this->_sendCmd( $cmd ) ; + } + + /** + * Authenticates the user using the PLAIN method. + * + * @param string $user The userid to authenticate as. + * @param string $pass The password to authenticate with. + * @param string $euser The effective uid to authenticate as. + * + * @return array Returns an array containing the response + * + * @access private + * @since 1.0 + */ + function _authLOGIN($user, $pass , $euser ) + { + $this->_sendCmd('AUTHENTICATE "LOGIN"'); + $this->_doCmd(sprintf('"%s"', base64_encode($user))); + $this->_doCmd(sprintf('"%s"', base64_encode($pass))); + } + + /** + * Authenticates the user using the CRAM-MD5 method. + * + * @param string $uid The userid to authenticate as. + * @param string $pwd The password to authenticate with. + * @param string $euser The effective uid to authenticate as. + * + * @return array Returns an array containing the response + * + * @access private + * @since 1.0 + */ + function _authCRAM_MD5($uid, $pwd, $euser) + { + if ( PEAR::isError( $challenge = $this->_doCmd( 'AUTHENTICATE "CRAM-MD5"' ) ) ) { + $this->_error=$challenge; + return $challenge; + } + $challenge=trim($challenge); + $challenge = base64_decode( trim($challenge) ); + $cram = &Auth_SASL::factory('crammd5'); + if ( PEAR::isError($resp=$cram->getResponse( $uid , $pwd , $challenge ) ) ) { + $this->_error=$resp; + return $resp; + } + $auth_str = base64_encode( $resp ); + if ( PEAR::isError($error = $this->_sendStringResponse( $auth_str ) ) ) { + $this->_error=$error; + return $error; + } + + } + + /** + * Authenticates the user using the DIGEST-MD5 method. + * + * @param string $uid The userid to authenticate as. + * @param string $pwd The password to authenticate with. + * @param string $euser The effective uid to authenticate as. + * + * @return array Returns an array containing the response + * + * @access private + * @since 1.0 + */ + function _authDigest_MD5($uid, $pwd, $euser) + { + if ( PEAR::isError( $challenge = $this->_doCmd('AUTHENTICATE "DIGEST-MD5"') ) ) { + $this->_error= $challenge; + return $challenge; + } + $challenge = base64_decode( $challenge ); + $digest = &Auth_SASL::factory('digestmd5'); + + if(PEAR::isError($param=$digest->getResponse($uid, $pwd, $challenge, "localhost", "sieve" , $euser) )) { + return $param; + } + $auth_str = base64_encode($param); + + if ( PEAR::isError($error = $this->_sendStringResponse( $auth_str ) ) ) { + $this->_error=$error; + return $error; + } + + if ( PEAR::isError( $challenge = $this->_doCmd() ) ) { + $this->_error=$challenge ; + return $challenge ; + } + + if( strtoupper(substr($challenge,0,2))== 'OK' ){ + return true; + } + + /** + * We don't use the protocol's third step because SIEVE doesn't allow + * subsequent authentication, so we just silently ignore it. + */ + if ( PEAR::isError($error = $this->_sendStringResponse( '' ) ) ) { + $this->_error=$error; + return $error; + } + + if (PEAR::isError($res = $this->_doCmd() )) { + return $res; + } + } + + /** + * Removes a script from the server + * + * @access private + * @param string $scriptname Name of the script to delete + * @return mixed True on success, PEAR_Error otherwise + */ + function _cmdDeleteScript($scriptname) + { + if (NET_SIEVE_STATE_TRANSACTION != $this->_state) { + $msg='Not currently in AUTHORISATION state'; + $code=1; + return $this->_raiseError($msg,$code); + } + if (PEAR::isError($res = $this->_doCmd(sprintf('DELETESCRIPT "%s"', $scriptname) ) )) { + return $res; + } + return true; + } + + /** + * Retrieves the contents of the named script + * + * @access private + * @param string $scriptname Name of the script to retrieve + * @return mixed The script if successful, PEAR_Error otherwise + */ + function _cmdGetScript($scriptname) + { + if (NET_SIEVE_STATE_TRANSACTION != $this->_state) { + $msg='Not currently in AUTHORISATION state'; + $code=1; + return $this->_raiseError($msg,$code); + } + + if (PEAR::isError($res = $this->_doCmd(sprintf('GETSCRIPT "%s"', $scriptname) ) ) ) { + return $res; + } + + return preg_replace('/{[0-9]+}\r\n/', '', $res); + } + + /** + * Sets the ACTIVE script, ie the one that gets run on new mail + * by the server + * + * @access private + * @param string $scriptname The name of the script to mark as active + * @return mixed True on success, PEAR_Error otherwise + */ + function _cmdSetActive($scriptname) + { + if (NET_SIEVE_STATE_TRANSACTION != $this->_state) { + $msg='Not currently in AUTHORISATION state'; + $code=1; + return $this->_raiseError($msg,$code); + } + + if (PEAR::isError($res = $this->_doCmd(sprintf('SETACTIVE "%s"', $scriptname) ) ) ) { + return $res; + } + + $this->_activeScript = $scriptname; + return true; + } + + /** + * Sends the LISTSCRIPTS command + * + * @access private + * @return mixed Two item array of scripts, and active script on success, + * PEAR_Error otherwise. + */ + function _cmdListScripts() + { + if (NET_SIEVE_STATE_TRANSACTION != $this->_state) { + $msg='Not currently in AUTHORISATION state'; + $code=1; + return $this->_raiseError($msg,$code); + } + + $scripts = array(); + $activescript = null; + + if (PEAR::isError($res = $this->_doCmd('LISTSCRIPTS'))) { + return $res; + } + + $res = explode("\r\n", $res); + + foreach ($res as $value) { + if (preg_match('/^"(.*)"( ACTIVE)?$/i', $value, $matches)) { + $scripts[] = $matches[1]; + if (!empty($matches[2])) { + $activescript = $matches[1]; + } + } + } + + return array($scripts, $activescript); + } + + /** + * Sends the PUTSCRIPT command to add a script to + * the server. + * + * @access private + * @param string $scriptname Name of the new script + * @param string $scriptdata The new script + * @return mixed True on success, PEAR_Error otherwise + */ + function _cmdPutScript($scriptname, $scriptdata) + { + if (NET_SIEVE_STATE_TRANSACTION != $this->_state) { + $msg='Not currently in TRANSACTION state'; + $code=1; + return $this->_raiseError($msg,$code); + } + + $stringLength = $this->_getLineLength($scriptdata); + + if (PEAR::isError($res = $this->_doCmd(sprintf("PUTSCRIPT \"%s\" {%d+}\r\n%s", $scriptname, $stringLength, $scriptdata) ))) { + return $res; + } + + return true; + } + + /** + * Sends the LOGOUT command and terminates the connection + * + * @access private + * @param boolean $sendLogoutCMD True to send LOGOUT command before disconnecting + * @return mixed True on success, PEAR_Error otherwise + */ + function _cmdLogout($sendLogoutCMD=true) + { + if (NET_SIEVE_STATE_DISCONNECTED === $this->_state) { + $msg='Not currently connected'; + $code=1; + return $this->_raiseError($msg,$code); + //return PEAR::raiseError('Not currently connected'); + } + + if($sendLogoutCMD){ + if (PEAR::isError($res = $this->_doCmd('LOGOUT'))) { + return $res; + } + } + + $this->_sock->disconnect(); + $this->_state = NET_SIEVE_STATE_DISCONNECTED; + return true; + } + + /** + * Sends the CAPABILITY command + * + * @access private + * @return mixed True on success, PEAR_Error otherwise + */ + function _cmdCapability() + { + if (NET_SIEVE_STATE_DISCONNECTED === $this->_state) { + $msg='Not currently connected'; + $code=1; + return $this->_raiseError($msg,$code); + } + + if (PEAR::isError($res = $this->_doCmd('CAPABILITY'))) { + return $res; + } + $this->_parseCapability($res); + return true; + } + + /** + * Checks if the server has space to store the script + * by the server + * + * @param string $scriptname The name of the script to mark as active + * @param integer $size The size of the script + * @return mixed True on success, PEAR_Error otherwise + */ + function haveSpace($scriptname,$size) + { + if (NET_SIEVE_STATE_TRANSACTION != $this->_state) { + $msg='Not currently in TRANSACTION state'; + $code=1; + return $this->_raiseError($msg,$code); + } + + if (PEAR::isError($res = $this->_doCmd(sprintf('HAVESPACE "%s" %d', $scriptname, $size) ) ) ) { + return $res; + } + + return true; + } + + /** + * Parses the response from the capability command. Stores + * the result in $this->_capability + * + * @access private + * @param string $data The response from the capability command + */ + function _parseCapability($data) + { + $data = preg_split('/\r?\n/', $data, -1, PREG_SPLIT_NO_EMPTY); + + for ($i = 0; $i < count($data); $i++) { + if (preg_match('/^"([a-z]+)"( "(.*)")?$/i', $data[$i], $matches)) { + switch (strtolower($matches[1])) { + case 'implementation': + $this->_capability['implementation'] = $matches[3]; + break; + + case 'sasl': + $this->_capability['sasl'] = preg_split('/\s+/', $matches[3]); + break; + + case 'sieve': + $this->_capability['extensions'] = preg_split('/\s+/', $matches[3]); + break; + + case 'starttls': + $this->_capability['starttls'] = true; + break; + } + } + } + } + + /** + * Sends a command to the server + * + * @access private + * @param string $cmd The command to send + */ + function _sendCmd($cmd) + { + $status = $this->_sock->getStatus(); + if (PEAR::isError($status) || $status['eof']) { + return new PEAR_Error( 'Failed to write to socket: (connection lost!) ' ); + } + if ( PEAR::isError( $error = $this->_sock->write( $cmd . "\r\n" ) ) ) { + return new PEAR_Error( 'Failed to write to socket: ' . $error->getMessage() ); + } + + if( $this->_debug ){ + // C: means this data was sent by the client (this class) + echo "C:$cmd\n"; + } + return true; + } + + /** + * Sends a string response to the server + * + * @access private + * @param string $cmd The command to send + */ + function _sendStringResponse($str) + { + $response='{' . $this->_getLineLength($str) . "+}\r\n" . $str ; + return $this->_sendCmd($response); + } + + function _recvLn() + { + $lastline=''; + if (PEAR::isError( $lastline = $this->_sock->gets( 8192 ) ) ) { + return new PEAR_Error( 'Failed to write to socket: ' . $lastline->getMessage() ); + } + $lastline=rtrim($lastline); + if($this->_debug){ + // S: means this data was sent by the IMAP Server + echo "S:$lastline\n" ; + } + + if( $lastline === '' ) { + return new PEAR_Error( 'Failed to receive from the socket' ); + } + + return $lastline; + } + + /** + * Send a command and retrieves a response from the server. + * + * + * @access private + * @param string $cmd The command to send + * @return mixed Reponse string if an OK response, PEAR_Error if a NO response + */ + function _doCmd($cmd = '' ) + { + $referralCount=0; + while($referralCount < $this->_maxReferralCount ){ + + if($cmd != '' ){ + if(PEAR::isError($error = $this->_sendCmd($cmd) )) { + return $error; + } + } + $response = ''; + + while (true) { + if(PEAR::isError( $line=$this->_recvLn() )){ + return $line; + } + if ('ok' === strtolower(substr($line, 0, 2))) { + $response .= $line; + return rtrim($response); + + } elseif ('no' === strtolower(substr($line, 0, 2))) { + // Check for string literal error message + if (preg_match('/^no {([0-9]+)\+?}/i', $line, $matches)) { + $line .= str_replace("\r\n", ' ', $this->_sock->read($matches[1] + 2 )); + if($this->_debug){ + echo "S:$line\n"; + } + } + $msg=trim($response . substr($line, 2)); + $code=3; + return $this->_raiseError($msg,$code); + } elseif ('bye' === strtolower(substr($line, 0, 3))) { + + if(PEAR::isError($error = $this->disconnect(false) ) ){ + $msg="Can't handle bye, The error was= " . $error->getMessage() ; + $code=4; + return $this->_raiseError($msg,$code); + } + //if (preg_match('/^bye \(referral "([^"]+)/i', $line, $matches)) { + if (preg_match('/^bye \(referral "(sieve:\/\/)?([^"]+)/i', $line, $matches)) { + // Check for referral, then follow it. Otherwise, carp an error. + // Replace the old host with the referral host preserving any protocol prefix + $this->_data['host'] = preg_replace('/\w+(?!(\w|\:\/\/)).*/',$matches[2],$this->_data['host']); + if (PEAR::isError($error = $this->_handleConnectAndLogin() ) ){ + $msg="Can't follow referral to " . $this->_data['host'] . ", The error was= " . $error->getMessage() ; + $code=5; + return $this->_raiseError($msg,$code); + } + break; + // Retry the command + if(PEAR::isError($error = $this->_sendCmd($cmd) )) { + return $error; + } + continue; + } + $msg=trim($response . $line); + $code=6; + return $this->_raiseError($msg,$code); + } elseif (preg_match('/^{([0-9]+)\+?}/i', $line, $matches)) { + // Matches String Responses. + //$line = str_replace("\r\n", ' ', $this->_sock->read($matches[1] + 2 )); + $str_size = $matches[1] + 2; + $line = ''; + $line_length = 0; + while ($line_length < $str_size) { + $line .= $this->_sock->read($str_size - $line_length); + $line_length = $this->_getLineLength($line); + } + if($this->_debug){ + echo "S:$line\n"; + } + if($this->_state != NET_SIEVE_STATE_AUTHORISATION) { + // receive the pending OK only if we aren't authenticating + // since string responses during authentication don't need an + // OK. + $this->_recvLn(); + } + return $line; + } + $response .= $line . "\r\n"; + $referralCount++; + } + } + $msg="Max referral count reached ($referralCount times) Cyrus murder loop error?"; + $code=7; + return $this->_raiseError($msg,$code); + } + + /** + * Sets the debug state + * + * @param boolean $debug + * @return void + */ + function setDebug($debug = true) + { + $this->_debug = $debug; + } + + /** + * Disconnect from the Sieve server + * + * @param string $scriptname The name of the script to be set as active + * @return mixed true on success, PEAR_Error on failure + */ + function disconnect($sendLogoutCMD=true) + { + return $this->_cmdLogout($sendLogoutCMD); + } + + /** + * Returns the name of the best authentication method that the server + * has advertised. + * + * @param string if !=null,authenticate with this method ($userMethod). + * + * @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. + * @access private + * @since 1.0 + */ + function _getBestAuthMethod($userMethod = null) + { + 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 new PEAR_Error("This server don't support any Auth methods SASL problem?"); + } + + if($userMethod != null ){ + $methods = array(); + $methods[] = $userMethod; + }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 new PEAR_Error("$method NOT supported authentication method!. This server " . + "supports these methods= $serverMethods, but I support $myMethods"); + }else{ + return new PEAR_Error("This server don't support any Auth methods"); + } + } + + /** + * Return the list of extensions the server supports + * + * @return mixed array on success, PEAR_Error on failure + */ + function getExtensions() + { + if (NET_SIEVE_STATE_DISCONNECTED === $this->_state) { + $msg='Not currently connected'; + $code=7; + return $this->_raiseError($msg,$code); + } + + return $this->_capability['extensions']; + } + + /** + * Return true if tyhe server has that extension + * + * @param string the extension to compare + * @return mixed array on success, PEAR_Error on failure + */ + function hasExtension($extension) + { + if (NET_SIEVE_STATE_DISCONNECTED === $this->_state) { + $msg='Not currently connected'; + $code=7; + return $this->_raiseError($msg,$code); + } + + if(is_array($this->_capability['extensions'] ) ){ + foreach( $this->_capability['extensions'] as $ext){ + if( trim( strtolower( $ext ) ) === trim( strtolower( $extension ) ) ) + return true; + } + } + return false; + } + + /** + * Return the list of auth methods the server supports + * + * @return mixed array on success, PEAR_Error on failure + */ + function getAuthMechs() + { + if (NET_SIEVE_STATE_DISCONNECTED === $this->_state) { + $msg='Not currently connected'; + $code=7; + return $this->_raiseError($msg,$code); + } + if(!isset($this->_capability['sasl']) ){ + $this->_capability['sasl']=array(); + } + return $this->_capability['sasl']; + } + + /** + * Return true if the server has that extension + * + * @param string the extension to compare + * @return mixed array on success, PEAR_Error on failure + */ + function hasAuthMech($method) + { + if (NET_SIEVE_STATE_DISCONNECTED === $this->_state) { + $msg='Not currently connected'; + $code=7; + return $this->_raiseError($msg,$code); + //return PEAR::raiseError('Not currently connected'); + } + + if(is_array($this->_capability['sasl'] ) ){ + foreach( $this->_capability['sasl'] as $ext){ + if( trim( strtolower( $ext ) ) === trim( strtolower( $method ) ) ) + return true; + } + } + return false; + } + + /** + * Return true if the TLS negotiation was successful + * + * @access private + * @return mixed true on success, PEAR_Error on failure + */ + function _startTLS() + { + if (PEAR::isError($res = $this->_doCmd("STARTTLS"))) { + return $res; + } + + if(stream_socket_enable_crypto($this->_sock->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT) == false) { + $msg='Failed to establish TLS connection'; + $code=2; + return $this->_raiseError($msg,$code); + } + + if($this->_debug === true) { + echo "STARTTLS Negotiation Successful\n"; + } + + // skip capability strings received after AUTHENTICATE + // wait for OK "TLS negotiation successful." + if(PEAR::isError($ret = $this->_doCmd() )) { + $msg='Failed to establish TLS connection, server said: ' . $res->getMessage(); + $code=2; + return $this->_raiseError($msg,$code); + } + + // RFC says we need to query the server capabilities again + // @TODO: don;'t call for capabilities if they are returned + // in tls negotiation result above + if(PEAR::isError($res = $this->_cmdCapability() )) { + $msg='Failed to connect, server said: ' . $res->getMessage(); + $code=2; + return $this->_raiseError($msg,$code); + } + return true; + } + + function _getLineLength($string) { + if (extension_loaded('mbstring') || @dl(PHP_SHLIB_PREFIX.'mbstring.'.PHP_SHLIB_SUFFIX)) { + return mb_strlen($string,'latin1'); + } else { + return strlen($string); + } + } +} +?> diff --git a/plugins/managesieve/lib/rcube_sieve.php b/plugins/managesieve/lib/rcube_sieve.php new file mode 100644 index 000000000..599bfdfed --- /dev/null +++ b/plugins/managesieve/lib/rcube_sieve.php @@ -0,0 +1,729 @@ + + + $Id$ + +*/ + +// Sieve Language Basics: http://www.ietf.org/rfc/rfc5228.txt + +define('SIEVE_ERROR_CONNECTION', 1); +define('SIEVE_ERROR_LOGIN', 2); +define('SIEVE_ERROR_NOT_EXISTS', 3); // script not exists +define('SIEVE_ERROR_INSTALL', 4); // script installation +define('SIEVE_ERROR_ACTIVATE', 5); // script activation +define('SIEVE_ERROR_OTHER', 255); // other/unknown error + + +class rcube_sieve +{ + var $sieve; // Net_Sieve object + var $error = false; // error flag + var $list = array(); // scripts list + + public $script; // rcube_sieve_script object + private $disabled; // array of disabled extensions + + /** + * Object constructor + * + * @param string Username (to managesieve login) + * @param string Password (to managesieve login) + * @param string Managesieve server hostname/address + * @param string Managesieve server port number + * @param string Enable/disable TLS use + * @param array Disabled extensions + */ + public function __construct($username, $password='', $host='localhost', $port=2000, $usetls=true, $disabled=array()) + { + $this->sieve = new Net_Sieve(); + +// $this->sieve->setDebug(); + if (PEAR::isError($this->sieve->connect($host, $port, NULL, $usetls))) + return $this->_set_error(SIEVE_ERROR_CONNECTION); + + if (PEAR::isError($this->sieve->login($username, $password))) + return $this->_set_error(SIEVE_ERROR_LOGIN); + + $this->disabled = $disabled; + $this->_get_script(); + } + + /** + * Getter for error code + */ + public function error() + { + return $this->error ? $this->error : false; + } + + public function save() + { + $script = $this->script->as_text(); + + if (!$script) + $script = '/* empty script */'; + + if (PEAR::isError($this->sieve->installScript('roundcube', $script))) + return $this->_set_error(SIEVE_ERROR_INSTALL); + + if (PEAR::isError($this->sieve->setActive('roundcube'))) + return $this->_set_error(SIEVE_ERROR_ACTIVATE); + + return true; + } + + public function get_extensions() + { + if ($this->sieve) { + $ext = $this->sieve->getExtensions(); + + if ($this->script) { + $supported = $this->script->get_extensions(); + foreach ($ext as $idx => $ext_name) + if (!in_array($ext_name, $supported)) + unset($ext[$idx]); + } + + return array_values($ext); + } + } + + private function _get_script() + { + if (!$this->sieve) + return false; + + $this->list = $this->sieve->listScripts(); + + if (PEAR::isError($this->list)) + return $this->_set_error(SIEVE_ERROR_OTHER); + + if (in_array('roundcube', $this->list)) + { + $script = $this->sieve->getScript('roundcube'); + + if (PEAR::isError($script)) + return $this->_set_error(SIEVE_ERROR_OTHER); + } + // import scripts from squirrelmail + elseif (in_array('phpscript', $this->list)) + { + $script = $this->sieve->getScript('phpscript'); + + $script = $this->_convert_from_squirrel_rules($script); + + $this->script = new rcube_sieve_script($script); + + $this->save(); + + $script = $this->sieve->getScript('roundcube'); + + if (PEAR::isError($script)) + return $this->_set_error(SIEVE_ERROR_OTHER); + } + else + { + $this->_set_error(SIEVE_ERROR_NOT_EXISTS); + $script = ''; + } + + $this->script = new rcube_sieve_script($script, $this->disabled); + } + + private function _convert_from_squirrel_rules($script) + { + $i = 0; + $name = array(); + // tokenize rules + if ($tokens = preg_split('/(#START_SIEVE_RULE.*END_SIEVE_RULE)\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE)) + foreach($tokens as $token) + { + if (preg_match('/^#START_SIEVE_RULE.*/', $token, $matches)) + { + $name[$i] = "unnamed rule ".($i+1); + $content .= "# rule:[".$name[$i]."]\n"; + } + elseif (isset($name[$i])) + { + $content .= "if ".$token."\n"; + $i++; + } + } + + return $content; + } + + + private function _set_error($error) + { + $this->error = $error; + return false; + } +} + +class rcube_sieve_script +{ + var $content = array(); // script rules array + + private $supported = array( // extensions supported by class + 'fileinto', + 'reject', + 'ereject', + 'vacation', // RFC5230 + // TODO: (most wanted first) body, imapflags, notify, regex + ); + + /** + * Object constructor + * + * @param string Script's text content + * @param array Disabled extensions + */ + public function __construct($script, $disabled) + { + global $CONFIG; + + if (!empty($disabled)) + foreach ($disabled as $ext) + if (($idx = array_search($ext, $this->supported)) !== false) + unset($this->supported[$idx]); + + $this->content = $this->_parse_text($script); + } + + /** + * Adds script contents as text to the script array (at the end) + * + * @param string Text script contents + */ + public function add_text($script) + { + $content = $this->_parse_text($script); + $result = false; + + // check existsing script rules names + foreach ($this->content as $idx => $elem) + $names[$elem['name']] = $idx; + + foreach ($content as $elem) + if (!isset($names[$elem['name']])) + { + array_push($this->content, $elem); + $result = true; + } + + return $result; + } + + /** + * Adds rule to the script (at the end) + * + * @param string Rule name + * @param array Rule content (as array) + */ + public function add_rule($content) + { + // TODO: check this->supported + array_push($this->content, $content); + return sizeof($this->content)-1; + } + + public function delete_rule($index) + { + if(isset($this->content[$index])) + { + unset($this->content[$index]); + return true; + } + return false; + } + + public function size() + { + return sizeof($this->content); + } + + public function update_rule($index, $content) + { + // TODO: check this->supported + if ($this->content[$index]) + { + $this->content[$index] = $content; + return $index; + } + return false; + } + + /** + * Returns script as text + */ + public function as_text() + { + $script = ''; + $exts = array(); + + // rules + foreach ($this->content as $idx => $rule) + { + $extension = ''; + $tests = array(); + $i = 0; + + // header + $script .= '# rule:[' . $rule['name'] . "]\n"; + + // constraints expressions + foreach ($rule['tests'] as $test) + { + $tests[$i] = ''; + switch ($test['test']) + { + case 'size': + $tests[$i] .= ($test['not'] ? 'not ' : ''); + $tests[$i] .= 'size :' . ($test['type']=='under' ? 'under ' : 'over ') . $test['arg']; + break; + case 'true': + $tests[$i] .= ($test['not'] ? 'not true' : 'true'); + break; + case 'exists': + $tests[$i] .= ($test['not'] ? 'not ' : ''); + if (is_array($test['arg'])) + $tests[$i] .= 'exists ["' . implode('", "', $this->_escape_string($test['arg'])) . '"]'; + else + $tests[$i] .= 'exists "' . $this->_escape_string($test['arg']) . '"'; + break; + case 'header': + $tests[$i] .= ($test['not'] ? 'not ' : ''); + $tests[$i] .= 'header :' . $test['type']; + if (is_array($test['arg1'])) + $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg1'])) . '"]'; + else + $tests[$i] .= ' "' . $this->_escape_string($test['arg1']) . '"'; + if (is_array($test['arg2'])) + $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg2'])) . '"]'; + else + $tests[$i] .= ' "' . $this->_escape_string($test['arg2']) . '"'; + break; + } + $i++; + } + + $script .= ($idx>0 ? 'els' : '').($rule['join'] ? 'if allof (' : 'if anyof ('); + if (sizeof($tests) > 1) + $script .= implode(",\n\t", $tests); + elseif (sizeof($tests)) + $script .= $tests[0]; + else + $script .= 'true'; + $script .= ")\n{\n"; + + // action(s) + foreach ($rule['actions'] as $action) + switch ($action['type']) + { + case 'fileinto': + $extension = 'fileinto'; + $script .= "\tfileinto \"" . $this->_escape_string($action['target']) . "\";\n"; + break; + case 'redirect': + $script .= "\tredirect \"" . $this->_escape_string($action['target']) . "\";\n"; + break; + case 'reject': + case 'ereject': + $extension = $action['type']; + if (strpos($action['target'], "\n")!==false) + $script .= "\t".$action['type']." text:\n" . $action['target'] . "\n.\n;\n"; + else + $script .= "\t".$action['type']." \"" . $this->_escape_string($action['target']) . "\";\n"; + break; + case 'keep': + case 'discard': + case 'stop': + $script .= "\t" . $action['type'] .";\n"; + break; + case 'vacation': + $extension = 'vacation'; + $script .= "\tvacation"; + if ($action['days']) + $script .= " :days " . $action['days']; + if ($action['addresses']) + $script .= " :addresses " . $this->_print_list($action['addresses']); + if ($action['subject']) + $script .= " :subject \"" . $this->_escape_string($action['subject']) . "\""; + if ($action['handle']) + $script .= " :handle \"" . $this->_escape_string($action['handle']) . "\""; + if ($action['from']) + $script .= " :from \"" . $this->_escape_string($action['from']) . "\""; + if ($action['mime']) + $script .= " :mime"; + if (strpos($action['reason'], "\n")!==false) + $script .= " text:\n" . $action['reason'] . "\n.\n;\n"; + else + $script .= " \"" . $this->_escape_string($action['reason']) . "\";\n"; + break; + } + + $script .= "}\n"; + + if ($extension && !isset($exts[$extension])) + $exts[$extension] = $extension; + } + + // requires + if (sizeof($exts)) + $script = 'require ["' . implode('","', $exts) . "\"];\n" . $script; + + return $script; + } + + /** + * Returns script object + * + */ + public function as_array() + { + return $this->content; + } + + /** + * Returns array of supported extensions + * + */ + public function get_extensions() + { + return array_values($this->supported); + } + + /** + * Converts text script to rules array + * + * @param string Text script + */ + private function _parse_text($script) + { + $i = 0; + $content = array(); + + // remove C comments + $script = preg_replace('|/\*.*?\*/|sm', '', $script); + + // tokenize rules + if ($tokens = preg_split('/(# rule:\[.*\])\r?\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE)) + foreach($tokens as $token) + { + if (preg_match('/^# rule:\[(.*)\]/', $token, $matches)) + { + $content[$i]['name'] = $matches[1]; + } + elseif (isset($content[$i]['name']) && sizeof($content[$i]) == 1) + { + if ($rule = $this->_tokenize_rule($token)) + { + $content[$i] = array_merge($content[$i], $rule); + $i++; + } + else // unknown rule format + unset($content[$i]); + } + } + + return $content; + } + + /** + * Convert text script fragment to rule object + * + * @param string Text rule + */ + private function _tokenize_rule($content) + { + $result = NULL; + + if (preg_match('/^(if|elsif|else)\s+((allof|anyof|exists|header|not|size)\s+(.*))\s+\{(.*)\}$/sm', trim($content), $matches)) + { + list($tests, $join) = $this->_parse_tests(trim($matches[2])); + $actions = $this->_parse_actions(trim($matches[5])); + + if ($tests && $actions) + $result = array( + 'tests' => $tests, + 'actions' => $actions, + 'join' => $join, + ); + } + + return $result; + } + + /** + * Parse body of actions section + * + * @param string Text body + * @return array Array of parsed action type/target pairs + */ + private function _parse_actions($content) + { + $result = NULL; + + // supported actions + $patterns[] = '^\s*discard;'; + $patterns[] = '^\s*keep;'; + $patterns[] = '^\s*stop;'; + $patterns[] = '^\s*redirect\s+(.*?[^\\\]);'; + if (in_array('fileinto', $this->supported)) + $patterns[] = '^\s*fileinto\s+(.*?[^\\\]);'; + if (in_array('reject', $this->supported)) { + $patterns[] = '^\s*reject\s+text:(.*)\n\.\n;'; + $patterns[] = '^\s*reject\s+(.*?[^\\\]);'; + $patterns[] = '^\s*ereject\s+text:(.*)\n\.\n;'; + $patterns[] = '^\s*ereject\s+(.*?[^\\\]);'; + } + if (in_array('vacation', $this->supported)) + $patterns[] = '^\s*vacation\s+(.*?[^\\\]);'; + + $pattern = '/(' . implode('$)|(', $patterns) . '$)/ms'; + + // parse actions body + if (preg_match_all($pattern, $content, $mm, PREG_SET_ORDER)) + { + foreach ($mm as $m) + { + $content = trim($m[0]); + + if(preg_match('/^(discard|keep|stop)/', $content, $matches)) + { + $result[] = array('type' => $matches[1]); + } + elseif(preg_match('/^fileinto/', $content)) + { + $result[] = array('type' => 'fileinto', 'target' => $this->_parse_string($m[sizeof($m)-1])); + } + elseif(preg_match('/^redirect/', $content)) + { + $result[] = array('type' => 'redirect', 'target' => $this->_parse_string($m[sizeof($m)-1])); + } + elseif(preg_match('/^(reject|ereject)\s+(.*);$/sm', $content, $matches)) + { + $result[] = array('type' => $matches[1], 'target' => $this->_parse_string($matches[2])); + } + elseif(preg_match('/^vacation\s+(.*);$/sm', $content, $matches)) + { + $vacation = array('type' => 'vacation'); + + if (preg_match('/:(days)\s+([0-9]+)/', $content, $vm)) { + $vacation['days'] = $vm[2]; + $content = preg_replace('/:(days)\s+([0-9]+)/', '', $content); + } + if (preg_match('/:(subject)\s+(".*?[^\\\]")/', $content, $vm)) { + $vacation['subject'] = $vm[2]; + $content = preg_replace('/:(subject)\s+(".*?[^\\\]")/', '', $content); + } + if (preg_match('/:(addresses)\s+\[(.*?[^\\\])\]/', $content, $vm)) { + $vacation['addresses'] = $this->_parse_list($vm[2]); + $content = preg_replace('/:(addresses)\s+\[(.*?[^\\\])\]/', '', $content); + } + if (preg_match('/:(handle)\s+(".*?[^\\\]")/', $content, $vm)) { + $vacation['handle'] = $vm[2]; + $content = preg_replace('/:(handle)\s+(".*?[^\\\]")/', '', $content); + } + if (preg_match('/:(from)\s+(".*?[^\\\]")/', $content, $vm)) { + $vacation['from'] = $vm[2]; + $content = preg_replace('/:(from)\s+(".*?[^\\\]")/', '', $content); + } + $content = preg_replace('/^vacation/', '', $content); + $content = preg_replace('/;$/', '', $content); + $content = trim($content); + if (preg_match('/^:(mime)/', $content, $vm)) { + $vacation['mime'] = true; + $content = preg_replace('/^:mime/', '', $content); + } + + $vacation['reason'] = $this->_parse_string($content); + + $result[] = $vacation; + } + } + } + + return $result; + } + + /** + * Parse test/conditions section + * + * @param string Text + */ + + private function _parse_tests($content) + { + $result = NULL; + + // lists + if (preg_match('/^(allof|anyof)\s+\((.*)\)$/sm', $content, $matches)) + { + $content = $matches[2]; + $join = $matches[1]=='allof' ? true : false; + } + else + $join = false; + + // supported tests regular expressions + // TODO: comparators, envelope + $patterns[] = '(not\s+)?(exists)\s+\[(.*?[^\\\])\]'; + $patterns[] = '(not\s+)?(exists)\s+(".*?[^\\\]")'; + $patterns[] = '(not\s+)?(true)'; + $patterns[] = '(not\s+)?(size)\s+:(under|over)\s+([0-9]+[KGM]{0,1})'; + $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+\[(.*?[^\\\]")\]\s+\[(.*?[^\\\]")\]'; + $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+(".*?[^\\\]")\s+(".*?[^\\\]")'; + $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+\[(.*?[^\\\]")\]\s+(".*?[^\\\]")'; + $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+(".*?[^\\\]")\s+\[(.*?[^\\\]")\]'; + + // join patterns... + $pattern = '/(' . implode(')|(', $patterns) . ')/'; + + // ...and parse tests list + if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) + { + foreach ($matches as $match) + { + $size = sizeof($match); + + if (preg_match('/^(not\s+)?size/', $match[0])) + { + $result[] = array( + 'test' => 'size', + 'not' => $match[$size-4] ? true : false, + 'type' => $match[$size-2], // under/over + 'arg' => $match[$size-1], // value + ); + } + elseif (preg_match('/^(not\s+)?header/', $match[0])) + { + $result[] = array( + 'test' => 'header', + 'not' => $match[$size-5] ? true : false, + 'type' => $match[$size-3], // is/contains/matches + 'arg1' => $this->_parse_list($match[$size-2]), // header(s) + 'arg2' => $this->_parse_list($match[$size-1]), // string(s) + ); + } + elseif (preg_match('/^(not\s+)?exists/', $match[0])) + { + $result[] = array( + 'test' => 'exists', + 'not' => $match[$size-3] ? true : false, + 'arg' => $this->_parse_list($match[$size-1]), // header(s) + ); + } + elseif (preg_match('/^(not\s+)?true/', $match[0])) + { + $result[] = array( + 'test' => 'true', + 'not' => $match[$size-2] ? true : false, + ); + } + } + } + + return array($result, $join); + } + + /** + * Parse string value + * + * @param string Text + */ + private function _parse_string($content) + { + $text = ''; + $content = trim($content); + + if (preg_match('/^text:(.*)\.$/sm', $content, $matches)) + $text = trim($matches[1]); + elseif (preg_match('/^"(.*)"$/', $content, $matches)) + $text = str_replace('\"', '"', $matches[1]); + + return $text; + } + + /** + * Escape special chars in string value + * + * @param string Text + */ + private function _escape_string($content) + { + $replace['/"/'] = '\\"'; + + if (is_array($content)) + { + for ($x=0, $y=sizeof($content); $x<$y; $x++) + $content[$x] = preg_replace(array_keys($replace), array_values($replace), $content[$x]); + + return $content; + } + else + return preg_replace(array_keys($replace), array_values($replace), $content); + } + + /** + * Parse string or list of strings to string or array of strings + * + * @param string Text + */ + private function _parse_list($content) + { + $result = array(); + + for ($x=0, $len=strlen($content); $x<$len; $x++) + { + switch ($content[$x]) + { + case '\\': + $str .= $content[++$x]; + break; + case '"': + if (isset($str)) + { + $result[] = $str; + unset($str); + } + else + $str = ''; + break; + default: + if(isset($str)) + $str .= $content[$x]; + break; + } + } + + if (sizeof($result)>1) + return $result; + elseif (sizeof($result) == 1) + return $result[0]; + else + return NULL; + } + + /** + * Convert array of elements to list of strings + * + * @param string Text + */ + private function _print_list($list) + { + $list = (array) $list; + foreach($list as $idx => $val) + $list[$idx] = $this->_escape_string($val); + + return '["' . implode('","', $list) . '"]'; + } +} + +?> diff --git a/plugins/managesieve/localization/bg_BG.inc b/plugins/managesieve/localization/bg_BG.inc new file mode 100644 index 000000000..96ce63bd0 --- /dev/null +++ b/plugins/managesieve/localization/bg_BG.inc @@ -0,0 +1,50 @@ + diff --git a/plugins/managesieve/localization/de_DE.inc b/plugins/managesieve/localization/de_DE.inc new file mode 100644 index 000000000..c3a2feb98 --- /dev/null +++ b/plugins/managesieve/localization/de_DE.inc @@ -0,0 +1,53 @@ + diff --git a/plugins/managesieve/localization/en_US.inc b/plugins/managesieve/localization/en_US.inc new file mode 100644 index 000000000..c671b83ef --- /dev/null +++ b/plugins/managesieve/localization/en_US.inc @@ -0,0 +1,53 @@ + diff --git a/plugins/managesieve/localization/fi_FI.inc b/plugins/managesieve/localization/fi_FI.inc new file mode 100644 index 000000000..f066ca6ea --- /dev/null +++ b/plugins/managesieve/localization/fi_FI.inc @@ -0,0 +1,49 @@ + diff --git a/plugins/managesieve/localization/fr_FR.inc b/plugins/managesieve/localization/fr_FR.inc new file mode 100644 index 000000000..632db98ea --- /dev/null +++ b/plugins/managesieve/localization/fr_FR.inc @@ -0,0 +1,53 @@ + diff --git a/plugins/managesieve/localization/gb_GB.inc b/plugins/managesieve/localization/gb_GB.inc new file mode 100644 index 000000000..c671b83ef --- /dev/null +++ b/plugins/managesieve/localization/gb_GB.inc @@ -0,0 +1,53 @@ + diff --git a/plugins/managesieve/localization/hu_HU.inc b/plugins/managesieve/localization/hu_HU.inc new file mode 100644 index 000000000..1647fbe23 --- /dev/null +++ b/plugins/managesieve/localization/hu_HU.inc @@ -0,0 +1,54 @@ + diff --git a/plugins/managesieve/localization/nl_NL.inc b/plugins/managesieve/localization/nl_NL.inc new file mode 100644 index 000000000..a02f93bde --- /dev/null +++ b/plugins/managesieve/localization/nl_NL.inc @@ -0,0 +1,49 @@ + diff --git a/plugins/managesieve/localization/pl_PL.inc b/plugins/managesieve/localization/pl_PL.inc new file mode 100644 index 000000000..18a6c7d62 --- /dev/null +++ b/plugins/managesieve/localization/pl_PL.inc @@ -0,0 +1,53 @@ + diff --git a/plugins/managesieve/localization/ru_RU.inc b/plugins/managesieve/localization/ru_RU.inc new file mode 100644 index 000000000..ad459a0f2 --- /dev/null +++ b/plugins/managesieve/localization/ru_RU.inc @@ -0,0 +1,49 @@ + diff --git a/plugins/managesieve/localization/sv_SE.inc b/plugins/managesieve/localization/sv_SE.inc new file mode 100644 index 000000000..48d015861 --- /dev/null +++ b/plugins/managesieve/localization/sv_SE.inc @@ -0,0 +1,54 @@ + diff --git a/plugins/managesieve/localization/zh_CN.inc b/plugins/managesieve/localization/zh_CN.inc new file mode 100644 index 000000000..fe63c6de8 --- /dev/null +++ b/plugins/managesieve/localization/zh_CN.inc @@ -0,0 +1,49 @@ + diff --git a/plugins/managesieve/managesieve.js b/plugins/managesieve/managesieve.js new file mode 100644 index 000000000..09139d6c5 --- /dev/null +++ b/plugins/managesieve/managesieve.js @@ -0,0 +1,381 @@ +/* Sieve Filters (tab) */ + +if (window.rcmail) { + rcmail.addEventListener('init', function(evt) { + // + var tab = $('').attr('id', 'settingstabpluginmanagesieve').addClass('tablink'); + + var button = $('').attr('href', rcmail.env.comm_path+'&_action=plugin.managesieve') + .attr('title', rcmail.gettext('managesieve.managefilters')) + .html(rcmail.gettext('managesieve.filters')) + .bind('click', function(e){ return rcmail.command('plugin.managesieve', this) }) + .appendTo(tab); + + // add button and register commands + rcmail.add_element(tab, 'tabs'); + rcmail.register_command('plugin.managesieve', function() { rcmail.goto_url('plugin.managesieve') }, true); + rcmail.register_command('plugin.managesieve-save', function() { rcmail.managesieve_save() }, true); + rcmail.register_command('plugin.managesieve-add', function() { rcmail.managesieve_add() }, true); + rcmail.register_command('plugin.managesieve-del', function() { rcmail.managesieve_del() }, true); + rcmail.register_command('plugin.managesieve-up', function() { rcmail.managesieve_up() }, true); + rcmail.register_command('plugin.managesieve-down', function() { rcmail.managesieve_down() }, true); + + if (rcmail.env.action == 'plugin.managesieve') + { + if (rcmail.gui_objects.sieveform) + rcmail.enable_command('plugin.managesieve-save', true); + else { + rcmail.enable_command('plugin.managesieve-del', 'plugin.managesieve-up', 'plugin.managesieve-down', false); + rcmail.enable_command('plugin.managesieve-add', !rcmail.env.sieveconnerror); + } + + if (rcmail.gui_objects.filterslist) { + var p = rcmail; + rcmail.filters_list = new rcube_list_widget(rcmail.gui_objects.filterslist, {multiselect:false, draggable:false, keyboard:false}); + rcmail.filters_list.addEventListener('select', function(o){ p.managesieve_select(o); }); + rcmail.filters_list.init(); + rcmail.filters_list.focus(); + } + } + }); + + /*********************************************************/ + /********* Managesieve filters methods *********/ + /*********************************************************/ + + rcube_webmail.prototype.managesieve_add = function() + { + this.load_managesieveframe(); + this.filters_list.clear_selection(); + }; + + rcube_webmail.prototype.managesieve_del = function() + { + var id = this.filters_list.get_single_selection(); + + if (confirm(this.get_label('managesieve.filterconfirmdelete'))) + this.http_request('plugin.managesieve', + '_act=delete&_fid='+this.filters_list.rows[id].uid, true); + }; + + rcube_webmail.prototype.managesieve_up = function() + { + var id = this.filters_list.get_single_selection(); + this.http_request('plugin.managesieve', + '_act=up&_fid='+this.filters_list.rows[id].uid, true); + }; + + rcube_webmail.prototype.managesieve_down = function() + { + var id = this.filters_list.get_single_selection(); + this.http_request('plugin.managesieve', + '_act=down&_fid='+this.filters_list.rows[id].uid, true); + }; + + rcube_webmail.prototype.managesieve_rowid = function(id) + { + var rows = this.filters_list.rows; + + for (var i=0; i id) + rows[i].uid = rows[i].uid-1; + } + break; + + case 'down': + var rows = this.filters_list.rows; + var from; + + // we need only to replace filter names... + for (var i=0; i0; i--) + { + if (rows[i] == null) { // removed row + } else if (i == id) { + this.enable_command('plugin.managesieve-down', false); + break; + } else { + this.enable_command('plugin.managesieve-down', true); + break; + } + } + }; + + // operations on filters form + rcube_webmail.prototype.managesieve_ruleadd = function(id) + { + this.http_post('plugin.managesieve', '_act=ruleadd&_rid='+id); + }; + + rcube_webmail.prototype.managesieve_rulefill = function(content, id, after) + { + if (content != '') + { + // create new element + var div = document.getElementById('rules'); + var row = document.createElement('div'); + + this.managesieve_insertrow(div, row, after); + // fill row after inserting (for IE) + row.setAttribute('id', 'rulerow'+id); + row.className = 'rulerow'; + row.innerHTML = content; + + this.managesieve_formbuttons(div); + } + }; + + rcube_webmail.prototype.managesieve_ruledel = function(id) + { + if (confirm(this.get_label('managesieve.ruledeleteconfirm'))) + { + var row = document.getElementById('rulerow'+id); + row.parentNode.removeChild(row); + this.managesieve_formbuttons(document.getElementById('rules')); + } + }; + + rcube_webmail.prototype.managesieve_actionadd = function(id) + { + this.http_post('plugin.managesieve', '_act=actionadd&_aid='+id); + }; + + rcube_webmail.prototype.managesieve_actionfill = function(content, id, after) + { + if (content != '') + { + var div = document.getElementById('actions'); + var row = document.createElement('div'); + + this.managesieve_insertrow(div, row, after); + // fill row after inserting (for IE) + row.className = 'actionrow'; + row.setAttribute('id', 'actionrow'+id); + row.innerHTML = content; + + this.managesieve_formbuttons(div); + } + }; + + rcube_webmail.prototype.managesieve_actiondel = function(id) + { + if (confirm(this.get_label('managesieve.actiondeleteconfirm'))) + { + var row = document.getElementById('actionrow'+id); + row.parentNode.removeChild(row); + this.managesieve_formbuttons(document.getElementById('actions')); + } + }; + + // insert rule/action row in specified place on the list + rcube_webmail.prototype.managesieve_insertrow = function(div, row, after) + { + for (var i=0; i0 || buttons.length>1) + { + $(button).removeClass('disabled'); + button.removeAttribute('disabled'); + } + else + { + $(button).addClass('disabled'); + button.setAttribute('disabled', true); + } + } + } +} diff --git a/plugins/managesieve/managesieve.php b/plugins/managesieve/managesieve.php new file mode 100644 index 000000000..18001c0e7 --- /dev/null +++ b/plugins/managesieve/managesieve.php @@ -0,0 +1,863 @@ + + * + * Configuration (main.inc.php): + +// managesieve server port +$rcmail_config['managesieve_port'] = 2000; + +// managesieve server address +$rcmail_config['managesieve_host'] = 'localhost'; + +// use or not TLS for managesieve server connection +// it's because I've problems with TLS and dovecot's managesieve plugin +// and it's not needed on localhost +$rcmail_config['managesieve_usetls'] = false; + +// default contents of filters script (eg. default spam filter) +$rcmail_config['managesieve_default'] = '/etc/dovecot/sieve/global'; + +// I need this because my dovecot (with listescape plugin) uses +// ':' delimiter, but creates folders with dot delimiter +$rcmail_config['managesieve_replace_delimiter'] = ''; + +// disabled sieve extensions (body, copy, date, editheader, encoded-character, +// envelope, environment, ereject, fileinto, ihave, imap4flags, index, +// mailbox, mboxmetadata, regex, reject, relational, servermetadata, +// spamtest, spamtestplus, subaddress, vacation, variables, virustest, etc. +// Note: not all extensions are implemented +$rcmail_config['managesieve_disabled_extensions'] = array(); + + */ + +class managesieve extends rcube_plugin +{ + public $task = 'settings'; + + private $sieve; + private $rc; + private $errors; + private $dir; + private $form; + private $script = array(); + private $exts = array(); + private $headers = array( + 'subject' => 'Subject', + 'sender' => 'From', + 'recipient' => 'To', + ); + + function init() + { + $rcmail = rcmail::get_instance(); + $this->rc = &$rcmail; + + // add Tab label/title + $this->add_texts('localization/', array('filters','managefilters')); + + // register actions + $this->register_action('plugin.managesieve', array($this, 'managesieve_init')); + $this->register_action('plugin.managesieve-save', array($this, 'managesieve_save')); + + // include main js script + $this->include_script('managesieve.js'); + } + + function managesieve_start() + { + // register UI objects + $this->rc->output->add_handlers(array( + 'filterslist' => array($this, 'filters_list'), + 'filterframe' => array($this, 'filter_frame'), + 'filterform' => array($this, 'filter_form'), + )); + + require_once($this->home . '/lib/Net/Sieve.php'); + require_once($this->home . '/lib/rcube_sieve.php'); + + // try to connect to managesieve server and to fetch the script + $this->sieve = new rcube_sieve($_SESSION['username'], + $this->rc->decrypt($_SESSION['password']), + $this->rc->config->get('managesieve_host', 'localhost'), + $this->rc->config->get('managesieve_port', 2000), + $this->rc->config->get('managesieve_usetls', false), + $this->rc->config->get('managesieve_disabled_extensions')); + + $error = $this->sieve->error(); + + if ($error == SIEVE_ERROR_NOT_EXISTS) + { + // if script not exists build default script contents + $script_file = $this->rc->config->get('managesieve_default'); + if ($script_file && is_readable($script_file)) + $this->sieve->script->add_text(file_get_contents($script_file)); + // that's not exactly an error + $error = false; + } + elseif ($error) + { + switch ($error) + { + case SIEVE_ERROR_CONNECTION: + case SIEVE_ERROR_LOGIN: + $this->rc->output->show_message('managesieve.filterconnerror', 'error'); + break; + default: + $this->rc->output->show_message('managesieve.filterunknownerror', 'error'); + break; + } + + // to disable 'Add filter' button set env variable + $this->rc->output->set_env('filterconnerror', true); + } + + // finally set script objects + if ($error) + { + $this->script = array(); + } + else + { + $this->script = $this->sieve->script->as_array(); + $this->exts = $this->sieve->get_extensions(); + } + + return $error; + } + + function managesieve_init() + { + // Init plugin and handle managesieve connection + $error = $this->managesieve_start(); + + // Handle user requests + if ($action = get_input_value('_act', RCUBE_INPUT_GPC)) + { + $fid = (int) get_input_value('_fid', RCUBE_INPUT_GET); + + if ($action=='up' && !$error) + { + if ($fid && isset($this->script[$fid]) && isset($this->script[$fid-1])) + { + if ($this->sieve->script->update_rule($fid, $this->script[$fid-1]) !== false + && $this->sieve->script->update_rule($fid-1, $this->script[$fid]) !== false) + $result = $this->sieve->save(); + + if ($result) { +// $this->rc->output->show_message('managesieve.filtersaved', 'confirmation'); + $this->rc->output->command('managesieve_updatelist', 'up', '', $fid); + } else + $this->rc->output->show_message('managesieve.filtersaveerror', 'error'); + } + } + elseif ($action=='down' && !$error) + { + if (isset($this->script[$fid]) && isset($this->script[$fid+1])) + { + if ($this->sieve->script->update_rule($fid, $this->script[$fid+1]) !== false + && $this->sieve->script->update_rule($fid+1, $this->script[$fid]) !== false) + $result = $this->sieve->save(); + + if ($result) { +// $this->rc->output->show_message('managesieve.filtersaved', 'confirmation'); + $this->rc->output->command('managesieve_updatelist', 'down', '', $fid); + } else + $this->rc->output->show_message('managesieve.filtersaveerror', 'error'); + } + } + elseif ($action=='delete' && !$error) + { + if (isset($this->script[$fid])) + { + if ($this->sieve->script->delete_rule($fid)) + $result = $this->sieve->save(); + + if (!$result) + $this->rc->output->show_message('managesieve.filterdeleteerror', 'error'); + else { + $this->rc->output->show_message('managesieve.filterdeleted', 'confirmation'); + $this->rc->output->command('managesieve_updatelist', 'delete', '', $fid); + } + } + } + elseif ($action=='ruleadd') + { + $rid = get_input_value('_rid', RCUBE_INPUT_GPC); + $id = $this->genid(); + $content = $this->rule_div($fid, $id, false); + + $this->rc->output->command('managesieve_rulefill', $content, $id, $rid); + } + elseif ($action=='actionadd') + { + $aid = get_input_value('_aid', RCUBE_INPUT_GPC); + $id = $this->genid(); + $content = $this->action_div($fid, $id, false); + + $this->rc->output->command('managesieve_actionfill', $content, $id, $aid); + } + + $this->rc->output->send(); + } + + $this->managesieve_send(); + } + + function managesieve_save() + { + // Init plugin and handle managesieve connection + $error = $this->managesieve_start(); + + // add/edit action + if (isset($_POST['_name'])) + { + $name = trim(get_input_value('_name', RCUBE_INPUT_POST)); + $fid = trim(get_input_value('_fid', RCUBE_INPUT_POST)); + $join = trim(get_input_value('_join', RCUBE_INPUT_POST)); + + // and arrays + $headers = $_POST['_header']; + $cust_headers = $_POST['_custom_header']; + $ops = $_POST['_rule_op']; + $sizeops = $_POST['_rule_size_op']; + $sizeitems = $_POST['_rule_size_item']; + $sizetargets = $_POST['_rule_size_target']; + $targets = $_POST['_rule_target']; + $act_types = $_POST['_action_type']; + $mailboxes = $_POST['_action_mailbox']; + $act_targets = $_POST['_action_target']; + $area_targets = $_POST['_action_target_area']; + $reasons = $_POST['_action_reason']; + $addresses = $_POST['_action_addresses']; + $days = $_POST['_action_days']; + + // we need a "hack" for radiobuttons + foreach ($sizeitems as $item) + $items[] = $item; + + $this->form['join'] = $join=='allof' ? true : false; + $this->form['name'] = $name; + $this->form['tests'] = array(); + $this->form['actions'] = array(); + + if ($name == '') + $this->errors['name'] = $this->gettext('cannotbeempty'); + else + foreach($this->script as $idx => $rule) + if($rule['name'] == $name && $idx != $fid) { + $this->errors['name'] = $this->gettext('ruleexist'); + break; + } + + $i = 0; + // rules + if ($join == 'any') + { + $this->form['tests'][0]['test'] = 'true'; + } + else foreach($headers as $idx => $header) + { + $header = $this->strip_value($header); + $target = $this->strip_value($targets[$idx]); + $op = $this->strip_value($ops[$idx]); + + // normal header + if (in_array($header, $this->headers)) + { + if(preg_match('/^not/', $op)) + $this->form['tests'][$i]['not'] = true; + $type = preg_replace('/^not/', '', $op); + + if ($type == 'exists') + { + $this->form['tests'][$i]['test'] = 'exists'; + $this->form['tests'][$i]['arg'] = $header; + } + else + { + $this->form['tests'][$i]['type'] = $type; + $this->form['tests'][$i]['test'] = 'header'; + $this->form['tests'][$i]['arg1'] = $header; + $this->form['tests'][$i]['arg2'] = $target; + + if ($target == '') + $this->errors['tests'][$i]['target'] = $this->gettext('cannotbeempty'); + } + } + else + switch ($header) + { + case 'size': + $sizeop = $this->strip_value($sizeops[$idx]); + $sizeitem = $this->strip_value($items[$idx]); + $sizetarget = $this->strip_value($sizetargets[$idx]); + + $this->form['tests'][$i]['test'] = 'size'; + $this->form['tests'][$i]['type'] = $sizeop; + $this->form['tests'][$i]['arg'] = $sizetarget.$sizeitem; + + if (!preg_match('/^[0-9]+(K|M|G)*$/i', $sizetarget)) + $this->errors['tests'][$i]['sizetarget'] = $this->gettext('wrongformat'); + break; + case '...': + $cust_header = $this->strip_value($cust_headers[$idx]); + + if(preg_match('/^not/', $op)) + $this->form['tests'][$i]['not'] = true; + $type = preg_replace('/^not/', '', $op); + + if ($cust_header == '') + $this->errors['tests'][$i]['header'] = $this->gettext('cannotbeempty'); + elseif (!preg_match('/^[a-z0-9-]+$/i', $cust_header)) + $this->errors['tests'][$i]['header'] = $this->gettext('forbiddenchars'); + + if ($type == 'exists') + { + $this->form['tests'][$i]['test'] = 'exists'; + $this->form['tests'][$i]['arg'] = $cust_header; + } + else + { + $this->form['tests'][$i]['test'] = 'header'; + $this->form['tests'][$i]['type'] = $type; + $this->form['tests'][$i]['arg1'] = $cust_header; + $this->form['tests'][$i]['arg2'] = $target; + + if ($target == '') + $this->errors['tests'][$i]['target'] = $this->gettext('cannotbeempty'); + } + break; + } + $i++; + } + + $i = 0; + // actions + foreach($act_types as $idx => $type) + { + $type = $this->strip_value($type); + $target = $this->strip_value($act_targets[$idx]); + + $this->form['actions'][$i]['type'] = $type; + + switch ($type) + { + case 'fileinto': + $mailbox = $this->strip_value($mailboxes[$idx]); + $this->form['actions'][$i]['target'] = $mailbox; + break; + case 'reject': + case 'ereject': + $target = $this->strip_value($area_targets[$idx]); + $this->form['actions'][$i]['target'] = str_replace("\r\n", "\n", $target); + + // if ($target == '') +// $this->errors['actions'][$i]['targetarea'] = $this->gettext('cannotbeempty'); + break; + case 'redirect': + $this->form['actions'][$i]['target'] = $target; + + if ($this->form['actions'][$i]['target'] == '') + $this->errors['actions'][$i]['target'] = $this->gettext('cannotbeempty'); + else if (!check_email($this->form['actions'][$i]['target'])) + $this->errors['actions'][$i]['target'] = $this->gettext('noemailwarning'); + break; + case 'vacation': + $reason = $this->strip_value($reasons[$idx]); + $this->form['actions'][$i]['reason'] = str_replace("\r\n", "\n", $reason); + $this->form['actions'][$i]['days'] = $days[$idx]; + $this->form['actions'][$i]['addresses'] = explode(',', $addresses[$idx]); +// @TODO: vacation :subject, :mime, :from, :handle + + if ($this->form['actions'][$i]['addresses']) { + foreach($this->form['actions'][$i]['addresses'] as $aidx => $address) { + $address = trim($address); + if (!$address) + unset($this->form['actions'][$i]['addresses'][$aidx]); + else if(!check_email($address)) { + $this->errors['actions'][$i]['addresses'] = $this->gettext('noemailwarning'); + break; + } else + $this->form['actions'][$i]['addresses'][$aidx] = $address; + } + } + + if ($this->form['actions'][$i]['reason'] == '') + $this->errors['actions'][$i]['reason'] = $this->gettext('cannotbeempty'); + if ($this->form['actions'][$i]['days'] && !preg_match('/^[0-9]+$/', $this->form['actions'][$i]['days'])) + $this->errors['actions'][$i]['days'] = $this->gettext('forbiddenchars'); + break; + } + + $i++; + } + + if (!$this->errors) + { + // zapis skryptu + if (!isset($this->script[$fid])) { + $fid = $this->sieve->script->add_rule($this->form); + $new = true; + } else + $fid = $this->sieve->script->update_rule($fid, $this->form); + + if ($fid !== false) + $save = $this->sieve->save(); + + if ($save && $fid !== false) + { + $this->rc->output->show_message('managesieve.filtersaved', 'confirmation'); + $this->rc->output->add_script(sprintf("rcmail.managesieve_updatelist('%s', '%s', %d);", + isset($new) ? 'add' : 'update', $this->form['name'], $fid), 'foot'); +// $this->rc->output->command('managesieve_updatelist', isset($new) ? 'add' : 'update', $this->form['name'], $fid); +// $this->rc->output->send(); + } + else + { + $this->rc->output->show_message('managesieve.filtersaveerror', 'error'); +// $this->rc->output->send(); + } + } + } + + $this->managesieve_send(); + } + + private function managesieve_send() + { + // Handle form action + if (isset($_GET['_framed']) || isset($_POST['_framed'])) + $this->rc->output->send('managesieve.managesieveedit'); + else { + $this->rc->output->set_pagetitle($this->gettext('filters')); + $this->rc->output->send('managesieve.managesieve'); + } + } + + // return the filters list as HTML table + function filters_list($attrib) + { + // add id to message list table if not specified + if (!strlen($attrib['id'])) + $attrib['id'] = 'rcmfilterslist'; + + // define list of cols to be displayed + $a_show_cols = array('managesieve.filtername'); + + foreach($this->script as $idx => $filter) + $result[] = array('managesieve.filtername' => $filter['name'], 'id' => $idx); + + // create XHTML table + $out = rcube_table_output($attrib, $result, $a_show_cols, 'id'); + + // set client env + $this->rc->output->add_gui_object('filterslist', $attrib['id']); + $this->rc->output->include_script('list.js'); + + // add some labels to client + $this->rc->output->add_label('managesieve.filterconfirmdelete'); + + return $out; + } + + function filter_frame($attrib) + { + if (!$attrib['id']) + $attrib['id'] = 'rcmfilterframe'; + + $attrib['name'] = $attrib['id']; + + $this->rc->output->set_env('contentframe', $attrib['name']); + $this->rc->output->set_env('blankpage', $attrib['src'] ? + $this->rc->output->abs_url($attrib['src']) : 'program/blank.gif'); + + return html::tag('iframe', $attrib); + } + + + function filter_form($attrib) + { + if (!$attrib['id']) + $attrib['id'] = 'rcmfilterform'; + + $fid = get_input_value('_fid', RCUBE_INPUT_GPC); + $scr = isset($this->form) ? $this->form : $this->script[$fid]; + + $hiddenfields = new html_hiddenfield(array('name' => '_task', 'value' => $this->rc->task)); + $hiddenfields->add(array('name' => '_action', 'value' => 'plugin.managesieve-save')); + $hiddenfields->add(array('name' => '_framed', 'value' => ($_POST['_framed'] || $_GET['_framed'] ? 1 : 0))); + $hiddenfields->add(array('name' => '_fid', 'value' => $fid)); + + $out = '
'."\n"; + $out .= $hiddenfields->show(); + + // 'any' flag + if (sizeof($scr['tests']) == 1 && $scr['tests'][0]['test'] == 'true' && !$scr['tests'][0]['not']) + $any = true; + + // filter name input + $field_id = '_name'; + $input_name = new html_inputfield(array('name' => '_name', 'id' => $field_id, 'size' => 30, + 'class' => ($this->errors['name'] ? 'error' : ''))); + + if (isset($scr)) + $input_name = $input_name->show($scr['name']); + else + $input_name = $input_name->show(); + + $out .= sprintf("\n %s

\n", + $field_id, Q($this->gettext('filtername')), $input_name); + + $out .= '
' . Q($this->gettext('messagesrules')) . "\n"; + + // any, allof, anyof radio buttons + $field_id = '_allof'; + $input_join = new html_radiobutton(array('name' => '_join', 'id' => $field_id, 'value' => 'allof', + 'onclick' => 'rule_join_radio(\'allof\')', 'class' => 'radio')); + + if (isset($scr) && !$any) + $input_join = $input_join->show($scr['join'] ? 'allof' : ''); + else + $input_join = $input_join->show(); + + $out .= sprintf("%s \n", + $input_join, $field_id, Q($this->gettext('filterallof'))); + + $field_id = '_anyof'; + $input_join = new html_radiobutton(array('name' => '_join', 'id' => $field_id, 'value' => 'anyof', + 'onclick' => 'rule_join_radio(\'anyof\')', 'class' => 'radio')); + + if (isset($scr) && !$any) + $input_join = $input_join->show($scr['join'] ? '' : 'anyof'); + else + $input_join = $input_join->show('anyof'); // default + + $out .= sprintf("%s\n", + $input_join, $field_id, Q($this->gettext('filteranyof'))); + + $field_id = '_any'; + $input_join = new html_radiobutton(array('name' => '_join', 'id' => $field_id, 'value' => 'any', + 'onclick' => 'rule_join_radio(\'any\')', 'class' => 'radio')); + + $input_join = $input_join->show($any ? 'any' : ''); + + $out .= sprintf("%s\n", + $input_join, $field_id, Q($this->gettext('filterany'))); + + $rows_num = isset($scr) ? sizeof($scr['tests']) : 1; + + $out .= '\n"; + + $out .= "
\n"; + + // actions + $out .= '
' . Q($this->gettext('messagesactions')) . "\n"; + + $rows_num = isset($scr) ? sizeof($scr['actions']) : 1; + + $out .= '
'; + for ($x=0; $x<$rows_num; $x++) + $out .= $this->action_div($fid, $x); + $out .= "
\n"; + + $out .= "
\n"; + + $this->rc->output->add_label('managesieve.ruledeleteconfirm'); + $this->rc->output->add_label('managesieve.actiondeleteconfirm'); + $this->rc->output->add_gui_object('sieveform', 'filterform'); + + return $out; + } + + function rule_div($fid, $id, $div=true) + { + $rule = isset($this->form) ? $this->form['tests'][$id] : $this->script[$fid]['tests'][$id]; + $rows_num = isset($this->form) ? sizeof($this->form['tests']) : sizeof($this->script[$fid]['tests']); + + $out = $div ? '
'."\n" : ''; + + $out .= ''; + + // add/del buttons + $out .= '
'; + + // headers select + $select_header = new html_select(array('name' => "_header[]", 'id' => 'header'.$id, + 'onchange' => 'header_select(' .$id .')')); + foreach($this->headers as $name => $val) + $select_header->add(Q($this->gettext($name)), Q($val)); + $select_header->add(Q($this->gettext('size')), 'size'); + $select_header->add(Q($this->gettext('...')), '...'); + + // TODO: list arguments + + if ((isset($rule['test']) && $rule['test'] == 'header') && in_array($rule['arg1'], $this->headers)) + $out .= $select_header->show($rule['arg1']); + elseif ((isset($rule['test']) && $rule['test'] == 'exists') && in_array($rule['arg'], $this->headers)) + $out .= $select_header->show($rule['arg']); + elseif (isset($rule['test']) && $rule['test'] == 'size') + $out .= $select_header->show('size'); + elseif (isset($rule['test']) && $rule['test'] != 'true') + $out .= $select_header->show('...'); + else + $out .= $select_header->show(); + + $out .= ''; + + if ((isset($rule['test']) && $rule['test'] == 'header') && !in_array($rule['arg1'], $this->headers)) + $custom = $rule['arg1']; + elseif ((isset($rule['test']) && $rule['test'] == 'exists') && !in_array($rule['arg'], $this->headers)) + $custom = $rule['arg']; + + $out .= '
+ error_class($id, 'test', 'header') + .' value="' .Q($custom). '" size="20" /> 
' . "\n"; + + // matching type select (operator) + $select_op = new html_select(array('name' => "_rule_op[]", 'id' => 'rule_op'.$id, + 'style' => 'display:' .($rule['test']!='size' ? 'inline' : 'none'), 'onchange' => 'rule_op_select('.$id.')')); + $select_op->add(Q($this->gettext('filtercontains')), 'contains'); + $select_op->add(Q($this->gettext('filternotcontains')), 'notcontains'); + $select_op->add(Q($this->gettext('filteris')), 'is'); + $select_op->add(Q($this->gettext('filterisnot')), 'notis'); + $select_op->add(Q($this->gettext('filterexists')), 'exists'); + $select_op->add(Q($this->gettext('filternotexists')), 'notexists'); +// $select_op->add(Q($this->gettext('filtermatches')), 'matches'); +// $select_op->add(Q($this->gettext('filternotmatches')), 'notmatches'); + + // target input (TODO: lists) + + if ($rule['test'] == 'header') + { + $out .= $select_op->show(($rule['not'] ? 'not' : '').$rule['type']); + $target = $rule['arg2']; + } + elseif ($rule['test'] == 'size') + { + $out .= $select_op->show(); + if(preg_match('/^([0-9]+)(K|M|G)*$/', $rule['arg'], $matches)) + { + $sizetarget = $matches[1]; + $sizeitem = $matches[2]; + } + } + else + { + $out .= $select_op->show(($rule['not'] ? 'not' : '').$rule['test']); + $target = ''; + } + + $out .= 'error_class($id, 'test', 'target') + . ' style="display:' . ($rule['test']!='size' && $rule['test'] != 'exists' ? 'inline' : 'none') . '" />'."\n"; + + $select_size_op = new html_select(array('name' => "_rule_size_op[]", 'id' => 'rule_size_op'.$id)); + $select_size_op->add(Q($this->gettext('filterunder')), 'under'); + $select_size_op->add(Q($this->gettext('filterover')), 'over'); + + $out .= '
'; + $out .= $select_size_op->show($rule['test']=='size' ? $rule['type'] : ''); + $out .= 'error_class($id, 'test', 'sizetarget') .' /> + B + kB + MB + GB'; + $out .= '
'; + $out .= '
'; + $out .= ' '; + $out .= ''; + $out .= '
'; + + $out .= $div ? "
\n" : ''; + + return $out; + } + + function action_div($fid, $id, $div=true) + { + $action = isset($this->form) ? $this->form['actions'][$id] : $this->script[$fid]['actions'][$id]; + $rows_num = isset($this->form) ? sizeof($this->form['actions']) : sizeof($this->script[$fid]['actions']); + + $out = $div ? '
'."\n" : ''; + + $out .= ''; + + // actions target inputs + $out .= ''; + + // add/del buttons + $out .= ''; + + $out .= '
'; + + // action select + $select_action = new html_select(array('name' => "_action_type[]", 'id' => 'action_type'.$id, + 'onchange' => 'action_type_select(' .$id .')')); + if (in_array('fileinto', $this->exts)) + $select_action->add(Q($this->gettext('messagemoveto')), 'fileinto'); + $select_action->add(Q($this->gettext('messageredirect')), 'redirect'); + if (in_array('reject', $this->exts)) + $select_action->add(Q($this->gettext('messagediscard')), 'reject'); + elseif (in_array('ereject', $this->exts)) + $select_action->add(Q($this->gettext('messagediscard')), 'ereject'); + if (in_array('vacation', $this->exts)) + $select_action->add(Q($this->gettext('messagereply')), 'vacation'); + $select_action->add(Q($this->gettext('messagedelete')), 'discard'); + $select_action->add(Q($this->gettext('rulestop')), 'stop'); + + $out .= $select_action->show($action['type']); + $out .= ''; + // shared targets + $out .= 'error_class($id, 'action', 'target') .' />'; + $out .= '\n"; + + // vacation + $out .= '
'; + $out .= ''. Q($this->gettext('vacationreason')) .'
' + .'\n"; + $out .= '
' .Q($this->gettext('vacationaddresses')) . '
' + .'error_class($id, 'action', 'addresses') .' />'; + $out .= '
' . Q($this->gettext('vacationdays')) . '
' + .'error_class($id, 'action', 'days') .' />'; + $out .= '
'; + + // mailbox select + $out .= ''; + $out .= '
'; + $out .= ' '; + $out .= ''; + $out .= '
'; + + $out .= $div ? "
\n" : ''; + + return $out; + } + + private function genid() + { + $result = intval(rcube_timer()); + return $result; + } + + private function strip_value($str) + { + return trim(strip_tags($str)); + } + + private function error_class($id, $type, $target, $name_only=false) + { + // TODO: tooltips + if ($type == 'test' && isset($this->errors['tests'][$id][$target])) + return ($name_only ? 'error' : ' class="error"'); + elseif ($type == 'action' && isset($this->errors['actions'][$id][$target])) + return ($name_only ? 'error' : ' class="error"'); + + return ''; + } + + private function check_email($email) + { + // Check for invalid characters + if (preg_match('/[\x00-\x1F\x7F-\xFF]/', $email)) + return false; + + // Check that there's one @ symbol, and that the lengths are right + if (!preg_match('/^[^@]{1,64}@[^@]{1,255}$/', $email)) + return false; + + // Split it into sections to make life easier + $email_array = explode('@', $email); + + // Check local part + $local_array = explode('.', $email_array[0]); + foreach ($local_array as $local_part) + if (!preg_match('/^(([A-Za-z0-9!#$%&\'*+\/=?^_`{|}~-]+)|("[^"]+"))$/', $local_part)) + return false; + + // Check domain part + if (preg_match('/^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}$/', $email_array[1]) + || preg_match('/^\[(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}\]$/', $email_array[1])) + return true; // If an IP address + else + { // If not an IP address + $domain_array = explode('.', $email_array[1]); + if (sizeof($domain_array) < 2) + return false; // Not enough parts to be a valid domain + + foreach ($domain_array as $domain_part) + if (!preg_match('/^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $domain_part)) + return false; + + return true; + } + + return false; + } + +} + +?> diff --git a/plugins/managesieve/skins/default/filter_add.png b/plugins/managesieve/skins/default/filter_add.png new file mode 100644 index 000000000..f5b34d175 Binary files /dev/null and b/plugins/managesieve/skins/default/filter_add.png differ diff --git a/plugins/managesieve/skins/default/filter_add_pas.png b/plugins/managesieve/skins/default/filter_add_pas.png new file mode 100644 index 000000000..ffe56da05 Binary files /dev/null and b/plugins/managesieve/skins/default/filter_add_pas.png differ diff --git a/plugins/managesieve/skins/default/filter_add_sel.png b/plugins/managesieve/skins/default/filter_add_sel.png new file mode 100644 index 000000000..c773009fd Binary files /dev/null and b/plugins/managesieve/skins/default/filter_add_sel.png differ diff --git a/plugins/managesieve/skins/default/filter_del.png b/plugins/managesieve/skins/default/filter_del.png new file mode 100644 index 000000000..92dc58903 Binary files /dev/null and b/plugins/managesieve/skins/default/filter_del.png differ diff --git a/plugins/managesieve/skins/default/filter_del_pas.png b/plugins/managesieve/skins/default/filter_del_pas.png new file mode 100644 index 000000000..f4ec48c57 Binary files /dev/null and b/plugins/managesieve/skins/default/filter_del_pas.png differ diff --git a/plugins/managesieve/skins/default/filter_del_sel.png b/plugins/managesieve/skins/default/filter_del_sel.png new file mode 100644 index 000000000..421e32bcc Binary files /dev/null and b/plugins/managesieve/skins/default/filter_del_sel.png differ diff --git a/plugins/managesieve/skins/default/filter_down.png b/plugins/managesieve/skins/default/filter_down.png new file mode 100644 index 000000000..451129dfe Binary files /dev/null and b/plugins/managesieve/skins/default/filter_down.png differ diff --git a/plugins/managesieve/skins/default/filter_down_pas.png b/plugins/managesieve/skins/default/filter_down_pas.png new file mode 100644 index 000000000..afa2ca591 Binary files /dev/null and b/plugins/managesieve/skins/default/filter_down_pas.png differ diff --git a/plugins/managesieve/skins/default/filter_down_sel.png b/plugins/managesieve/skins/default/filter_down_sel.png new file mode 100644 index 000000000..60614f99d Binary files /dev/null and b/plugins/managesieve/skins/default/filter_down_sel.png differ diff --git a/plugins/managesieve/skins/default/filter_up.png b/plugins/managesieve/skins/default/filter_up.png new file mode 100644 index 000000000..675561c3a Binary files /dev/null and b/plugins/managesieve/skins/default/filter_up.png differ diff --git a/plugins/managesieve/skins/default/filter_up_pas.png b/plugins/managesieve/skins/default/filter_up_pas.png new file mode 100644 index 000000000..73551893c Binary files /dev/null and b/plugins/managesieve/skins/default/filter_up_pas.png differ diff --git a/plugins/managesieve/skins/default/filter_up_sel.png b/plugins/managesieve/skins/default/filter_up_sel.png new file mode 100644 index 000000000..7c818662a Binary files /dev/null and b/plugins/managesieve/skins/default/filter_up_sel.png differ diff --git a/plugins/managesieve/skins/default/managesieve.css b/plugins/managesieve/skins/default/managesieve.css new file mode 100644 index 000000000..fa5c6bdcc --- /dev/null +++ b/plugins/managesieve/skins/default/managesieve.css @@ -0,0 +1,157 @@ +/***** RoundCube|Filters styles *****/ + + +#filterslist +{ + position: absolute; + left: 20px; + width: 220px; + top: 130px; + bottom: 30px; + border: 1px solid #999999; + background-color: #F9F9F9; + overflow: auto; + /* css hack for IE */ + height: expression((parseInt(document.documentElement.clientHeight)-155)+'px'); +} + +#filters-table +{ + width: 100%; + table-layout: fixed; + /* css hack for IE */ + width: expression(document.getElementById('filterslist').clientWidth); +} + +#filters-table tbody td +{ + cursor: pointer; +} + +#filtersbuttons +{ + position: absolute; + left: 20px; + top: 95px; +} + +#filter-box +{ + position: absolute; + top: 95px; + left: 250px; + right: 20px; + bottom: 30px; + border: 1px solid #999999; + overflow: hidden; + /* css hack for IE */ + width: expression((parseInt(document.documentElement.clientWidth)-30-parseInt(document.getElementById('filterslist').offsetLeft)-parseInt(document.getElementById('filterslist').offsetWidth))+'px'); + height: expression((parseInt(document.documentElement.clientHeight)-120)+'px'); +} + +#filter-frame +{ + background-color: #F9F9F9; + border: none; +} + +body.iframe +{ + background-color: #F9F9F9; + min-width: 740px; + width: expression(Math.max(740, document.documentElement.clientWidth)+'px'); +} + +#filter-form +{ + min-width: 650px; + white-space: nowrap; + background-color: #F9F9F9; + padding: 20px 10px 10px 10px; +} + +#filter-form input, select +{ + font-size: 10pt; + font-family: inherit; +} + +fieldset +{ + background-color: white; +} + +label +{ + color: #666666; +} + +#rules, #actions +{ + margin-top: 5px; + width: 100%; + padding: 0; + border-collapse: collapse; +} + +div.rulerow, div.actionrow +{ + width: 100%; + padding: 2px; + white-space: nowrap; + float: left; + border: 1px solid white; + display: block; +} + +div.rulerow:hover, div.actionrow:hover +{ + padding: 2px; + white-space: nowrap; + display: block; + float: left; + background: #F2F2F2; + border: 1px solid silver; +} + +div.rulerow table, div.actionrow table +{ + width: 100%; + padding: 0px; +} + +td.rowbuttons +{ + width: 98%; + text-align: right; + white-space: nowrap; +} + +td.rowactions, td.rowtargets +{ + width: 1%; + white-space: nowrap; +} + +input.disabled, input.disabled:hover +{ + color: #999999; +} + +input.error, textarea.error +{ + background-color: #FFFF88; +} + +input.box, +input.radio +{ + border: 0; +} + +span.label +{ + color: #666666; + font-size: 10px; + white-space: nowrap; +} diff --git a/plugins/managesieve/skins/default/templates/managesieve.html b/plugins/managesieve/skins/default/templates/managesieve.html new file mode 100644 index 000000000..998df568f --- /dev/null +++ b/plugins/managesieve/skins/default/templates/managesieve.html @@ -0,0 +1,32 @@ + + + +<roundcube:object name="pagetitle" /> + + + + + + + + + + + +
+ + + + +
+ +
+ +
+ +
+ +
+ + + diff --git a/plugins/managesieve/skins/default/templates/managesieveedit.html b/plugins/managesieve/skins/default/templates/managesieveedit.html new file mode 100644 index 000000000..f03945eab --- /dev/null +++ b/plugins/managesieve/skins/default/templates/managesieveedit.html @@ -0,0 +1,110 @@ + + + +<roundcube:object name="pagetitle" /> + + + + + + + + +
+ + +

+ +

+ + +
+ + + +