From e499a14e13f37aa2031fe7443e5dcd04018aac58 Mon Sep 17 00:00:00 2001
From: Aleksander Machniak <alec@alec.pl>
Date: Thu, 28 Apr 2016 06:28:20 -0400
Subject: [PATCH] - Support 'duplicate' extension [RFC 7352] - Improve errors handling in filter form

---
 CHANGELOG                                                |    3 
 plugins/managesieve/composer.json                        |    2 
 plugins/managesieve/tests/src/parser_duplicate           |   16 ++
 plugins/managesieve/skins/classic/managesieve_mail.css   |    2 
 plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php |   99 +++++++++++++++-
 plugins/managesieve/Changelog                            |    6 +
 plugins/managesieve/skins/larry/managesieve.css          |   18 ++-
 plugins/managesieve/localization/en_US.inc               |   14 ++
 plugins/managesieve/skins/classic/managesieve.css        |   16 +
 plugins/managesieve/lib/Roundcube/rcube_sieve_script.php |  108 ++++++++++++-----
 plugins/managesieve/managesieve.js                       |   23 +++
 11 files changed, 247 insertions(+), 60 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index a9f5ebf..ff3886f 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,9 @@
 CHANGELOG Roundcube Webmail
 ===========================
 
+- Managesieve: Support 'duplicate' extension [RFC 7352]
+- Managesieve: Unhide advanced rule controls if there are inputs with errors
+- Managesieve: Display warning message when filter form contains errors
 - Enigma: Added enigma_debug option
 - Fix message list multi-select/deselect issue (#5219)
 - Fix bug where getting HTML editor content could steal focus from other form controls (#5223)
diff --git a/plugins/managesieve/Changelog b/plugins/managesieve/Changelog
index be39611..3b42e27 100644
--- a/plugins/managesieve/Changelog
+++ b/plugins/managesieve/Changelog
@@ -1,3 +1,9 @@
+* version 8.7 [2016-]
+-----------------------------------------------------------
+- Support 'duplicate' extension [RFC 7352]
+- Unhide advanced rule controls if there are inputs with errors
+- Display warning message when filter form contains errors
+
 * version 8.6 [2016-04-06]
 -----------------------------------------------------------
 - Refactored script parser to be 100x faster
diff --git a/plugins/managesieve/composer.json b/plugins/managesieve/composer.json
index b1fd1b5..afdda51 100644
--- a/plugins/managesieve/composer.json
+++ b/plugins/managesieve/composer.json
@@ -3,7 +3,7 @@
     "type": "roundcube-plugin",
     "description": "Adds a possibility to manage Sieve scripts (incoming mail filters). It's clickable interface which operates on text scripts and communicates with server using managesieve protocol. Adds Filters tab in Settings.",
     "license": "GPLv3+",
-    "version": "8.6",
+    "version": "8.7",
     "authors": [
         {
             "name": "Aleksander Machniak",
diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
index b013351..3bb5eac 100644
--- a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
+++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
@@ -63,7 +63,7 @@
         1 => 'notifyimportancehigh'
     );
 
-    const VERSION  = '8.6';
+    const VERSION  = '8.7';
     const PROGNAME = 'Roundcube (Managesieve)';
     const PORT     = 4190;
 
@@ -609,6 +609,12 @@
             $lastindexes    = rcube_utils::get_input_value('_rule_index_last', rcube_utils::INPUT_POST);
             $dateheaders    = rcube_utils::get_input_value('_rule_date_header', rcube_utils::INPUT_POST);
             $dateparts      = rcube_utils::get_input_value('_rule_date_part', rcube_utils::INPUT_POST);
+            $message        = rcube_utils::get_input_value('_rule_message', rcube_utils::INPUT_POST);
+            $dup_handles    = rcube_utils::get_input_value('_rule_duplicate_handle', rcube_utils::INPUT_POST, true);
+            $dup_headers    = rcube_utils::get_input_value('_rule_duplicate_header', rcube_utils::INPUT_POST, true);
+            $dup_uniqueids  = rcube_utils::get_input_value('_rule_duplicate_uniqueid', rcube_utils::INPUT_POST, true);
+            $dup_seconds    = rcube_utils::get_input_value('_rule_duplicate_seconds', rcube_utils::INPUT_POST);
+            $dup_lasts      = rcube_utils::get_input_value('_rule_duplicate_last', rcube_utils::INPUT_POST);
             $act_types      = rcube_utils::get_input_value('_action_type', rcube_utils::INPUT_POST, true);
             $mailboxes      = rcube_utils::get_input_value('_action_mailbox', rcube_utils::INPUT_POST, true);
             $act_targets    = rcube_utils::get_input_value('_action_target', rcube_utils::INPUT_POST, true);
@@ -816,6 +822,34 @@
                         $this->form['tests'][$i]['part'] = $trans;
                         if ($trans == 'content') {
                             $this->form['tests'][$i]['content'] = $trans_type;
+                        }
+                    }
+                    else if ($header == 'message') {
+                        $test = $this->strip_value($message[$idx]);
+
+                        if (preg_match('/^not/', $test)) {
+                            $this->form['tests'][$i]['not'] = true;
+                            $test = substr($test, 3);
+                        }
+
+                        $this->form['tests'][$i]['test'] = $test;
+
+                        if ($test == 'duplicate') {
+                            $this->form['tests'][$i]['last']     = !empty($dup_lasts[$idx]);
+                            $this->form['tests'][$i]['handle']   = trim($dup_handles[$idx]);
+                            $this->form['tests'][$i]['header']   = trim($dup_headers[$idx]);
+                            $this->form['tests'][$i]['uniqueid'] = trim($dup_uniqueids[$idx]);
+                            $this->form['tests'][$i]['seconds']  = trim($dup_seconds[$idx]);
+
+                            if ($this->form['tests'][$i]['seconds']
+                                && preg_match('/[^0-9]/', $this->form['tests'][$i]['seconds'])
+                            ) {
+                                $this->errors['tests'][$i]['duplicate_seconds'] = $this->plugin->gettext('forbiddenchars');
+                            }
+
+                            if ($this->form['tests'][$i]['header'] && $this->form['tests'][$i]['uniqueid']) {
+                                $this->errors['tests'][$i]['duplicate_uniqueid'] = $this->plugin->gettext('duplicate.conflict.err');
+                            }
                         }
                     }
                     else {
@@ -1094,6 +1128,9 @@
                     $this->rc->output->show_message('managesieve.filtersaveerror', 'error');
 //                  $this->rc->output->send();
                 }
+            }
+            else {
+                $this->rc->output->show_message('managesieve.filterformerror', 'warning');
             }
         }
 
