From a393c37c4fdda083dc2afd525b489fe78a38ce0e Mon Sep 17 00:00:00 2001 From: alecpl Date: Thu, 21 May 2009 07:14:47 +0000 Subject: [PATCH] - managesieve plugin --- plugins/managesieve/Changelog | 59 + plugins/managesieve/lib/Net/Sieve.php | 1159 +++++++++++++++++ plugins/managesieve/lib/rcube_sieve.php | 729 +++++++++++ plugins/managesieve/localization/bg_BG.inc | 50 + plugins/managesieve/localization/de_DE.inc | 53 + plugins/managesieve/localization/en_US.inc | 53 + plugins/managesieve/localization/fi_FI.inc | 49 + plugins/managesieve/localization/fr_FR.inc | 53 + plugins/managesieve/localization/gb_GB.inc | 53 + plugins/managesieve/localization/hu_HU.inc | 54 + plugins/managesieve/localization/nl_NL.inc | 49 + plugins/managesieve/localization/pl_PL.inc | 53 + plugins/managesieve/localization/ru_RU.inc | 49 + plugins/managesieve/localization/sv_SE.inc | 54 + plugins/managesieve/localization/zh_CN.inc | 49 + plugins/managesieve/managesieve.js | 381 ++++++ plugins/managesieve/managesieve.php | 863 ++++++++++++ .../managesieve/skins/default/filter_add.png | Bin 0 -> 1812 bytes .../skins/default/filter_add_pas.png | Bin 0 -> 1571 bytes .../skins/default/filter_add_sel.png | Bin 0 -> 1800 bytes .../managesieve/skins/default/filter_del.png | Bin 0 -> 1881 bytes .../skins/default/filter_del_pas.png | Bin 0 -> 1637 bytes .../skins/default/filter_del_sel.png | Bin 0 -> 1856 bytes .../managesieve/skins/default/filter_down.png | Bin 0 -> 1882 bytes .../skins/default/filter_down_pas.png | Bin 0 -> 1614 bytes .../skins/default/filter_down_sel.png | Bin 0 -> 1865 bytes .../managesieve/skins/default/filter_up.png | Bin 0 -> 1868 bytes .../skins/default/filter_up_pas.png | Bin 0 -> 1608 bytes .../skins/default/filter_up_sel.png | Bin 0 -> 1861 bytes .../managesieve/skins/default/managesieve.css | 157 +++ .../skins/default/templates/managesieve.html | 32 + .../default/templates/managesieveedit.html | 110 ++ 32 files changed, 4109 insertions(+) create mode 100644 plugins/managesieve/Changelog create mode 100644 plugins/managesieve/lib/Net/Sieve.php create mode 100644 plugins/managesieve/lib/rcube_sieve.php create mode 100644 plugins/managesieve/localization/bg_BG.inc create mode 100644 plugins/managesieve/localization/de_DE.inc create mode 100644 plugins/managesieve/localization/en_US.inc create mode 100644 plugins/managesieve/localization/fi_FI.inc create mode 100644 plugins/managesieve/localization/fr_FR.inc create mode 100644 plugins/managesieve/localization/gb_GB.inc create mode 100644 plugins/managesieve/localization/hu_HU.inc create mode 100644 plugins/managesieve/localization/nl_NL.inc create mode 100644 plugins/managesieve/localization/pl_PL.inc create mode 100644 plugins/managesieve/localization/ru_RU.inc create mode 100644 plugins/managesieve/localization/sv_SE.inc create mode 100644 plugins/managesieve/localization/zh_CN.inc create mode 100644 plugins/managesieve/managesieve.js create mode 100644 plugins/managesieve/managesieve.php create mode 100644 plugins/managesieve/skins/default/filter_add.png create mode 100644 plugins/managesieve/skins/default/filter_add_pas.png create mode 100644 plugins/managesieve/skins/default/filter_add_sel.png create mode 100644 plugins/managesieve/skins/default/filter_del.png create mode 100644 plugins/managesieve/skins/default/filter_del_pas.png create mode 100644 plugins/managesieve/skins/default/filter_del_sel.png create mode 100644 plugins/managesieve/skins/default/filter_down.png create mode 100644 plugins/managesieve/skins/default/filter_down_pas.png create mode 100644 plugins/managesieve/skins/default/filter_down_sel.png create mode 100644 plugins/managesieve/skins/default/filter_up.png create mode 100644 plugins/managesieve/skins/default/filter_up_pas.png create mode 100644 plugins/managesieve/skins/default/filter_up_sel.png create mode 100644 plugins/managesieve/skins/default/managesieve.css create mode 100644 plugins/managesieve/skins/default/templates/managesieve.html create mode 100644 plugins/managesieve/skins/default/templates/managesieveedit.html 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 0000000000000000000000000000000000000000..f5b34d175eaf12740dece2f0ded5b2afc991f1f6 GIT binary patch literal 1812 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 VXH!QY=a&Eg002ovPDHLkV1oY6_5uI^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c773009fde2e532cfb7136a0fcaf8dfd8379c34f GIT binary patch literal 1800 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 Uv^}2YC;$Ke07*qoM6N<$f)W^tTL1t6 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..afa2ca59108a4e9b1ce5bde40911c9f54aec81a2 GIT binary patch literal 1614 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 literal 0 HcmV?d00001 diff --git a/plugins/managesieve/skins/default/filter_up.png b/plugins/managesieve/skins/default/filter_up.png new file mode 100644 index 0000000000000000000000000000000000000000..675561c3a23a91557404bdcea1d5b044fbfa31d3 GIT binary patch literal 1868 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 literal 0 HcmV?d00001 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" /> + + + + + + + + +
+ + +

+ +

+ + +
+ + + +