From 613f96839cbf63d475a4f56cb1841647bb23ad0c Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Tue, 26 Aug 2014 01:24:21 -0400 Subject: Added simple API to manage vacation rule --- plugins/managesieve/Changelog | 1 + plugins/managesieve/lib/Roundcube/rcube_sieve.php | 76 +++---- .../lib/Roundcube/rcube_sieve_engine.php | 189 ++++++++++------- .../lib/Roundcube/rcube_sieve_vacation.php | 230 +++++++++++++++++++++ plugins/managesieve/managesieve.php | 4 +- 5 files changed, 386 insertions(+), 114 deletions(-) diff --git a/plugins/managesieve/Changelog b/plugins/managesieve/Changelog index 01afe6906..88526168e 100644 --- a/plugins/managesieve/Changelog +++ b/plugins/managesieve/Changelog @@ -1,3 +1,4 @@ +- Added simple API to manage vacation rule - Fix missing css/js scripts in filter form in mail task - Fix default vacation status (#1490019) - Make possible to set vacation start/end date and time diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve.php b/plugins/managesieve/lib/Roundcube/rcube_sieve.php index a8e29d77c..389c85012 100644 --- a/plugins/managesieve/lib/Roundcube/rcube_sieve.php +++ b/plugins/managesieve/lib/Roundcube/rcube_sieve.php @@ -22,17 +22,6 @@ // Managesieve Protocol: RFC5804 -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_DELETE', 6); // script deletion -define('SIEVE_ERROR_INTERNAL', 7); // internal error -define('SIEVE_ERROR_DEACTIVATE', 8); // script activation -define('SIEVE_ERROR_OTHER', 255); // other/unknown error - - class rcube_sieve { private $sieve; // Net_Sieve object @@ -43,6 +32,16 @@ class rcube_sieve public $current; // name of currently loaded script private $exts; // array of supported extensions + const ERROR_CONNECTION = 1; + const ERROR_LOGIN = 2; + const ERROR_NOT_EXISTS = 3; // script not exists + const ERROR_INSTALL = 4; // script installation + const ERROR_ACTIVATE = 5; // script activation + const ERROR_DELETE = 6; // script deletion + const ERROR_INTERNAL = 7; // internal error + const ERROR_DEACTIVATE = 8; // script activation + const ERROR_OTHER = 255; // other/unknown error + /** * Object constructor @@ -70,7 +69,7 @@ class rcube_sieve } if (PEAR::isError($this->sieve->connect($host, $port, $options, $usetls))) { - return $this->_set_error(SIEVE_ERROR_CONNECTION); + return $this->_set_error(self::ERROR_CONNECTION); } if (!empty($auth_cid)) { @@ -82,7 +81,7 @@ class rcube_sieve if (PEAR::isError($this->sieve->login($username, $password, $auth_type ? strtoupper($auth_type) : null, $authz)) ) { - return $this->_set_error(SIEVE_ERROR_LOGIN); + return $this->_set_error(self::ERROR_LOGIN); } $this->exts = $this->get_extensions(); @@ -117,10 +116,10 @@ class rcube_sieve public function save($name = null) { if (!$this->sieve) - return $this->_set_error(SIEVE_ERROR_INTERNAL); + return $this->_set_error(self::ERROR_INTERNAL); if (!$this->script) - return $this->_set_error(SIEVE_ERROR_INTERNAL); + return $this->_set_error(self::ERROR_INTERNAL); if (!$name) $name = $this->current; @@ -131,7 +130,7 @@ class rcube_sieve $script = '/* empty script */'; if (PEAR::isError($this->sieve->installScript($name, $script))) - return $this->_set_error(SIEVE_ERROR_INSTALL); + return $this->_set_error(self::ERROR_INSTALL); return true; } @@ -142,13 +141,13 @@ class rcube_sieve public function save_script($name, $content = null) { if (!$this->sieve) - return $this->_set_error(SIEVE_ERROR_INTERNAL); + return $this->_set_error(self::ERROR_INTERNAL); if (!$content) $content = '/* empty script */'; if (PEAR::isError($this->sieve->installScript($name, $content))) - return $this->_set_error(SIEVE_ERROR_INSTALL); + return $this->_set_error(self::ERROR_INSTALL); return true; } @@ -159,13 +158,13 @@ class rcube_sieve public function activate($name = null) { if (!$this->sieve) - return $this->_set_error(SIEVE_ERROR_INTERNAL); + return $this->_set_error(self::ERROR_INTERNAL); if (!$name) $name = $this->current; if (PEAR::isError($this->sieve->setActive($name))) - return $this->_set_error(SIEVE_ERROR_ACTIVATE); + return $this->_set_error(self::ERROR_ACTIVATE); return true; } @@ -176,10 +175,10 @@ class rcube_sieve public function deactivate() { if (!$this->sieve) - return $this->_set_error(SIEVE_ERROR_INTERNAL); + return $this->_set_error(self::ERROR_INTERNAL); if (PEAR::isError($this->sieve->setActive(''))) - return $this->_set_error(SIEVE_ERROR_DEACTIVATE); + return $this->_set_error(self::ERROR_DEACTIVATE); return true; } @@ -190,7 +189,7 @@ class rcube_sieve public function remove($name = null) { if (!$this->sieve) - return $this->_set_error(SIEVE_ERROR_INTERNAL); + return $this->_set_error(self::ERROR_INTERNAL); if (!$name) $name = $this->current; @@ -198,10 +197,10 @@ class rcube_sieve // script must be deactivated first if ($name == $this->sieve->getActive()) if (PEAR::isError($this->sieve->setActive(''))) - return $this->_set_error(SIEVE_ERROR_DELETE); + return $this->_set_error(self::ERROR_DELETE); if (PEAR::isError($this->sieve->removeScript($name))) - return $this->_set_error(SIEVE_ERROR_DELETE); + return $this->_set_error(self::ERROR_DELETE); if ($name == $this->current) $this->current = null; @@ -218,9 +217,14 @@ class rcube_sieve return $this->exts; if (!$this->sieve) - return $this->_set_error(SIEVE_ERROR_INTERNAL); + return $this->_set_error(self::ERROR_INTERNAL); $ext = $this->sieve->getExtensions(); + + if (PEAR::isError($ext)) { + return array(); + } + // we're working on lower-cased names $ext = array_map('strtolower', (array) $ext); @@ -242,12 +246,12 @@ class rcube_sieve if (!$this->list) { if (!$this->sieve) - return $this->_set_error(SIEVE_ERROR_INTERNAL); + return $this->_set_error(self::ERROR_INTERNAL); $list = $this->sieve->listScripts(); if (PEAR::isError($list)) - return $this->_set_error(SIEVE_ERROR_OTHER); + return $this->_set_error(self::ERROR_OTHER); $this->list = $list; } @@ -261,7 +265,7 @@ class rcube_sieve public function get_active() { if (!$this->sieve) - return $this->_set_error(SIEVE_ERROR_INTERNAL); + return $this->_set_error(self::ERROR_INTERNAL); return $this->sieve->getActive(); } @@ -272,7 +276,7 @@ class rcube_sieve public function load($name) { if (!$this->sieve) - return $this->_set_error(SIEVE_ERROR_INTERNAL); + return $this->_set_error(self::ERROR_INTERNAL); if ($this->current == $name) return true; @@ -280,7 +284,7 @@ class rcube_sieve $script = $this->sieve->getScript($name); if (PEAR::isError($script)) - return $this->_set_error(SIEVE_ERROR_OTHER); + return $this->_set_error(self::ERROR_OTHER); // try to parse from Roundcube format $this->script = $this->_parse($script); @@ -296,7 +300,7 @@ class rcube_sieve public function load_script($script) { if (!$this->sieve) - return $this->_set_error(SIEVE_ERROR_INTERNAL); + return $this->_set_error(self::ERROR_INTERNAL); // try to parse from Roundcube format $this->script = $this->_parse($script); @@ -341,12 +345,12 @@ class rcube_sieve public function get_script($name) { if (!$this->sieve) - return $this->_set_error(SIEVE_ERROR_INTERNAL); + return $this->_set_error(self::ERROR_INTERNAL); $content = $this->sieve->getScript($name); if (PEAR::isError($content)) - return $this->_set_error(SIEVE_ERROR_OTHER); + return $this->_set_error(self::ERROR_OTHER); return $content; } @@ -357,13 +361,13 @@ class rcube_sieve public function copy($name, $copy) { if (!$this->sieve) - return $this->_set_error(SIEVE_ERROR_INTERNAL); + return $this->_set_error(self::ERROR_INTERNAL); if ($copy) { $content = $this->sieve->getScript($copy); if (PEAR::isError($content)) - return $this->_set_error(SIEVE_ERROR_OTHER); + return $this->_set_error(self::ERROR_OTHER); } return $this->save_script($name, $content); diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php index 59e116ffb..302c7c7a1 100644 --- a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php +++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php @@ -73,7 +73,7 @@ class rcube_sieve_engine */ function __construct($plugin) { - $this->rc = rcmail::get_instance(); + $this->rc = rcube::get_instance(); $this->plugin = $plugin; } @@ -91,6 +91,76 @@ class rcube_sieve_engine 'filtersetform' => array($this, 'filterset_form'), )); + // connect to managesieve server + $error = $this->connect($_SESSION['username'], $this->rc->decrypt($_SESSION['password'])); + + // load current/active script + if (!$error) { + // Get list of scripts + $list = $this->list_scripts(); + + // reset current script when entering filters UI (#1489412) + if ($this->rc->action == 'plugin.managesieve') { + $this->rc->session->remove('managesieve_current'); + } + + if ($mode != 'vacation') { + if (!empty($_GET['_set']) || !empty($_POST['_set'])) { + $script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_GPC, true); + } + else if (!empty($_SESSION['managesieve_current'])) { + $script_name = $_SESSION['managesieve_current']; + } + } + + $error = $this->load_script($script_name); + } + + // finally set script objects + if ($error) { + switch ($error) { + case rcube_sieve::ERROR_CONNECTION: + case rcube_sieve::ERROR_LOGIN: + $this->rc->output->show_message('managesieve.filterconnerror', 'error'); + rcube::raise_error(array('code' => 403, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Unable to connect to managesieve on $host:$port"), true, false); + break; + + default: + $this->rc->output->show_message('managesieve.filterunknownerror', 'error'); + break; + } + + // reload interface in case of possible error when specified script wasn't found (#1489412) + if ($script_name !== null && !empty($list) && !in_array($script_name, $list)) { + $this->rc->output->command('reload', 500); + } + + // to disable 'Add filter' button set env variable + $this->rc->output->set_env('filterconnerror', true); + $this->script = array(); + } + else { + $this->exts = $this->sieve->get_extensions(); + $this->init_script(); + $this->rc->output->set_env('currentset', $this->sieve->current); + $_SESSION['managesieve_current'] = $this->sieve->current; + } + + return $error; + } + + /** + * Connect to configured managesieve server + * + * @param string $username User login + * @param string $password User password + * + * @return int Connection status: 0 on success, >0 on failure + */ + public function connect($username, $password) + { // Get connection parameters $host = $this->rc->config->get('managesieve_host', 'localhost'); $port = $this->rc->config->get('managesieve_port'); @@ -112,8 +182,8 @@ class rcube_sieve_engine } $plugin = $this->rc->plugins->exec_hook('managesieve_connect', array( - 'user' => $_SESSION['username'], - 'password' => $this->rc->decrypt($_SESSION['password']), + 'user' => $username, + 'password' => $password, 'host' => $host, 'port' => $port, 'usetls' => $tls, @@ -140,94 +210,61 @@ class rcube_sieve_engine $plugin['socket_options'] ); - if (!($error = $this->sieve->error())) { - // Get list of scripts - $list = $this->list_scripts(); + return $this->sieve->error(); + } - // reset current script when entering filters UI (#1489412) - if ($this->rc->action == 'plugin.managesieve') { - $this->rc->session->remove('managesieve_current'); - } + /** + * Load specified (or active) script + * + * @param string $script_name Optional script name + * + * @return int Connection status: 0 on success, >0 on failure + */ + public function load_script($script_name = null) + { + // Get list of scripts + $list = $this->list_scripts(); - if ($mode != 'vacation') { - if (!empty($_GET['_set']) || !empty($_POST['_set'])) { - $script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_GPC, true); - } - else if (!empty($_SESSION['managesieve_current'])) { - $script_name = $_SESSION['managesieve_current']; - } + if ($script_name === null || $script_name === '') { + // get (first) active script + if (!empty($this->active[0])) { + $script_name = $this->active[0]; + } + else if ($list) { + $script_name = $list[0]; } + // create a new (initial) script + else { + // if script not exists build default script contents + $script_file = $this->rc->config->get('managesieve_default'); + $script_name = $this->rc->config->get('managesieve_script_name'); - if ($script_name === null || $script_name === '') { - // get (first) active script - if (!empty($this->active[0])) { - $script_name = $this->active[0]; - } - else if ($list) { - $script_name = $list[0]; + if (empty($script_name)) { + $script_name = 'roundcube'; } - // create a new (initial) script - else { - // if script not exists build default script contents - $script_file = $this->rc->config->get('managesieve_default'); - $script_name = $this->rc->config->get('managesieve_script_name'); - - if (empty($script_name)) - $script_name = 'roundcube'; - if ($script_file && is_readable($script_file)) - $content = file_get_contents($script_file); - - // add script and set it active - if ($this->sieve->save_script($script_name, $content)) { - $this->activate_script($script_name); - $this->list[] = $script_name; - } + if ($script_file && is_readable($script_file)) { + $content = file_get_contents($script_file); } - } - if ($script_name) { - $this->sieve->load($script_name); + // add script and set it active + if ($this->sieve->save_script($script_name, $content)) { + $this->activate_script($script_name); + $this->list[] = $script_name; + } } - - $error = $this->sieve->error(); } - // finally set script objects - if ($error) { - switch ($error) { - case SIEVE_ERROR_CONNECTION: - case SIEVE_ERROR_LOGIN: - $this->rc->output->show_message('managesieve.filterconnerror', 'error'); - rcube::raise_error(array('code' => 403, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Unable to connect to managesieve on $host:$port"), true, false); - break; - - default: - $this->rc->output->show_message('managesieve.filterunknownerror', 'error'); - break; - } - - // reload interface in case of possible error when specified script wasn't found (#1489412) - if ($script_name !== null && !empty($list) && !in_array($script_name, $list)) { - $this->rc->output->command('reload', 500); - } - - // to disable 'Add filter' button set env variable - $this->rc->output->set_env('filterconnerror', true); - $this->script = array(); - } - else { - $this->exts = $this->sieve->get_extensions(); - $this->init_script(); - $this->rc->output->set_env('currentset', $this->sieve->current); - $_SESSION['managesieve_current'] = $this->sieve->current; + if ($script_name) { + $this->sieve->load($script_name); } - return $error; + return $this->sieve->error(); } + /** + * User interface actions handler + */ function actions() { $error = $this->start(); diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve_vacation.php b/plugins/managesieve/lib/Roundcube/rcube_sieve_vacation.php index 820265b86..10aaea0e9 100644 --- a/plugins/managesieve/lib/Roundcube/rcube_sieve_vacation.php +++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_vacation.php @@ -23,6 +23,8 @@ class rcube_sieve_vacation extends rcube_sieve_engine { + protected $error; + function actions() { $error = $this->start('vacation'); @@ -503,4 +505,232 @@ class rcube_sieve_vacation extends rcube_sieve_engine return $result; } + + /** + * API: get vacation rule + * + * @return array Vacation rule information + */ + public function get_vacation() + { + $this->exts = $this->sieve->get_extensions(); + $this->init_script(); + $this->vacation_rule(); + + // check supported extensions + $date_extension = in_array('date', $this->exts); + $regex_extension = in_array('regex', $this->exts); + $seconds_extension = in_array('vacation-seconds', $this->exts); + + // set user's timezone + try { + $timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT')); + } + catch (Exception $e) { + $timezone = new DateTimeZone('GMT'); + } + + if ($date_extension) { + $date_value = array(); + foreach ((array) $this->vacation['tests'] as $test) { + if ($test['test'] == 'currentdate') { + $idx = $test['type'] == 'value-ge' ? 'start' : 'end'; + + if ($test['part'] == 'date') { + $date_value[$idx]['date'] = $test['arg']; + } + else if ($test['part'] == 'iso8601') { + $date_value[$idx]['datetime'] = $test['arg']; + } + } + } + + foreach ($date_value as $idx => $value) { + $$idx = new DateTime($value['datetime'] ?: $value['date'], $timezone); + } + } + else if ($regex_extension) { + // Sieve 'date' extension not available, read start/end from RegEx based rules instead + if ($date_tests = self::parse_regexp_tests($this->vacation['tests'])) { + $from = new DateTime($date_tests['from'] . ' ' . '00:00:00', $timezone); + $to = new DateTime($date_tests['to'] . ' ' . '23:59:59', $timezone); + } + } + + if (isset($this->vacation['seconds'])) { + $interval = $this->vacation['seconds'] . 's'; + } + else if (isset($this->vacation['days'])) { + $interval = $this->vacation['days'] . 'd'; + } + + $vacation = array( + 'supported' => $this->exts, + 'interval' => $interval, + 'start' => $start, + 'end' => $end, + 'enabled' => $this->vacation['reason'] && empty($this->vacation['disabled']), + 'message' => $this->vacation['reason'], + 'subject' => $this->vacation['subject'], + 'action' => $this->vacation['action'], + 'target' => $this->vacation['target'], + 'addresses' => $this->vacation['addresses'], + ); + + return $vacation; + } + + /** + * API: set vacation rule + * + * @param array $vacation Vacation rule information (see self::get_vacation()) + * + * @return bool True on success, False on failure + */ + public function set_vacation($data) + { + $this->exts = $this->sieve->get_extensions(); + $this->error = false; + + $this->init_script(); + $this->vacation_rule(); + + // check supported extensions + $date_extension = in_array('date', $this->exts); + $regex_extension = in_array('regex', $this->exts); + $seconds_extension = in_array('vacation-seconds', $this->exts); + + $vacation['type'] = 'vacation'; + $vacation['reason'] = $this->strip_value(str_replace("\r\n", "\n", $data['message'])); + $vacation['addresses'] = $data['addresses']; + $vacation['subject'] = $data['subject']; + $vacation_tests = (array) $this->vacation['tests']; + + foreach ((array) $vacation['addresses'] as $aidx => $address) { + $vacation['addresses'][$aidx] = $address = trim($address); + + if (empty($address)) { + unset($vacation['addresses'][$aidx]); + } + else if (!rcube_utils::check_email($address)) { + $this->error = "Invalid address in vacation addresses: $address"; + return false; + } + } + + if ($vacation['reason'] == '') { + $this->error = "No vacation message specified"; + return false; + } + + if ($data['interval']) { + if (!preg_match('/^([0-9]+)\s*([sd])$/', $data['interval'], $m)) { + $this->error = "Invalid vacation interval value: " . $data['interval']; + return false; + } + else if ($m[1]) { + $vacation[strtolower($m[2]) == 's' ? 'seconds' : 'days'] = $m[1]; + } + } + + // find and remove existing date/regex/true rules + foreach ((array) $vacation_tests as $idx => $t) { + if ($t['test'] == 'currentdate' || $t['test'] == 'true' + || ($t['test'] == 'header' && $t['type'] == 'regex' && $t['arg1'] == 'received') + ) { + unset($vacation_tests[$idx]); + } + } + + if ($date_extension) { + foreach (array('start', 'end') as $var) { + if ($dt = $data[$var]) { + $vacation_tests[] = array( + 'test' => 'currentdate', + 'part' => 'iso8601', + 'type' => 'value-' . ($var == 'start' ? 'ge' : 'le'), + 'zone' => $dt->format('O'), + 'arg' => str_replace('+00:00', 'Z', strtoupper($dt->format('c'))), + ); + } + } + } + else if ($regex_extension) { + // Add date range rules if range specified + if ($data['start'] && $data['end']) { + if ($tests = self::build_regexp_tests($data['start'], $data['end'], $error)) { + $vacation_tests = array_merge($vacation_tests, $tests); + } + + if ($error) { + $this->error = "Invalid dates specified or unsupported period length"; + return false; + } + } + } + + if ($data['action'] == 'redirect' || $data['action'] == 'copy') { + if (empty($data['target']) || !rcube_utils::check_email($data['target'])) { + $this->error = "Invalid address in action taget: " . $data['target']; + return false; + } + } + else if ($data['action'] && $data['action'] != 'keep' && $data['action'] != 'discard') { + $this->error = "Unsupported vacation action: " . $data['action']; + return false; + } + + if (empty($vacation_tests)) { + $vacation_tests = $this->rc->config->get('managesieve_vacation_test', array(array('test' => 'true'))); + } + + // @TODO: handle situation when there's no active script + + $rule = $this->vacation; + $rule['type'] = 'if'; + $rule['name'] = $rule['name'] ?: 'Out-of-Office'; + $rule['disabled'] = isset($data['enabled']) && !$data['enabled']; + $rule['tests'] = $vacation_tests; + $rule['join'] = $date_extension ? count($vacation_tests) > 1 : false; + $rule['actions'] = array($vacation); + + if ($data['action'] && $data['action'] != 'keep') { + $rule['actions'][] = array( + 'type' => $data['action'] == 'discard' ? 'discard' : 'redirect', + 'copy' => $data['action'] == 'copy', + 'target' => $data['action'] != 'discard' ? $data['target'] : '', + ); + } + + // reset original vacation rule + if (isset($this->vacation['idx'])) { + $this->script[$this->vacation['idx']] = null; + } + + array_unshift($this->script, $rule); + + $this->sieve->script->content = array_values(array_filter($this->script)); + + return $this->save_script(); + } + + /** + * API: connect to managesieve server + */ + public function connect($username, $password) + { + if (!parent::connect($username, $password)) { + return $this->load_script(); + } + } + + /** + * API: Returns last error + * + * @return string Error message + */ + public function get_error() + { + return $this->error; + } } diff --git a/plugins/managesieve/managesieve.php b/plugins/managesieve/managesieve.php index 60be9bd22..478f26b00 100644 --- a/plugins/managesieve/managesieve.php +++ b/plugins/managesieve/managesieve.php @@ -37,7 +37,7 @@ class managesieve extends rcube_plugin function init() { - $this->rc = rcmail::get_instance(); + $this->rc = rcube::get_instance(); // register actions $this->register_action('plugin.managesieve', array($this, 'managesieve_actions')); @@ -230,7 +230,7 @@ class managesieve extends rcube_plugin /** * Initializes engine object */ - private function get_engine($type = null) + public function get_engine($type = null) { if (!$this->engine) { $this->load_config(); -- cgit v1.2.3