@@ -1406,12 +1443,16 @@
             $select_header->add($header, $index);
         }
         $select_header->add($this->plugin->gettext('...'), '...');
-        if (in_array('body', $this->exts))
+        if (in_array('body', $this->exts)) {
             $select_header->add($this->plugin->gettext('body'), 'body');
+        }
         $select_header->add($this->plugin->gettext('size'), 'size');
         if (in_array('date', $this->exts)) {
             $select_header->add($this->plugin->gettext('datetest'), 'date');
             $select_header->add($this->plugin->gettext('currdate'), 'currentdate');
+        }
+        if (in_array('duplicate', $this->exts)) {
+            $select_header->add($this->plugin->gettext('message'), 'message');
         }
 
         if (isset($rule['test'])) {
@@ -1433,6 +1474,9 @@
             }
             else if (in_array($rule['test'], array('size', 'body', 'date', 'currentdate'))) {
                 $test = $rule['test'];
+            }
+            else if (in_array($rule['test'], array('duplicate'))) {
+                $test = 'message';
             }
             else if ($rule['test'] != 'true') {
                 $test = '...';
@@ -1460,7 +1504,7 @@
 
         // matching type select (operator)
         $select_op = new html_select(array('name' => "_rule_op[]", 'id' => 'rule_op'.$id,
-            'style' => 'display:' .($rule['test']!='size' ? 'inline' : 'none'),
+            'style' => 'display:' .(!in_array($rule['test'], array('size', 'duplicate')) ? 'inline' : 'none'),
             'class' => 'operator_selector',
             'onchange' => 'rule_op_select(this, '.$id.')'));
         $select_op->add(rcube::Q($this->plugin->gettext('filtercontains')), 'contains');
@@ -1527,9 +1571,22 @@
             $tout .= $select_dp->show($rule['test'] == 'currentdate' || $rule['test'] == 'date' ? $rule['part'] : '');
         }
 
+        // message test select (e.g. duplicate)
+        if (in_array('duplicate', $this->exts)) {
+            $select_msg = new html_select(array('name' => "_rule_message[]", 'id' => 'rule_message'.$id,
+                'style' => in_array($rule['test'], array('duplicate')) ? '' : 'display:none',
+                'class' => 'message_selector',
+            ));
+
+            $select_msg->add(rcube::Q($this->plugin->gettext('duplicate')), 'duplicate');
+            $select_msg->add(rcube::Q($this->plugin->gettext('notduplicate')), 'notduplicate');
+
+            $tout .= $select_msg->show($test);
+        }
+
         $tout .= $select_op->show($test);
         $tout .= $this->list_input($id, 'rule_target', $target,
-            $rule['test'] != 'size' && $rule['test'] != 'exists',
+            $rule['test'] != 'size' && $rule['test'] != 'exists' && $rule['test'] != 'duplicate',
             $this->error_class($id, 'test', 'target', 'rule_target')) . "\n";
 
         $select_size_op = new html_select(array('name' => "_rule_size_op[]", 'id' => 'rule_size_op'.$id));
@@ -1555,8 +1612,9 @@
             'onchange' => 'rule_mod_select(' .$id .')'));
         $select_mod->add(rcube::Q($this->plugin->gettext('none')), '');
         $select_mod->add(rcube::Q($this->plugin->gettext('address')), 'address');
-        if (in_array('envelope', $this->exts))
+        if (in_array('envelope', $this->exts)) {
             $select_mod->add(rcube::Q($this->plugin->gettext('envelope')), 'envelope');
+        }
 
         $select_type = new html_select(array('name' => "_rule_mod_type[]", 'id' => 'rule_mod_type'.$id));
         $select_type->add(rcube::Q($this->plugin->gettext('allparts')), 'all');
@@ -1567,7 +1625,7 @@
             $select_type->add(rcube::Q($this->plugin->gettext('detail')), 'detail');
         }
 
-        $need_mod = !in_array($rule['test'], array('size', 'body', 'date', 'currentdate'));
+        $need_mod = !in_array($rule['test'], array('size', 'body', 'date', 'currentdate', 'duplicate'));
         $mout = '<div id="rule_mod' .$id. '" class="adv"' . (!$need_mod ? ' style="display:none"' : '') . '>';
         $mout .= ' <span class="label">' . rcube::Q($this->plugin->gettext('modifier')) . ' </span>';
         $mout .= $select_mod->show($rule['test']);
@@ -1604,7 +1662,8 @@
         }
 
         // Comparators
-        $mout .= '<div id="rule_comp' .$id. '" class="adv"' . ($rule['test'] == 'size' ? ' style="display:none"' : '') . '>';
+        $need_comp = $rule['test'] != 'size' && $rule['test'] != 'duplicate';
+        $mout .= '<div id="rule_comp' .$id. '" class="adv"' . (!$need_comp ? ' style="display:none"' : '') . '>';
         $mout .= '<span class="label">' . rcube::Q($this->plugin->gettext('comparator')) . '</span>';
         $mout .= $select_comp->show($rule['comparator']);
         $mout .= '</div>';
@@ -1633,6 +1692,32 @@
             $mout .= '</div>';
         }
 
+        // Duplicate
+        if (in_array('duplicate', $this->exts)) {
+            $need_duplicate = $rule['test'] == 'duplicate';
+            $mout .= '<div id="rule_duplicate_div' .$id. '" class="adv"'. (!$need_duplicate ? ' style="display:none"' : '') .'>';
+            $mout .= '<span class="label">' . rcube::Q($this->plugin->gettext('duplicate.handle')) . '</span>';
+            $mout .= '<input type="text" name="_rule_duplicate_handle[]" id="rule_duplicate_handle'.$id
+                . '" value="'. ($rule['handle'] ? rcube::JQ($rule['handle']) : '')
+                . '" size="30"' . $this->error_class($id, 'test', 'duplicate_handle', 'rule_duplicate_handle') .' /><br>';
+            $mout .= '<span class="label">' . rcube::Q($this->plugin->gettext('duplicate.header')) . '</span>';
+            $mout .= '<input type="text" name="_rule_duplicate_header[]" id="rule_duplicate_header'.$id
+                . '" value="'. ($rule['header'] ? rcube::JQ($rule['header']) : '')
+                . '" size="30"' . $this->error_class($id, 'test', 'duplicate_header', 'rule_duplicate_header') .' /><br>';
+            $mout .= '<span class="label">' . rcube::Q($this->plugin->gettext('duplicate.uniqueid')) . '</span>';
+            $mout .= '<input type="text" name="_rule_duplicate_uniqueid[]" id="rule_duplicate_uniqueid'.$id
+                . '" value="'. ($rule['uniqueid'] ? rcube::JQ($rule['uniqueid']) : '')
+                . '" size="30"' . $this->error_class($id, 'test', 'duplicate_uniqueid', 'rule_duplicate_uniqueid') .' /><br>';
+            $mout .= '<span class="label">' . rcube::Q($this->plugin->gettext('duplicate.seconds')) . '</span>';
+            $mout .= '<input type="text" name="_rule_duplicate_seconds[]" id="rule_duplicate_seconds'.$id
+                . '" value="'. rcube::JQ($rule['seconds'])
+                . '" size="6"' . $this->error_class($id, 'test', 'duplicate_seconds', 'rule_duplicate_seconds') .' />';
+            $mout .= '&nbsp;<input type="checkbox" name="_rule_duplicate_last['.$id.']" id="rule_duplicate_last'.$id
+                . '" value="1"' . (!empty($rule['last']) ? ' checked="checked"' : '') . ' />'
+                . '<label for="rule_duplicate_last'.$id.'">'.rcube::Q($this->plugin->gettext('duplicate.last')).'</label>';
+            $mout .= '</div>';
+        }
+
         // Build output table
         $out = $div ? '<div class="rulerow" id="rulerow' .$id .'">'."\n" : '';
         $out .= '<table><tr>';
diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve_script.php b/plugins/managesieve/lib/Roundcube/rcube_sieve_script.php
index c603dd5..b6a121b 100644
--- a/plugins/managesieve/lib/Roundcube/rcube_sieve_script.php
+++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_script.php
@@ -24,21 +24,22 @@
 {
     public $content = array();      // script rules array
 
-    private $vars = array();        // "global" variables
-    private $prefix = '';           // script header (comments)
-    private $supported = array(     // Sieve extensions supported by class
+    private $vars      = array();   // "global" variables
+    private $prefix    = '';        // script header (comments)
+    private $supported = array(     // supported Sieve extensions:
         'body',                     // RFC5173
         'copy',                     // RFC3894
         'date',                     // RFC5260
+        'duplicate',                // RFC7352
         'enotify',                  // RFC5435
         'envelope',                 // RFC5228
         'ereject',                  // RFC5429
         'fileinto',                 // RFC5228
         'imapflags',                // draft-melnikov-sieve-imapflags-06
         'imap4flags',               // RFC5232
-        'include',                  // draft-ietf-sieve-include-12
+        'include',                  // RFC6609
         'index',                    // RFC5260
-        'notify',                   // draft-martin-sieve-notify-01,
+        'notify',                   // RFC5435
         'regex',                    // draft-ietf-sieve-regex-01
         'reject',                   // RFC5429
         'relational',               // RFC3431
@@ -317,7 +318,29 @@
                         $tests[$i] .= ' ' . self::escape_string($test['arg']);
 
                         break;
+
+                    case 'duplicate':
+                        array_push($exts, 'duplicate');
+
+                        $tests[$i] .= ($test['not'] ? 'not ' : '') . $test['test'];
+
+                        $tokens = array('handle', 'uniqueid', 'header');
+                        foreach ($tokens as $token)
+                            if ($test[$token] !== null && $test[$token] !== '') {
+                                $tests[$i] .= " :$token " . self::escape_string($test[$token]);
+                            }
+
+                        if (!empty($test['seconds'])) {
+                            $tests[$i] .= ' :seconds ' . intval($test['seconds']);
+                        }
+
+                        if (!empty($test['last'])) {
+                            $tests[$i] .= ' :last';
+                        }
+
+                        break;
                     }
+
                     $i++;
                 }
             }
@@ -749,6 +772,23 @@
                 $tests[] = $test;
                 break;
 
+            case 'duplicate':
+                $test = array('test' => $token, 'not' => $not);
+
+                for ($i=0, $len=count($tokens); $i<$len; $i++) {
+                    if (!is_array($tokens[$i])) {
+                        if (preg_match('/^:(handle|header|uniqueid|seconds)$/i', $tokens[$i], $m)) {
+                            $test[strtolower($m[1])] = $tokens[++$i];
+                        }
+                        else if (preg_match('/^:last$/i', $tokens[$i])) {
+                            $test['last'] = true;
+                        }
+                    }
+                }
+
+                $tests[] = $test;
+                break;
+
             case 'exists':
                 $tests[] = array('test' => 'exists', 'not' => $not,
                     'arg'  => array_pop($tokens));
@@ -966,33 +1006,25 @@
         $result = array();
 
         for ($i=0, $len=count($tokens); $i<$len; $i++) {
-            if (!is_array($tokens[$i]) && $tokens[$i][0] == ':') {
-                if (preg_match('/^:comparator$/i', $tokens[$i])) {
-                    $test['comparator'] = $tokens[++$i];
-                    continue;
-                }
-
-                if (preg_match('/^:(count|value)$/i', $tokens[$i])) {
-                    $test['type'] = strtolower(substr($tokens[$i], 1)) . '-' . $tokens[++$i];
-                    continue;
-                }
-
-                if (preg_match('/^:(is|contains|matches|regex)$/i', $tokens[$i])) {
-                    $test['type'] = strtolower(substr($tokens[$i], 1));
-                    continue;
-                }
-
-                if (preg_match('/^:index$/i', $tokens[$i])) {
-                    $test['index'] = intval($tokens[++$i]);
-                    if ($tokens[$i+1] && preg_match('/^:last$/i', $tokens[$i+1])) {
-                        $test['last'] = true;
-                        $i++;
-                    }
-                    continue;
-                }
+            if (!is_array($tokens[$i]) && preg_match('/^:comparator$/i', $tokens[$i])) {
+                $test['comparator'] = $tokens[++$i];
             }
-
-            $result[] = $tokens[$i];
+            else if (!is_array($tokens[$i]) && preg_match('/^:(count|value)$/i', $tokens[$i])) {
+                $test['type'] = strtolower(substr($tokens[$i], 1)) . '-' . $tokens[++$i];
+            }
+            else if (!is_array($tokens[$i]) && preg_match('/^:(is|contains|matches|regex)$/i', $tokens[$i])) {
+                $test['type'] = strtolower(substr($tokens[$i], 1));
+            }
+            else if (!is_array($tokens[$i]) && preg_match('/^:index$/i', $tokens[$i])) {
+                $test['index'] = intval($tokens[++$i]);
+                if ($tokens[$i+1] && preg_match('/^:last$/i', $tokens[$i+1])) {
+                    $test['last'] = true;
+                    $i++;
+                }
+           }
+           else {
+               $result[] = $tokens[$i];
+           }
         }
 
         $tokens = $result;
@@ -1098,7 +1130,6 @@
     {
         $result = array();
         $length = strlen($str);
-        $mask   = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:_';
 
         // remove spaces from the beginning of the string
         while ($position < $length && (!$num || $num === true || count($result) < $num)) {
@@ -1184,11 +1215,18 @@
                 if ($position == $length) {
                     break 2;
                 }
+                if ($length - $position < 2) {
+                    $result[] = substr($str, $position);
+                    $position = $length;
+                    break;
+                }
 
                 // tag/identifier/number
-                if ($len = strspn($str, $mask, $position)) {
-                    $atom      = substr($str, $position, $len);
-                    $position += $len;
+                if (preg_match('/[a-zA-Z0-9:_]+/', $str, $m, PREG_OFFSET_CAPTURE, $position)
+                    && $m[0][1] == $position
+                ) {
+                    $atom      = $m[0][0];
+                    $position += strlen($atom);
 
                     if ($atom != 'text:') {
                         $result[] = $atom;
diff --git a/plugins/managesieve/localization/en_US.inc b/plugins/managesieve/localization/en_US.inc
index 906ffe8..c15193d 100644
--- a/plugins/managesieve/localization/en_US.inc
+++ b/plugins/managesieve/localization/en_US.inc
@@ -56,7 +56,7 @@
 $labels['del'] = 'Delete';
 $labels['sender'] = 'Sender';
 $labels['recipient'] = 'Recipient';
-$labels['vacationaddr'] = 'My email addresses:';
+$labels['vacationaddr'] = 'My e-mail addresses:';
 $labels['vacationdays'] = 'How often send messages (in days):';
 $labels['vacationinterval'] = 'How often send messages:';
 $labels['vacationreason'] = 'Message body (vacation reason):';
@@ -174,7 +174,7 @@
 $labels['vacation.status'] = 'Status';
 $labels['vacation.on'] = 'On';
 $labels['vacation.off'] = 'Off';
-$labels['vacation.addresses'] = 'My email addresses';
+$labels['vacation.addresses'] = 'My e-mail addresses';
 $labels['vacation.interval'] = 'Reply interval';
 $labels['vacation.after'] = 'Put vacation rule after';
 $labels['vacation.saving'] = 'Saving data...';
@@ -191,6 +191,14 @@
 $labels['ariasummaryfiltersetslist'] = 'List of filter sets';
 $labels['filterstitle'] = 'Edit incoming mail filters';
 $labels['vacationtitle'] = 'Edit out-of-office rule';
+$labels['message'] = 'Message';
+$labels['duplicate'] = 'is duplicate';
+$labels['notduplicate'] = 'is not duplicate';
+$labels['duplicate.handle'] = 'handle:';
+$labels['duplicate.header'] = 'header:';
+$labels['duplicate.uniqueid'] = 'identifier:';
+$labels['duplicate.seconds'] = 'timeout (seconds):';
+$labels['duplicate.last'] = 'relative to the last execution';
 
 $messages = array();
 $messages['filterunknownerror'] = 'Unknown server error.';
@@ -199,6 +207,7 @@
 $messages['filterdeleted'] = 'Filter deleted successfully.';
 $messages['filtersaved'] = 'Filter saved successfully.';
 $messages['filtersaveerror'] = 'Unable to save filter. Server error occurred.';
+$messages['filterformerror'] = 'Filter form contains errors.';
 $messages['filterdeleteconfirm'] = 'Do you really want to delete selected filter?';
 $messages['ruledeleteconfirm'] = 'Are you sure, you want to delete selected rule?';
 $messages['actiondeleteconfirm'] = 'Are you sure, you want to delete selected action?';
@@ -228,5 +237,6 @@
 $messages['saveerror'] = 'Unable to save data. Server error occurred.';
 $messages['vacationsaved'] = 'Vacation data saved successfully.';
 $messages['emptyvacationbody'] = 'Body of vacation message is required!';
+$messages['duplicate.conflict.err'] = 'Both header and unique identifier are not alowed.';
 
 ?>
diff --git a/plugins/managesieve/managesieve.js b/plugins/managesieve/managesieve.js
index 117f01a..1316a5a 100644
--- a/plugins/managesieve/managesieve.js
+++ b/plugins/managesieve/managesieve.js
@@ -599,6 +599,7 @@
 {
   var obj = document.getElementById('header' + id),
     size = document.getElementById('rule_size' + id),
+    msg = document.getElementById('rule_message' + id),
     op = document.getElementById('rule_op' + id),
     header = document.getElementById('custom_header' + id + '_list'),
     mod = document.getElementById('rule_mod' + id),
@@ -610,7 +611,11 @@
 
   if (h == 'size') {
     size.style.display = 'inline';
-    $.each([op, header, mod, trans, comp], function() { this.style.display = 'none'; });
+    $.each([op, header, mod, trans, comp, msg], function() { this.style.display = 'none'; });
+  }
+  else if (h == 'message') {
+    msg.style.display = 'inline';
+    $.each([op, header, mod, trans, comp, size], function() { this.style.display = 'none'; });
   }
   else {
     header.style.display = h != '...' ? 'none' : 'inline-block';
@@ -619,6 +624,7 @@
     comp.style.display = '';
     mod.style.display = h == 'body' || h == 'currentdate' || h == 'date' ? 'none' : 'block';
     trans.style.display = h == 'body' ? 'block' : 'none';
+    msg.style.display = h == 'message' ? 'block' : 'none';
   }
 
   if (datepart)
@@ -638,7 +644,7 @@
   if (!header)
     header = document.getElementById('header' + id).value;
 
-  target.style.display = obj.value == 'exists' || obj.value == 'notexists' || header == 'size' ? 'none' : 'inline-block';
+  target.style.display = obj.value.match(/^(exists|notexists)$/) || header.match(/^(size|message)$/) ? 'none' : 'inline-block';
 };
 
 function rule_trans_select(id)
@@ -653,6 +659,7 @@
 {
   var obj = document.getElementById('rule_mod_op' + id),
     target = document.getElementById('rule_mod_type' + id),
+    duplicate = document.getElementById('rule_duplicate_div' + id),
     index = document.getElementById('rule_index_div' + id);
 
   if (!header)
@@ -661,7 +668,10 @@
   target.style.display = obj.value != 'address' && obj.value != 'envelope' ? 'none' : 'inline';
 
   if (index)
-    index.style.display = header != 'body' && header != 'currentdate' && header != 'size' && obj.value != 'envelope'  ? '' : 'none';
+    index.style.display = !header.match(/^(body|currentdate|size|message)$/) && obj.value != 'envelope'  ? '' : 'none';
+
+  if (duplicate)
+    duplicate.style.display = header == 'message' ? '' : 'none';
 };
 
 function rule_join_radio(value)
@@ -954,6 +964,13 @@
     .click(function() {  // show drop-down upon clicks
       $(this).autocomplete('search', $(this).val() || ' ');
     })
+
+  // display advanced controls when contain errors
+  $('input.error').each(function() {
+    if (String(this.id).match(/([0-9]+)$/)) {
+      $('#ruleadv' + RegExp.$1 + '.show').click();
+    }
+  });
 }
 
 
diff --git a/plugins/managesieve/skins/classic/managesieve.css b/plugins/managesieve/skins/classic/managesieve.css
index 7419cd3..84e24f8 100644
--- a/plugins/managesieve/skins/classic/managesieve.css
+++ b/plugins/managesieve/skins/classic/managesieve.css
@@ -172,12 +172,12 @@
 
 td.advbutton a.show
 {
-  background: url(images/down_small.gif) center no-repeat;
+  background: url(images/down_small.gif?v=8629.106) center no-repeat;
 }
 
 td.advbutton a.hide
 {
-  background: url(images/up_small.gif) center no-repeat;
+  background: url(images/up_small.gif?v=c56c.106) center no-repeat;
 }
 
 td.rowbuttons
@@ -225,6 +225,11 @@
   margin-left: 10px;
 }
 
+td.rowtargets div.adv input
+{
+  margin-bottom: 1px;
+}
+
 html.mozilla #filter-form select
 {
   padding-top: 3px;
@@ -255,6 +260,7 @@
 }
 
 td.rowtargets span,
+td.rowtargets label,
 span.label
 {
   color: #666666;
@@ -305,7 +311,7 @@
 
 a.button.add
 {
-  background: url(images/add.png) no-repeat;
+  background: url(images/add.png?v=a165.280) no-repeat;
   width: 30px;
   height: 20px;
   margin-right: 4px;
@@ -314,7 +320,7 @@
 
 a.button.del
 {
-  background: url(images/del.png) no-repeat;
+  background: url(images/del.png?v=3c27.247) no-repeat;
   width: 30px;
   height: 20px;
   display: inline-block;
@@ -400,7 +406,7 @@
   display: inline-block;
   width: 16px;
   height: 16px;
-  background: url(images/erase.png) -1px 0 no-repeat #eee;
+  background: url(images/erase.png?v=3052.453) -1px 0 no-repeat #eee;
   cursor: pointer;
 }
 
diff --git a/plugins/managesieve/skins/classic/managesieve_mail.css b/plugins/managesieve/skins/classic/managesieve_mail.css
index 1ade4f7..a05585f 100644
--- a/plugins/managesieve/skins/classic/managesieve_mail.css
+++ b/plugins/managesieve/skins/classic/managesieve_mail.css
@@ -1,5 +1,5 @@
 #messagemenu li a.filterlink {
-  background-image: url(images/filter.png);
+  background-image: url(images/filter.png?v=b0fe.547);
   background-position: 7px 1px;
 }
 
diff --git a/plugins/managesieve/skins/larry/managesieve.css b/plugins/managesieve/skins/larry/managesieve.css
index 5193b42..baac489 100644
--- a/plugins/managesieve/skins/larry/managesieve.css
+++ b/plugins/managesieve/skins/larry/managesieve.css
@@ -93,6 +93,7 @@
 #filter-form legend, #filter-form label
 {
   color: #666666;
+  vertical-align: middle;
 }
 
 #rules, #actions
@@ -146,12 +147,12 @@
 
 td.advbutton a.show
 {
-  background: url(images/down_small.gif) center no-repeat;
+  background: url(images/down_small.gif?v=8629.106) center no-repeat;
 }
 
 td.advbutton a.hide
 {
-  background: url(images/up_small.gif) center no-repeat;
+  background: url(images/up_small.gif?v=c56c.106) center no-repeat;
 }
 
 td.rowbuttons
@@ -197,6 +198,11 @@
 
 td.rowtargets div a {
   margin-left: 10px;
+}
+
+td.rowtargets div.adv input
+{
+  margin-bottom: 1px;
 }
 
 input.disabled, input.disabled:hover
@@ -282,7 +288,7 @@
 
 a.button.add
 {
-  background: url(images/add.png) no-repeat;
+  background: url(images/add.png?v=a165.280) no-repeat;
   width: 30px;
   height: 20px;
   margin-right: 4px;
@@ -291,7 +297,7 @@
 
 a.button.del
 {
-  background: url(images/del.png) no-repeat;
+  background: url(images/del.png?v=3c27.247) no-repeat;
   width: 30px;
   height: 20px;
   display: inline-block;
@@ -393,7 +399,7 @@
   display: inline-block;
   width: 16px;
   height: 16px;
-  background: url(images/erase.png) -1px -1px no-repeat #eee;
+  background: url(images/erase.png?v=3052.453) -1px -1px no-repeat #eee;
   cursor: pointer;
 }
 
@@ -414,7 +420,7 @@
 
 /* vacation form */
 #settings-sections .vacation a {
-  background-image: url(images/vacation_icons.png);
+  background-image: url(images/vacation_icons.png?v=e738.767);
 	background-repeat: no-repeat;
 	background-position: 7px 1px;
 }
diff --git a/plugins/managesieve/tests/src/parser_duplicate b/plugins/managesieve/tests/src/parser_duplicate
new file mode 100644
index 0000000..4d10d77
--- /dev/null
+++ b/plugins/managesieve/tests/src/parser_duplicate
@@ -0,0 +1,16 @@
+require ["duplicate","fileinto"];
+# rule:[test-duplicate]
+if duplicate
+{
+	fileinto "urgent";
+}
+# rule:[test-duplicate-2]
+if allof (duplicate :handle "support" :header "X-Ticket-ID", header :contains "subject" "fileserver")
+{
+	fileinto "test";
+}
+# rule:[test-duplicate-3]
+if not duplicate :uniqueid "test" :seconds 1800
+{
+	discard;
+}

--
Gitblit v1.9.1