From c994e0e7cd9f593eb21ff80c7c1ddbeaf2a1b12a Mon Sep 17 00:00:00 2001
From: alecpl <alec@alec.pl>
Date: Fri, 18 Nov 2011 09:44:48 -0500
Subject: [PATCH] - Applied fixes from trunk up to r5451

---
 plugins/managesieve/tests/parser_body.phpt              |   49 ++
 plugins/managesieve/localization/en_US.inc              |   19 
 plugins/managesieve/managesieve.php                     |  347 ++++++++++++-----
 plugins/managesieve/skins/default/managesieve.css       |   38 +
 plugins/managesieve/tests/parser_prefix.phpt            |   25 +
 plugins/managesieve/lib/rcube_sieve_script.php          |  187 +++++++-
 plugins/managesieve/tests/parser_include.phpt           |   30 +
 plugins/managesieve/package.xml                         |  100 +++++
 plugins/managesieve/tests/parser_kep14.phpt             |    2 
 CHANGELOG                                               |    1 
 plugins/managesieve/localization/pl_PL.inc              |   19 
 plugins/managesieve/tests/parser.phpt                   |   90 ++--
 plugins/managesieve/tests/parser_vacation.phpt          |   39 +
 plugins/managesieve/Changelog                           |   12 
 plugins/managesieve/tests/parser_variables.phpt         |   39 +
 plugins/managesieve/tests/parser_relational.phpt        |   25 +
 plugins/newmail_notifier/newmail_notifier.php           |    2 
 /dev/null                                               |    0 
 plugins/managesieve/skins/default/images/up_small.gif   |    0 
 plugins/managesieve/lib/rcube_sieve.php                 |   15 
 plugins/managesieve/tests/parset_subaddress.phpt        |   38 +
 program/include/rcube_imap.php                          |    2 
 plugins/managesieve/tests/parser_imapflags.phpt         |   28 +
 program/js/googiespell.js                               |    2 
 plugins/managesieve/managesieve.js                      |   41 ++
 plugins/managesieve/skins/default/images/down_small.gif |    0 
 program/js/app.js                                       |    2 
 27 files changed, 972 insertions(+), 180 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index c0e4586..328865d 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,7 @@
 CHANGELOG Roundcube Webmail
 ===========================
 
+
 - Fix handling of HTML form elements in messages (#1485137)
 - Fix regression in setting recipient to self when replying to a Sent message (#1487074)
 - Fix listing of folders in hidden namespaces (#1486796)
diff --git a/plugins/managesieve/Changelog b/plugins/managesieve/Changelog
index 1855104..d7ef8e5 100644
--- a/plugins/managesieve/Changelog
+++ b/plugins/managesieve/Changelog
@@ -1,4 +1,16 @@
+- Fixed setting test type to :is when none is specified
+
+* version 5.0-rc1 [2011-11-17]
+-----------------------------------------------------------
 - Fixed sorting of scripts, scripts including aware of the sort order
+- Fixed import of rules with unsupported tests
+- Added 'address' and 'envelope' tests support
+- Added 'body' extension support (RFC5173)
+- Added 'subaddress' extension support (RFC5233)
+- Added comparators support
+- Changed Sender/Recipient labels to From/To
+- Fixed importing rule names from Ingo
+- Fixed handling of extensions disabled in config
 
 * version 5.0-beta [2011-10-17]
 -----------------------------------------------------------
diff --git a/plugins/managesieve/lib/rcube_sieve.php b/plugins/managesieve/lib/rcube_sieve.php
index 7b7ea6e..2ed2e54 100644
--- a/plugins/managesieve/lib/rcube_sieve.php
+++ b/plugins/managesieve/lib/rcube_sieve.php
@@ -44,7 +44,6 @@
 
     public $script;                 // rcube_sieve_script object
     public $current;                // name of currently loaded script
-    private $disabled;              // array of disabled extensions
     private $exts;                  // array of supported extensions
 
 
@@ -89,7 +88,17 @@
         }
 
         $this->exts     = $this->get_extensions();
-        $this->disabled = $disabled;
+
+        // disable features by config
+        if (!empty($disabled)) {
+            // we're working on lower-cased names
+            $disabled = array_map('strtolower', (array) $disabled);
+            foreach ($disabled as $ext) {
+                if (($idx = array_search($ext, $this->exts)) !== false) {
+                    unset($this->exts[$idx]);
+                }
+            }
+        }
     }
 
     public function __destruct() {
@@ -301,7 +310,7 @@
     private function _parse($txt)
     {
         // parse
-        $script = new rcube_sieve_script($txt, $this->disabled, $this->exts);
+        $script = new rcube_sieve_script($txt, $this->exts);
 
         // fix/convert to Roundcube format
         if (!empty($script->content)) {
diff --git a/plugins/managesieve/lib/rcube_sieve_script.php b/plugins/managesieve/lib/rcube_sieve_script.php
index 3c6993d..f5ad62c 100644
--- a/plugins/managesieve/lib/rcube_sieve_script.php
+++ b/plugins/managesieve/lib/rcube_sieve_script.php
@@ -29,9 +29,9 @@
 
     private $vars = array();        // "global" variables
     private $prefix = '';           // script header (comments)
-    private $capabilities = array(); // Sieve extensions supported by server
     private $supported = array(     // Sieve extensions supported by class
-        'fileinto',                 // RFC3028
+        'fileinto',                 // RFC5228
+        'envelope',                 // RFC5228
         'reject',                   // RFC5429
         'ereject',                  // RFC5429
         'copy',                     // RFC3894
@@ -42,29 +42,29 @@
         'imap4flags',               // RFC5232
         'include',                  // draft-ietf-sieve-include-12
         'variables',                // RFC5229
-        // TODO: body, notify
+        'body',                     // RFC5173
+        'subaddress',               // RFC5233
+        // @TODO: enotify/notify, spamtest+virustest, mailbox, date
     );
 
     /**
      * Object constructor
      *
      * @param  string  Script's text content
-     * @param  array   List of disabled extensions
      * @param  array   List of capabilities supported by server
      */
-    public function __construct($script, $disabled=array(), $capabilities=array())
+    public function __construct($script, $capabilities=array())
     {
-        if (!empty($disabled)) {
-            // we're working on lower-cased names
-            $disabled = array_map('strtolower', (array) $disabled);
-            foreach ($disabled as $ext) {
-                if (($idx = array_search($ext, $this->supported)) !== false) {
+        $capabilities = array_map('strtolower', (array) $capabilities);
+
+        // disable features by server capabilities
+        if (!empty($capabilities)) {
+            foreach ($this->supported as $idx => $ext) {
+                if (!in_array($ext, $capabilities)) {
                     unset($this->supported[$idx]);
                 }
             }
         }
-
-        $this->capabilities = array_map('strtolower', (array) $capabilities);
 
         // Parse text content of the script
         $this->_parse_text($script);
@@ -182,7 +182,7 @@
         $idx    = 0;
 
         if (!empty($this->vars)) {
-            if (in_array('variables', (array)$this->capabilities)) {
+            if (in_array('variables', (array)$this->supported)) {
                 $has_vars = true;
                 array_push($exts, 'variables');
             }
@@ -222,33 +222,95 @@
                         $tests[$i] .= ($test['not'] ? 'not ' : '');
                         $tests[$i] .= 'size :' . ($test['type']=='under' ? 'under ' : 'over ') . $test['arg'];
                         break;
+
                     case 'true':
                         $tests[$i] .= ($test['not'] ? 'false' : 'true');
                         break;
+
                     case 'exists':
                         $tests[$i] .= ($test['not'] ? 'not ' : '');
                         $tests[$i] .= 'exists ' . self::escape_string($test['arg']);
                         break;
+
                     case 'header':
                         $tests[$i] .= ($test['not'] ? 'not ' : '');
+                        $tests[$i] .= 'header';
 
-                        // relational operator + comparator
-                        if (preg_match('/^(value|count)-([gteqnl]{2})/', $test['type'], $m)) {
-                            array_push($exts, 'relational');
-                            array_push($exts, 'comparator-i;ascii-numeric');
+                        if (!empty($test['type'])) {
+                            // relational operator + comparator
+                            if (preg_match('/^(value|count)-([gteqnl]{2})/', $test['type'], $m)) {
+                                array_push($exts, 'relational');
+                                array_push($exts, 'comparator-i;ascii-numeric');
 
-                            $tests[$i] .= 'header :' . $m[1] . ' "' . $m[2] . '" :comparator "i;ascii-numeric"';
-                        }
-                        else {
-                            if ($test['type'] == 'regex') {
-                                array_push($exts, 'regex');
+                                $tests[$i] .= ' :' . $m[1] . ' "' . $m[2] . '" :comparator "i;ascii-numeric"';
                             }
+                            else {
+                                $this->add_comparator($test, $tests[$i], $exts);
 
-                            $tests[$i] .= 'header :' . $test['type'];
+                                if ($test['type'] == 'regex') {
+                                    array_push($exts, 'regex');
+                                }
+
+                                $tests[$i] .= ' :' . $test['type'];
+                            }
                         }
 
                         $tests[$i] .= ' ' . self::escape_string($test['arg1']);
                         $tests[$i] .= ' ' . self::escape_string($test['arg2']);
+                        break;
+
+                    case 'address':
+                    case 'envelope':
+                        if ($test['test'] == 'envelope') {
+                            array_push($exts, 'envelope');
+                        }
+
+                        $tests[$i] .= ($test['not'] ? 'not ' : '');
+                        $tests[$i] .= $test['test'];
+
+                        if (!empty($test['part'])) {
+                            $tests[$i] .= ' :' . $test['part'];
+                            if ($test['part'] == 'user' || $test['part'] == 'detail') {
+                                array_push($exts, 'subaddress');
+                            }
+                        }
+
+                        $this->add_comparator($test, $tests[$i], $exts);
+
+                        if (!empty($test['type'])) {
+                            if ($test['type'] == 'regex') {
+                                array_push($exts, 'regex');
+                            }
+                            $tests[$i] .= ' :' . $test['type'];
+                        }
+
+                        $tests[$i] .= ' ' . self::escape_string($test['arg1']);
+                        $tests[$i] .= ' ' . self::escape_string($test['arg2']);
+                        break;
+
+                    case 'body':
+                        array_push($exts, 'body');
+
+                        $tests[$i] .= ($test['not'] ? 'not ' : '') . 'body';
+
+                        $this->add_comparator($test, $tests[$i], $exts);
+
+                        if (!empty($test['part'])) {
+                            $tests[$i] .= ' :' . $test['part'];
+
+                            if (!empty($test['content']) && $test['part'] == 'content') {
+                                $tests[$i] .= ' ' . self::escape_string($test['content']);
+                            }
+                        }
+
+                        if (!empty($test['type'])) {
+                            if ($test['type'] == 'regex') {
+                                array_push($exts, 'regex');
+                            }
+                            $tests[$i] .= ' :' . $test['type'];
+                        }
+
+                        $tests[$i] .= ' ' . self::escape_string($test['arg']);
                         break;
                     }
                     $i++;
@@ -311,7 +373,7 @@
                     case 'addflag':
                     case 'setflag':
                     case 'removeflag':
-                        if (is_array($this->capabilities) && in_array('imap4flags', $this->capabilities))
+                        if (in_array('imap4flags', $this->supported))
                             array_push($exts, 'imap4flags');
                         else
                             array_push($exts, 'imapflags');
@@ -448,7 +510,7 @@
             // handle script header
             if (empty($options['prefix'])) {
                 $options['prefix'] = true;
-                if ($prefix && strpos($prefix, 'Generated by Ingo')) {
+                if ($prefix && strpos($prefix, 'horde.org/ingo')) {
                     $options['format'] = 'INGO';
                 }
             }
@@ -559,7 +621,7 @@
                 $header = array('test' => 'header', 'not' => $not, 'arg1' => '', 'arg2' => '');
                 for ($i=0, $len=count($tokens); $i<$len; $i++) {
                     if (!is_array($tokens[$i]) && preg_match('/^:comparator$/i', $tokens[$i])) {
-                        $i++;
+                        $header['comparator'] = $tokens[++$i];
                     }
                     else if (!is_array($tokens[$i]) && preg_match('/^:(count|value)$/i', $tokens[$i])) {
                         $header['type'] = strtolower(substr($tokens[$i], 1)) . '-' . $tokens[++$i];
@@ -570,6 +632,52 @@
                     else {
                         $header['arg1'] = $header['arg2'];
                         $header['arg2'] = $tokens[$i];
+                    }
+                }
+
+                $tests[] = $header;
+                break;
+
+            case 'address':
+            case 'envelope':
+                $header = array('test' => $token, 'not' => $not, 'arg1' => '', 'arg2' => '');
+                for ($i=0, $len=count($tokens); $i<$len; $i++) {
+                    if (!is_array($tokens[$i]) && preg_match('/^:comparator$/i', $tokens[$i])) {
+                        $header['comparator'] = $tokens[++$i];
+                    }
+                    else if (!is_array($tokens[$i]) && preg_match('/^:(is|contains|matches|regex)$/i', $tokens[$i])) {
+                        $header['type'] = strtolower(substr($tokens[$i], 1));
+                    }
+                    else if (!is_array($tokens[$i]) && preg_match('/^:(localpart|domain|all|user|detail)$/i', $tokens[$i])) {
+                        $header['part'] = strtolower(substr($tokens[$i], 1));
+                    }
+                    else {
+                        $header['arg1'] = $header['arg2'];
+                        $header['arg2'] = $tokens[$i];
+                    }
+                }
+
+                $tests[] = $header;
+                break;
+
+            case 'body':
+                $header = array('test' => 'body', 'not' => $not, 'arg' => '');
+                for ($i=0, $len=count($tokens); $i<$len; $i++) {
+                    if (!is_array($tokens[$i]) && preg_match('/^:comparator$/i', $tokens[$i])) {
+                        $header['comparator'] = $tokens[++$i];
+                    }
+                    else if (!is_array($tokens[$i]) && preg_match('/^:(is|contains|matches|regex)$/i', $tokens[$i])) {
+                        $header['type'] = strtolower(substr($tokens[$i], 1));
+                    }
+                    else if (!is_array($tokens[$i]) && preg_match('/^:(raw|content|text)$/i', $tokens[$i])) {
+                        $header['part'] = strtolower(substr($tokens[$i], 1));
+
+                        if ($header['part'] == 'content') {
+                            $header['content'] = $tokens[++$i];
+                        }
+                    }
+                    else {
+                        $header['arg'] = $tokens[$i];
                     }
                 }
 
@@ -597,9 +705,7 @@
         }
 
         // ...and actions block
-        if ($tests) {
-            $actions = $this->_parse_actions($content);
-        }
+        $actions = $this->_parse_actions($content);
 
         if ($tests && $actions) {
             $result = array(
@@ -747,6 +853,29 @@
     }
 
     /**
+     *
+     */
+    private function add_comparator($test, &$out, &$exts)
+    {
+        if (empty($test['comparator'])) {
+            return;
+        }
+
+        if ($test['comparator'] == 'i;ascii-numeric') {
+            array_push($exts, 'relational');
+            array_push($exts, 'comparator-i;ascii-numeric');
+        }
+        else if (!in_array($test['comparator'], array('i;octet', 'i;ascii-casemap'))) {
+            array_push($exts, 'comparator-' . $test['comparator']);
+        }
+
+        // skip default comparator
+        if ($test['comparator'] != 'i;ascii-casemap') {
+            $out .= ' :comparator ' . self::escape_string($test['comparator']);
+        }
+    }
+
+    /**
      * Escape special chars into quoted string value or multi-line string
      * or list of strings
      *
diff --git a/plugins/managesieve/localization/en_US.inc b/plugins/managesieve/localization/en_US.inc
index 24cd72a..94e0ba6 100644
--- a/plugins/managesieve/localization/en_US.inc
+++ b/plugins/managesieve/localization/en_US.inc
@@ -82,6 +82,25 @@
 $labels['usedata'] = 'Use following data in the filter:';
 $labels['nextstep'] = 'Next Step';
 $labels['...'] = '...';
+$labels['advancedopts'] = 'Advanced options';
+$labels['body'] = 'Body';
+$labels['address'] = 'address';
+$labels['envelope'] = 'envelope';
+$labels['modifier'] = 'modifier:';
+$labels['text'] = 'text';
+$labels['undecoded'] = 'undecoded (raw)';
+$labels['contenttype'] = 'content type';
+$labels['modtype'] = 'type:';
+$labels['allparts'] = 'all';
+$labels['domain'] = 'domain';
+$labels['localpart'] = 'local part';
+$labels['user'] = 'user';
+$labels['detail'] = 'detail';
+$labels['comparator'] = 'comparator:';
+$labels['default'] = 'default';
+$labels['octet'] = 'strict (octet)';
+$labels['asciicasemap'] = 'case insensitive (ascii-casemap)';
+$labels['asciinumeric'] = 'numeric (ascii-numeric)';
 
 $messages = array();
 $messages['filterunknownerror'] = 'Unknown server error.';
diff --git a/plugins/managesieve/localization/pl_PL.inc b/plugins/managesieve/localization/pl_PL.inc
index eaa09e1..c0ec2ed 100644
--- a/plugins/managesieve/localization/pl_PL.inc
+++ b/plugins/managesieve/localization/pl_PL.inc
@@ -81,6 +81,25 @@
 $labels['usedata'] = 'Użyj następujących danych do utworzenia filtra:';
 $labels['nextstep'] = 'Następny krok';
 $labels['...'] = '...';
+$labels['advancedopts'] = 'Zaawansowane opcje';
+$labels['body'] = 'Treść';
+$labels['address'] = 'adres';
+$labels['envelope'] = 'koperta (envelope)';
+$labels['modifier'] = 'modyfikator:';
+$labels['text'] = 'tekst';
+$labels['undecoded'] = 'nie (raw)';
+$labels['contenttype'] = 'typ części (content type)';
+$labels['modtype'] = 'typ:';
+$labels['allparts'] = 'wszystkie';
+$labels['domain'] = 'domena';
+$labels['localpart'] = 'część lokalna';
+$labels['user'] = 'użytkownik';
+$labels['detail'] = 'detal';
+$labels['comparator'] = 'komparator:';
+$labels['default'] = 'domyślny';
+$labels['octet'] = 'dokładny (octet)';
+$labels['asciicasemap'] = 'nierozróżniający wielkości liter (ascii-casemap)';
+$labels['asciinumeric'] = 'numeryczny (ascii-numeric)';
 
 $messages = array();
 $messages['filterunknownerror'] = 'Nieznany błąd serwera.';
diff --git a/plugins/managesieve/managesieve.js b/plugins/managesieve/managesieve.js
index 5f9c670..a7b15f7 100644
--- a/plugins/managesieve/managesieve.js
+++ b/plugins/managesieve/managesieve.js
@@ -542,19 +542,28 @@
     size = document.getElementById('rule_size' + id),
     op = document.getElementById('rule_op' + id),
     target = document.getElementById('rule_target' + id),
-    header = document.getElementById('custom_header' + id);
+    header = document.getElementById('custom_header' + id),
+    mod = document.getElementById('rule_mod' + id),
+    trans = document.getElementById('rule_trans' + id),
+    comp = document.getElementById('rule_comp' + id);
 
   if (obj.value == 'size') {
     size.style.display = 'inline';
     op.style.display = 'none';
     target.style.display = 'none';
     header.style.display = 'none';
+    mod.style.display = 'none';
+    trans.style.display = 'none';
+    comp.style.display = 'none';
   }
   else {
     header.style.display = obj.value != '...' ? 'none' : 'inline';
     size.style.display = 'none';
     op.style.display = 'inline';
+    comp.style.display = '';
     rule_op_select(id);
+    mod.style.display = obj.value == 'body' ? 'none' : 'block';
+    trans.style.display = obj.value == 'body' ? 'block' : 'none';
   }
 
   obj.style.width = obj.value == '...' ? '40px' : '';
@@ -568,11 +577,41 @@
   target.style.display = obj.value == 'exists' || obj.value == 'notexists' ? 'none' : 'inline';
 };
 
+function rule_trans_select(id)
+{
+  var obj = document.getElementById('rule_trans_op' + id),
+    target = document.getElementById('rule_trans_type' + id);
+
+  target.style.display = obj.value != 'content' ? 'none' : 'inline';
+};
+
+function rule_mod_select(id)
+{
+  var obj = document.getElementById('rule_mod_op' + id),
+    target = document.getElementById('rule_mod_type' + id);
+
+  target.style.display = obj.value != 'address' && obj.value != 'envelope' ? 'none' : 'inline';
+};
+
 function rule_join_radio(value)
 {
   $('#rules').css('display', value == 'any' ? 'none' : 'block');
 };
 
+function rule_adv_switch(id, elem)
+{
+  var elem = $(elem), enabled = elem.hasClass('hide'), adv = $('#rule_advanced'+id);
+
+  if (enabled) {
+    adv.hide();
+    elem.removeClass('hide').addClass('show');
+  }
+  else {
+    adv.show();
+    elem.removeClass('show').addClass('hide');
+  }
+}
+
 function action_type_select(id)
 {
   var obj = document.getElementById('action_type' + id),
diff --git a/plugins/managesieve/managesieve.php b/plugins/managesieve/managesieve.php
index 0787c76..5ac406a 100644
--- a/plugins/managesieve/managesieve.php
+++ b/plugins/managesieve/managesieve.php
@@ -45,9 +45,23 @@
     private $list;
     private $active = array();
     private $headers = array(
-        'subject'   => 'Subject',
-        'sender'    => 'From',
-        'recipient' => 'To',
+        'subject' => 'Subject',
+        'from'    => 'From',
+        'to'      => 'To',
+    );
+    private $addr_headers = array(
+        // Required
+        "from", "to", "cc", "bcc", "sender", "resent-from", "resent-to",
+        // Additional (RFC 822 / RFC 2822)
+        "reply-to", "resent-reply-to", "resent-sender", "resent-cc", "resent-bcc",
+        // Non-standard (RFC 2076, draft-palme-mailext-headers-08.txt)
+        "for-approval", "for-handling", "for-comment", "apparently-to", "errors-to",
+        "delivered-to", "return-receipt-to", "x-admin", "read-receipt-to",
+        "x-confirm-reading-to", "return-receipt-requested",
+        "registered-mail-reply-requested-by", "mail-followup-to", "mail-reply-to",
+        "abuse-reports-to", "x-complaints-to", "x-report-abuse-to",
+        // Undocumented
+        "x-beenthere",
     );
 
     const VERSION = '5.0';
@@ -588,6 +602,11 @@
             $sizeitems      = get_input_value('_rule_size_item', RCUBE_INPUT_POST);
             $sizetargets    = get_input_value('_rule_size_target', RCUBE_INPUT_POST);
             $targets        = get_input_value('_rule_target', RCUBE_INPUT_POST, true);
+            $mods           = get_input_value('_rule_mod', RCUBE_INPUT_POST);
+            $mod_types      = get_input_value('_rule_mod_type', RCUBE_INPUT_POST);
+            $body_trans     = get_input_value('_rule_trans', RCUBE_INPUT_POST);
+            $body_types     = get_input_value('_rule_trans_type', RCUBE_INPUT_POST, true);
+            $comparators    = get_input_value('_rule_comp', RCUBE_INPUT_POST);
             $act_types      = get_input_value('_action_type', RCUBE_INPUT_POST, true);
             $mailboxes      = get_input_value('_action_mailbox', RCUBE_INPUT_POST, true);
             $act_targets    = get_input_value('_action_target', RCUBE_INPUT_POST, true);
@@ -625,23 +644,101 @@
             }
             else {
                 foreach ($headers as $idx => $header) {
-                    $header = $this->strip_value($header);
-                    $target = $this->strip_value($targets[$idx], true);
-                    $op     = $this->strip_value($ops[$idx]);
+                    $header     = $this->strip_value($header);
+                    $target     = $this->strip_value($targets[$idx], true);
+                    $operator   = $this->strip_value($ops[$idx]);
+                    $comparator = $this->strip_value($comparators[$idx]);
 
-                    // normal header
-                    if (in_array($header, $this->headers)) {
-                        if (preg_match('/^not/', $op))
+                    if ($header == '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;
+
+                        if ($sizetarget == '')
+                            $this->errors['tests'][$i]['sizetarget'] = $this->gettext('cannotbeempty');
+                        else if (!preg_match('/^[0-9]+(K|M|G)?$/i', $sizetarget.$sizeitem, $m)) {
+                            $this->errors['tests'][$i]['sizetarget'] = $this->gettext('forbiddenchars');
+                            $this->form['tests'][$i]['item'] = $sizeitem;
+                        }
+                        else
+                            $this->form['tests'][$i]['arg'] .= $m[1];
+                    }
+                    else if ($header == 'body') {
+                        $trans      = $this->strip_value($body_trans[$idx]);
+                        $trans_type = $this->strip_value($body_types[$idx], true);
+
+                        if (preg_match('/^not/', $operator))
                             $this->form['tests'][$i]['not'] = true;
-                        $type = preg_replace('/^not/', '', $op);
+                        $type = preg_replace('/^not/', '', $operator);
+
+                        if ($type == 'exists') {
+                            $this->errors['tests'][$i]['op'] = true;
+                        }
+
+                        $this->form['tests'][$i]['test'] = 'body';
+                        $this->form['tests'][$i]['type'] = $type;
+                        $this->form['tests'][$i]['arg']  = $target;
+
+                        if ($target == '' && $type != 'exists')
+                            $this->errors['tests'][$i]['target'] = $this->gettext('cannotbeempty');
+                        else if (preg_match('/^(value|count)-/', $type) && !preg_match('/[0-9]+/', $target))
+                            $this->errors['tests'][$i]['target'] = $this->gettext('forbiddenchars');
+
+                        $this->form['tests'][$i]['part'] = $trans;
+                        if ($trans == 'content') {
+                            $this->form['tests'][$i]['content'] = $trans_type;
+                        }
+                    }
+                    else {
+                        $cust_header = $headers = $this->strip_value($cust_headers[$idx]);
+                        $mod      = $this->strip_value($mods[$idx]);
+                        $mod_type = $this->strip_value($mod_types[$idx]);
+
+                        if (preg_match('/^not/', $operator))
+                            $this->form['tests'][$i]['not'] = true;
+                        $type = preg_replace('/^not/', '', $operator);
+
+                        if ($header == '...') {
+                            $headers = preg_split('/[\s,]+/', $cust_header, -1, PREG_SPLIT_NO_EMPTY);
+
+                            if (!count($headers))
+                                $this->errors['tests'][$i]['header'] = $this->gettext('cannotbeempty');
+                            else {
+                                foreach ($headers as $hr)
+                                    if (!preg_match('/^[a-z0-9-]+$/i', $hr))
+                                        $this->errors['tests'][$i]['header'] = $this->gettext('forbiddenchars');
+                            }
+
+                            if (empty($this->errors['tests'][$i]['header']))
+                                $cust_header = (is_array($headers) && count($headers) == 1) ? $headers[0] : $headers;
+                        }
 
                         if ($type == 'exists') {
                             $this->form['tests'][$i]['test'] = 'exists';
-                            $this->form['tests'][$i]['arg'] = $header;
+                            $this->form['tests'][$i]['arg'] = $header == '...' ? $cust_header : $header;
                         }
                         else {
+                            $test   = 'header';
+                            $header = $header == '...' ? $cust_header : $header;
+
+                            if ($mod == 'address' || $mod == 'envelope') {
+                                $found = false;
+                                if (empty($this->errors['tests'][$i]['header'])) {
+                                    foreach ((array)$header as $hdr) {
+                                        if (!in_array(strtolower(trim($hdr)), $this->addr_headers))
+                                            $found = true;
+                                    }
+                                }
+                                if (!$found)
+                                    $test = $mod;
+                            }
+
                             $this->form['tests'][$i]['type'] = $type;
-                            $this->form['tests'][$i]['test'] = 'header';
+                            $this->form['tests'][$i]['test'] = $test;
                             $this->form['tests'][$i]['arg1'] = $header;
                             $this->form['tests'][$i]['arg2'] = $target;
 
@@ -649,65 +746,20 @@
                                 $this->errors['tests'][$i]['target'] = $this->gettext('cannotbeempty');
                             else if (preg_match('/^(value|count)-/', $type) && !preg_match('/[0-9]+/', $target))
                                 $this->errors['tests'][$i]['target'] = $this->gettext('forbiddenchars');
+
+                            if ($mod) {
+                                $this->form['tests'][$i]['part'] = $mod_type;
+                            }
                         }
                     }
-                    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 ($header != 'size' && $comparator) {
+                        if (preg_match('/^(value|count)/', $this->form['tests'][$i]['type']))
+                            $comparator = 'i;ascii-numeric';
 
-                            if ($sizetarget == '')
-                                $this->errors['tests'][$i]['sizetarget'] = $this->gettext('cannotbeempty');
-                            else if (!preg_match('/^[0-9]+(K|M|G)*$/i', $sizetarget))
-                                $this->errors['tests'][$i]['sizetarget'] = $this->gettext('forbiddenchars');
-                            break;
-                        case '...':
-                            $cust_header = $headers = $this->strip_value($cust_headers[$idx]);
+                        $this->form['tests'][$i]['comparator'] = $comparator;
+                    }
 
-                            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');
-                            else {
-                                $headers = preg_split('/[\s,]+/', $cust_header, -1, PREG_SPLIT_NO_EMPTY);
-
-                                if (!count($headers))
-                                    $this->errors['tests'][$i]['header'] = $this->gettext('cannotbeempty');
-                                else {
-                                    foreach ($headers as $hr)
-                                        if (!preg_match('/^[a-z0-9-]+$/i', $hr))
-                                            $this->errors['tests'][$i]['header'] = $this->gettext('forbiddenchars');
-                                }
-                            }
-
-                            if (empty($this->errors['tests'][$i]['header']))
-                                $cust_header = (is_array($headers) && count($headers) == 1) ? $headers[0] : $headers;
-
-                            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');
-                                else if (preg_match('/^(value|count)-/', $type) && !preg_match('/[0-9]+/', $target))
-                                    $this->errors['tests'][$i]['target'] = $this->gettext('forbiddenchars');
-                            }
-                            break;
-                        }
                     $i++;
                 }
             }
@@ -1140,43 +1192,52 @@
         $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 ? '<div class="rulerow" id="rulerow' .$id .'">'."\n" : '';
-
-        $out .= '<table><tr><td class="rowactions">';
-
         // headers select
         $select_header = new html_select(array('name' => "_header[]", 'id' => 'header'.$id,
             'onchange' => 'rule_header_select(' .$id .')'));
         foreach($this->headers as $name => $val)
             $select_header->add(Q($this->gettext($name)), Q($val));
+        if (in_array('body', $this->exts))
+            $select_header->add(Q($this->gettext('body')), 'body');
         $select_header->add(Q($this->gettext('size')), 'size');
         $select_header->add(Q($this->gettext('...')), '...');
 
         // TODO: list arguments
+        $aout = '';
 
-        if ((isset($rule['test']) && $rule['test'] == 'header')
-            && !is_array($rule['arg1']) && in_array($rule['arg1'], $this->headers))
-            $out .= $select_header->show($rule['arg1']);
+        if ((isset($rule['test']) && in_array($rule['test'], array('header', 'address', 'envelope')))
+            && !is_array($rule['arg1']) && in_array($rule['arg1'], $this->headers)
+        ) {
+            $aout .= $select_header->show($rule['arg1']);
+        }
         else if ((isset($rule['test']) && $rule['test'] == 'exists')
-            && !is_array($rule['arg']) && in_array($rule['arg'], $this->headers))
-            $out .= $select_header->show($rule['arg']);
+            && !is_array($rule['arg']) && in_array($rule['arg'], $this->headers)
+        ) {
+            $aout .= $select_header->show($rule['arg']);
+        }
         else if (isset($rule['test']) && $rule['test'] == 'size')
-            $out .= $select_header->show('size');
+            $aout .= $select_header->show('size');
+        else if (isset($rule['test']) && $rule['test'] == 'body')
+            $aout .= $select_header->show('body');
         else if (isset($rule['test']) && $rule['test'] != 'true')
-            $out .= $select_header->show('...');
+            $aout .= $select_header->show('...');
         else
-            $out .= $select_header->show();
+            $aout .= $select_header->show();
 
-        $out .= '</td><td class="rowtargets">';
+        if (isset($rule['test']) && in_array($rule['test'], array('header', 'address', 'envelope'))) {
+            if (is_array($rule['arg1']))
+                $custom = implode(', ', $rule['arg1']);
+            else if (!in_array($rule['arg1'], $this->headers))
+                $custom = $rule['arg1'];
+        }
+        else if (isset($rule['test']) && $rule['test'] == 'exists') {
+            if (is_array($rule['arg']))
+                $custom = implode(', ', $rule['arg']);
+            else if (!in_array($rule['arg'], $this->headers))
+                $custom = $rule['arg'];
+        }
 
-        if ((isset($rule['test']) && $rule['test'] == 'header')
-            && (is_array($rule['arg1']) || !in_array($rule['arg1'], $this->headers)))
-            $custom = is_array($rule['arg1']) ? implode(', ', $rule['arg1']) : $rule['arg1'];
-        else if ((isset($rule['test']) && $rule['test'] == 'exists')
-            && (is_array($rule['arg']) || !in_array($rule['arg'], $this->headers)))
-            $custom = is_array($rule['arg']) ? implode(', ', $rule['arg']) : $rule['arg'];
-
-        $out .= '<div id="custom_header' .$id. '" style="display:' .(isset($custom) ? 'inline' : 'none'). '">
+        $tout = '<div id="custom_header' .$id. '" style="display:' .(isset($custom) ? 'inline' : 'none'). '">
             <input type="text" name="_custom_header[]" id="custom_header_i'.$id.'" '
             . $this->error_class($id, 'test', 'header', 'custom_header_i')
             .' value="' .Q($custom). '" size="15" />&nbsp;</div>' . "\n";
@@ -1215,33 +1276,43 @@
 
         // target input (TODO: lists)
 
-        if ($rule['test'] == 'header') {
-            $out .= $select_op->show(($rule['not'] ? 'not' : '').$rule['type']);
+        if (in_array($rule['test'], array('header', 'address', 'envelope'))) {
+            $test   = ($rule['not'] ? 'not' : '').($rule['type'] ? $rule['type'] : 'is');
             $target = $rule['arg2'];
         }
+        else if ($rule['test'] == 'body') {
+            $test   = ($rule['not'] ? 'not' : '').($rule['type'] ? $rule['type'] : 'is');
+            $target = $rule['arg'];
+        }
         else if ($rule['test'] == 'size') {
-            $out .= $select_op->show();
-            if (preg_match('/^([0-9]+)(K|M|G)*$/', $rule['arg'], $matches)) {
+            $test   = '';
+            $target = '';
+            if (preg_match('/^([0-9]+)(K|M|G)?$/', $rule['arg'], $matches)) {
                 $sizetarget = $matches[1];
                 $sizeitem = $matches[2];
             }
+            else {
+                $sizetarget = $rule['arg'];
+                $sizeitem = $rule['item'];
+            }
         }
         else {
-            $out .= $select_op->show(($rule['not'] ? 'not' : '').$rule['test']);
-            $target = '';
+            $test   = ($rule['not'] ? 'not' : '').$rule['test'];
+            $target =  '';
         }
 
-        $out .= '<input type="text" name="_rule_target[]" id="rule_target' .$id. '"
+        $tout .= $select_op->show($test);
+        $tout .= '<input type="text" name="_rule_target[]" id="rule_target' .$id. '"
             value="' .Q($target). '" size="20" ' . $this->error_class($id, 'test', 'target', 'rule_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');
+        $select_size_op->add(Q($this->gettext('filterunder')), 'under');
 
-        $out .= '<div id="rule_size' .$id. '" style="display:' . ($rule['test']=='size' ? 'inline' : 'none') .'">';
-        $out .= $select_size_op->show($rule['test']=='size' ? $rule['type'] : '');
-        $out .= '<input type="text" name="_rule_size_target[]" id="rule_size_i'.$id.'" value="'.$sizetarget.'" size="10" ' 
+        $tout .= '<div id="rule_size' .$id. '" style="display:' . ($rule['test']=='size' ? 'inline' : 'none') .'">';
+        $tout .= $select_size_op->show($rule['test']=='size' ? $rule['type'] : '');
+        $tout .= '<input type="text" name="_rule_size_target[]" id="rule_size_i'.$id.'" value="'.$sizetarget.'" size="10" ' 
             . $this->error_class($id, 'test', 'sizetarget', 'rule_size_i') .' />
             <input type="radio" name="_rule_size_item['.$id.']" value=""'
                 . (!$sizeitem ? ' checked="checked"' : '') .' class="radio" />'.rcube_label('B').'
@@ -1251,7 +1322,82 @@
                 . ($sizeitem=='M' ? ' checked="checked"' : '') .' class="radio" />'.rcube_label('MB').'
             <input type="radio" name="_rule_size_item['.$id.']" value="G"'
                 . ($sizeitem=='G' ? ' checked="checked"' : '') .' class="radio" />'.rcube_label('GB');
-        $out .= '</div>';
+        $tout .= '</div>';
+
+        // Advanced modifiers (address, envelope)
+        $select_mod = new html_select(array('name' => "_rule_mod[]", 'id' => 'rule_mod_op'.$id,
+            'onchange' => 'rule_mod_select(' .$id .')'));
+        $select_mod->add(Q($this->gettext('none')), '');
+        $select_mod->add(Q($this->gettext('address')), 'address');
+        if (in_array('envelope', $this->exts))
+            $select_mod->add(Q($this->gettext('envelope')), 'envelope');
+
+        $select_type = new html_select(array('name' => "_rule_mod_type[]", 'id' => 'rule_mod_type'.$id));
+        $select_type->add(Q($this->gettext('allparts')), 'all');
+        $select_type->add(Q($this->gettext('domain')), 'domain');
+        $select_type->add(Q($this->gettext('localpart')), 'localpart');
+        if (in_array('subaddress', $this->exts)) {
+            $select_type->add(Q($this->gettext('user')), 'user');
+            $select_type->add(Q($this->gettext('detail')), 'detail');
+        }
+
+        $need_mod = $rule['test'] != 'size' && $rule['test'] != 'body';
+        $mout = '<div id="rule_mod' .$id. '" class="adv" style="display:' . ($need_mod ? 'block' : 'none') .'">';
+        $mout .= ' <span>';
+        $mout .= Q($this->gettext('modifier')) . ' ';
+        $mout .= $select_mod->show($rule['test']);
+        $mout .= '</span>';
+        $mout .= ' <span id="rule_mod_type' . $id . '"';
+        $mout .= ' style="display:' . (in_array($rule['test'], array('address', 'envelope')) ? 'inline' : 'none') .'">';
+        $mout .= Q($this->gettext('modtype')) . ' ';
+        $mout .= $select_type->show($rule['part']);
+        $mout .= '</span>';
+        $mout .= '</div>';
+
+        // Advanced modifiers (body transformations)
+        $select_mod = new html_select(array('name' => "_rule_trans[]", 'id' => 'rule_trans_op'.$id,
+            'onchange' => 'rule_trans_select(' .$id .')'));
+        $select_mod->add(Q($this->gettext('text')), 'text');
+        $select_mod->add(Q($this->gettext('undecoded')), 'raw');
+        $select_mod->add(Q($this->gettext('contenttype')), 'content');
+
+        $mout .= '<div id="rule_trans' .$id. '" class="adv" style="display:' . ($rule['test'] == 'body' ? 'block' : 'none') .'">';
+        $mout .= ' <span>';
+        $mout .= Q($this->gettext('modifier')) . ' ';
+        $mout .= $select_mod->show($rule['part']);
+        $mout .= '<input type="text" name="_rule_trans_type[]" id="rule_trans_type'.$id
+            . '" value="'.(is_array($rule['content']) ? implode(',', $rule['content']) : $rule['content'])
+            .'" size="20" style="display:' . ($rule['part'] == 'content' ? 'inline' : 'none') .'"'
+            . $this->error_class($id, 'test', 'part', 'rule_trans_type') .' />';
+        $mout .= '</span>';
+        $mout .= '</div>';
+
+        // Advanced modifiers (body transformations)
+        $select_comp = new html_select(array('name' => "_rule_comp[]", 'id' => 'rule_comp_op'.$id));
+        $select_comp->add(Q($this->gettext('default')), '');
+        $select_comp->add(Q($this->gettext('octet')), 'i;octet');
+        $select_comp->add(Q($this->gettext('asciicasemap')), 'i;ascii-casemap');
+        if (in_array('comparator-i;ascii-numeric', $this->exts)) {
+            $select_comp->add(Q($this->gettext('asciinumeric')), 'i;ascii-numeric');
+        }
+
+        $mout .= '<div id="rule_comp' .$id. '" class="adv" style="display:' . ($rule['test'] != 'size' ? 'block' : 'none') .'">';
+        $mout .= ' <span>';
+        $mout .= Q($this->gettext('comparator')) . ' ';
+        $mout .= $select_comp->show($rule['comparator']);
+        $mout .= '</span>';
+        $mout .= '</div>';
+
+        // Build output table
+        $out = $div ? '<div class="rulerow" id="rulerow' .$id .'">'."\n" : '';
+        $out .= '<table><tr>';
+        $out .= '<td class="advbutton">';
+        $out .= '<a href="#" id="ruleadv' . $id .'" title="'. Q($this->gettext('advancedopts')). '"
+            onclick="rule_adv_switch(' . $id .', this)" class="show">&nbsp;&nbsp;</a>';
+        $out .= '</td>';
+        $out .= '<td class="rowactions">' . $aout . '</td>';
+        $out .= '<td class="rowtargets">' . $tout . "\n";
+        $out .= '<div id="rule_advanced' .$id. '" style="display:none">' . $mout . '</div>';
         $out .= '</td>';
 
         // add/del buttons
@@ -1260,7 +1406,8 @@
             onclick="rcmail.managesieve_ruleadd(' . $id .')" class="button add"></a>';
         $out .= '<a href="#" id="ruledel' . $id .'" title="'. Q($this->gettext('del')). '"
             onclick="rcmail.managesieve_ruledel(' . $id .')" class="button del' . ($rows_num<2 ? ' disabled' : '') .'"></a>';
-        $out .= '</td></tr></table>';
+        $out .= '</td>';
+        $out .= '</tr></table>';
 
         $out .= $div ? "</div>\n" : '';
 
diff --git a/plugins/managesieve/package.xml b/plugins/managesieve/package.xml
new file mode 100644
index 0000000..56655d2
--- /dev/null
+++ b/plugins/managesieve/package.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" packagerversion="1.9.0" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+    http://pear.php.net/dtd/tasks-1.0.xsd
+    http://pear.php.net/dtd/package-2.0
+    http://pear.php.net/dtd/package-2.0.xsd">
+	<name>managesieve</name>
+	<channel>pear.roundcube.net</channel>
+	<summary>Sieve filters manager for Roundcube</summary>
+	<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.
+	</description>
+	<lead>
+		<name>Aleksander Machniak</name>
+		<user>alec</user>
+		<email>alec@alec.pl</email>
+		<active>yes</active>
+	</lead>
+	<date>2011-11-17</date>
+	<version>
+		<release>5.0</release>
+		<api>5.0</api>
+	</version>
+	<stability>
+		<release>stable</release>
+		<api>stable</api>
+	</stability>
+	<license uri="http://www.gnu.org/licenses/gpl-2.0.html">GNU GPLv2</license>
+    <notes>-</notes>
+	<contents>
+		<dir baseinstalldir="/" name="/">
+			<file name="managesieve.php" role="php">
+				<tasks:replace from="@name@" to="name" type="package-info"/>
+				<tasks:replace from="@package_version@" to="version" type="package-info"/>
+			</file>
+			<file name="managesieve.js" role="data">
+				<tasks:replace from="@name@" to="name" type="package-info"/>
+				<tasks:replace from="@package_version@" to="version" type="package-info"/>
+			</file>
+			<file name="localization/bg_BG.inc" role="data"></file>
+			<file name="localization/cs_CZ.inc" role="data"></file>
+			<file name="localization/de_CH.inc" role="data"></file>
+			<file name="localization/de_DE.inc" role="data"></file>
+			<file name="localization/el_GR.inc" role="data"></file>
+			<file name="localization/en_GB.inc" role="data"></file>
+			<file name="localization/en_US.inc" role="data"></file>
+			<file name="localization/es_AR.inc" role="data"></file>
+			<file name="localization/es_ES.inc" role="data"></file>
+			<file name="localization/et_EE.inc" role="data"></file>
+			<file name="localization/fi_FI.inc" role="data"></file>
+			<file name="localization/fr_FR.inc" role="data"></file>
+			<file name="localization/gl_ES.inc" role="data"></file>
+			<file name="localization/hr_HR.inc" role="data"></file>
+			<file name="localization/hu_HU.inc" role="data"></file>
+			<file name="localization/it_IT.inc" role="data"></file>
+			<file name="localization/ja_JP.inc" role="data"></file>
+			<file name="localization/lv_LV.inc" role="data"></file>
+			<file name="localization/nb_NO.inc" role="data"></file>
+			<file name="localization/nl_NL.inc" role="data"></file>
+			<file name="localization/pl_PL.inc" role="data"></file>
+			<file name="localization/pt_BR.inc" role="data"></file>
+			<file name="localization/pt_PT.inc" role="data"></file>
+			<file name="localization/ru_RU.inc" role="data"></file>
+			<file name="localization/sk_SK.inc" role="data"></file>
+			<file name="localization/sl_SI.inc" role="data"></file>
+			<file name="localization/sv_SE.inc" role="data"></file>
+			<file name="localization/uk_UA.inc" role="data"></file>
+			<file name="localization/zh_CN.inc" role="data"></file>
+			<file name="localization/zh_TW.inc" role="data"></file>
+			<file name="skins/default/managesieve.css" role="data"></file>
+			<file name="skins/default/managesieve_mail.css" role="data"></file>
+			<file name="skins/default/templates/filteredit.html" role="data"></file>
+			<file name="skins/default/templates/managesieve.html" role="data"></file>
+			<file name="skins/default/templates/setedit.html" role="data"></file>
+			<file name="skins/default/images/add.png" role="data"></file>
+			<file name="skins/default/images/del.png" role="data"></file>
+			<file name="skins/default/images/down_small.gif" role="data"></file>
+			<file name="skins/default/images/filter.png" role="data"></file>
+			<file name="skins/default/images/up_small.gif" role="data"></file>
+			<file name="managesieve.php" role="php"></file>
+			<file name="lib/rcube_sieve.php" role="php"></file>
+			<file name="lib/rcube_sieve_script.php" role="php"></file>
+			<file name="lib/Net/Sieve.php" role="php"></file>
+			<file name="config.inc.php.dist" role="data"></file>
+		</dir>
+		<!-- / -->
+	</contents>
+	<dependencies>
+		<required>
+			<php>
+				<min>5.2.1</min>
+			</php>
+			<pearinstaller>
+				<min>1.7.0</min>
+			</pearinstaller>
+		</required>
+	</dependencies>
+	<phprelease/>
+</package>
diff --git a/plugins/managesieve/skins/default/images/down_small.gif b/plugins/managesieve/skins/default/images/down_small.gif
new file mode 100644
index 0000000..f865893
--- /dev/null
+++ b/plugins/managesieve/skins/default/images/down_small.gif
Binary files differ
diff --git a/plugins/managesieve/skins/default/images/toolbar.png b/plugins/managesieve/skins/default/images/toolbar.png
deleted file mode 100644
index 473dbc8..0000000
--- a/plugins/managesieve/skins/default/images/toolbar.png
+++ /dev/null
Binary files differ
diff --git a/plugins/managesieve/skins/default/images/up_small.gif b/plugins/managesieve/skins/default/images/up_small.gif
new file mode 100644
index 0000000..40deb89
--- /dev/null
+++ b/plugins/managesieve/skins/default/images/up_small.gif
Binary files differ
diff --git a/plugins/managesieve/skins/default/managesieve.css b/plugins/managesieve/skins/default/managesieve.css
index cef26d4..0b82be6 100644
--- a/plugins/managesieve/skins/default/managesieve.css
+++ b/plugins/managesieve/skins/default/managesieve.css
@@ -149,6 +149,35 @@
   min-width: 620px;
 }
 
+td
+{
+  vertical-align: top;
+}
+
+td.advbutton
+{
+  width: 1%;
+}
+
+td.advbutton a
+{
+  display: block;
+  padding-top: 14px;
+  height: 6px;
+  width: 12px;
+  text-decoration: none;
+}
+
+td.advbutton a.show
+{
+  background: url(images/down_small.gif) center no-repeat;
+}
+
+td.advbutton a.hide
+{
+  background: url(images/up_small.gif) center no-repeat;
+}
+
 td.rowbuttons
 {
   text-align: right;
@@ -169,6 +198,11 @@
   padding-left: 3px;
 }
 
+td.rowtargets div.adv
+{
+  padding-top: 3px;
+}
+
 input.disabled, input.disabled:hover
 {
   color: #999999;
@@ -183,6 +217,7 @@
 input.radio
 {
   border: 0;
+  margin-top: 0;
 }
 
 select.operator_selector
@@ -190,6 +225,7 @@
   width: 200px;
 }
 
+td.rowtargets span,
 span.label
 {
   color: #666666;
@@ -243,7 +279,7 @@
   background: url(images/add.png) no-repeat;
   width: 30px;
   height: 20px;
-  margin-right: 4px; 
+  margin-right: 4px;
   display: inline-block;
 }
 
diff --git a/plugins/managesieve/tests/parser.phpt b/plugins/managesieve/tests/parser.phpt
index d703534..aec0421 100644
--- a/plugins/managesieve/tests/parser.phpt
+++ b/plugins/managesieve/tests/parser.phpt
@@ -6,7 +6,7 @@
 include '../lib/rcube_sieve_script.php';
 
 $txt = '
-require ["fileinto","vacation","reject","relational","comparator-i;ascii-numeric","imapflags"];
+require ["fileinto","reject","envelope"];
 # rule:[spam]
 if anyof (header :contains "X-DSPAM-Result" "Spam")
 {
@@ -14,26 +14,15 @@
 	stop;
 }
 # rule:[test1]
-if anyof (header :contains ["From","To"] "test@domain.tld")
+if anyof (header :comparator "i;ascii-casemap" :contains ["From","To"] "test@domain.tld")
 {
 	discard;
 	stop;
 }
 # rule:[test2]
-if anyof (not header :contains ["Subject"] "[test]", header :contains "Subject" "[test2]")
+if anyof (not header :comparator "i;octet" :contains ["Subject"] "[test]", header :contains "Subject" "[test2]")
 {
 	fileinto "test";
-	stop;
-}
-# rule:[test-vacation]
-if anyof (header :contains "Subject" "vacation")
-{
-	vacation :days 1 text:
-# test
-test test /* test */
-test
-.
-;
 	stop;
 }
 # rule:[comments]
@@ -44,24 +33,40 @@
 }
 # rule:[reject]
 if size :over 5000K {
-    reject "Message over 5MB size limit. Please contact me before sending this.";
+	reject "Message over 5MB size limit. Please contact me before sending this.";
 }
-# rule:[redirect]
-if header :value "ge" :comparator "i;ascii-numeric"
-    ["X-Spam-score"] ["14"] {redirect "test@test.tld";}
-# rule:[imapflags]
-if header :matches "Subject" "^Test$" {
-    setflag "\\\\Seen";
-    addflag ["\\\\Answered","\\\\Deleted"];
+# rule:[false]
+if false # size :over 5000K
+{
+	stop; /* rule disabled */
+}
+# rule:[true]
+if true
+{
+	stop;
+}
+fileinto "Test";
+# rule:[address test]
+if address :all :is "From" "nagios@domain.tld"
+{
+	fileinto "domain.tld";
+	stop;
+}
+# rule:[envelope test]
+if envelope :domain :is "From" "domain.tld"
+{
+	fileinto "domain.tld";
+	stop;
 }
 ';
 
 $s = new rcube_sieve_script($txt);
 echo $s->as_text();
 
+// -------------------------------------------------------------------------------
 ?>
 --EXPECT--
-require ["fileinto","vacation","reject","relational","comparator-i;ascii-numeric","imapflags"];
+require ["fileinto","reject","envelope"];
 # rule:[spam]
 if header :contains "X-DSPAM-Result" "Spam"
 {
@@ -75,20 +80,9 @@
 	stop;
 }
 # rule:[test2]
-if anyof (not header :contains "Subject" "[test]", header :contains "Subject" "[test2]")
+if anyof (not header :comparator "i;octet" :contains "Subject" "[test]", header :contains "Subject" "[test2]")
 {
 	fileinto "test";
-	stop;
-}
-# rule:[test-vacation]
-if header :contains "Subject" "vacation"
-{
-	vacation :days 1 text:
-# test
-test test /* test */
-test
-.
-;
 	stop;
 }
 # rule:[comments]
@@ -101,14 +95,26 @@
 {
 	reject "Message over 5MB size limit. Please contact me before sending this.";
 }
-# rule:[redirect]
-if header :value "ge" :comparator "i;ascii-numeric" "X-Spam-score" "14"
+# rule:[false]
+if false # size :over 5000K
 {
-	redirect "test@test.tld";
+	stop;
 }
-# rule:[imapflags]
-if header :matches "Subject" "^Test$"
+# rule:[true]
+if true
 {
-	setflag "\\Seen";
-	addflag ["\\Answered","\\Deleted"];
+	stop;
+}
+fileinto "Test";
+# rule:[address test]
+if address :all :is "From" "nagios@domain.tld"
+{
+	fileinto "domain.tld";
+	stop;
+}
+# rule:[envelope test]
+if envelope :domain :is "From" "domain.tld"
+{
+	fileinto "domain.tld";
+	stop;
 }
diff --git a/plugins/managesieve/tests/parser_body.phpt b/plugins/managesieve/tests/parser_body.phpt
new file mode 100644
index 0000000..08ad549
--- /dev/null
+++ b/plugins/managesieve/tests/parser_body.phpt
@@ -0,0 +1,49 @@
+--TEST--
+Test of Sieve body extension (RFC5173)
+--SKIPIF--
+--FILE--
+<?php
+include '../lib/rcube_sieve_script.php';
+
+$txt = '
+require ["body","fileinto"];
+if body :raw :contains "MAKE MONEY FAST"
+{
+	stop;
+}
+if body :content "text" :contains ["missile","coordinates"]
+{
+	fileinto "secrets";
+}
+if body :content "audio/mp3" :contains ""
+{
+	fileinto "jukebox";
+}
+if body :text :contains "project schedule"
+{
+	fileinto "project/schedule";
+}
+';
+
+$s = new rcube_sieve_script($txt);
+echo $s->as_text();
+
+?>
+--EXPECT--
+require ["body","fileinto"];
+if body :raw :contains "MAKE MONEY FAST"
+{
+	stop;
+}
+if body :content "text" :contains ["missile","coordinates"]
+{
+	fileinto "secrets";
+}
+if body :content "audio/mp3" :contains ""
+{
+	fileinto "jukebox";
+}
+if body :text :contains "project schedule"
+{
+	fileinto "project/schedule";
+}
diff --git a/plugins/managesieve/tests/parser_imapflags.phpt b/plugins/managesieve/tests/parser_imapflags.phpt
new file mode 100644
index 0000000..a4bc465
--- /dev/null
+++ b/plugins/managesieve/tests/parser_imapflags.phpt
@@ -0,0 +1,28 @@
+--TEST--
+Test of Sieve vacation extension (RFC5232)
+--SKIPIF--
+--FILE--
+<?php
+include '../lib/rcube_sieve_script.php';
+
+$txt = '
+require ["imapflags"];
+# rule:[imapflags]
+if header :matches "Subject" "^Test$" {
+    setflag "\\\\Seen";
+    addflag ["\\\\Answered","\\\\Deleted"];
+}
+';
+
+$s = new rcube_sieve_script($txt, array('imapflags'));
+echo $s->as_text();
+
+?>
+--EXPECT--
+require ["imapflags"];
+# rule:[imapflags]
+if header :matches "Subject" "^Test$"
+{
+	setflag "\\Seen";
+	addflag ["\\Answered","\\Deleted"];
+}
diff --git a/plugins/managesieve/tests/parser_include.phpt b/plugins/managesieve/tests/parser_include.phpt
new file mode 100644
index 0000000..addc0d4
--- /dev/null
+++ b/plugins/managesieve/tests/parser_include.phpt
@@ -0,0 +1,30 @@
+--TEST--
+Test of Sieve include extension
+--SKIPIF--
+--FILE--
+<?php
+include '../lib/rcube_sieve_script.php';
+
+$txt = '
+require ["include"];
+
+include "script.sieve";
+# rule:[two]
+if true
+{
+    include :optional "second.sieve";
+}
+';
+
+$s = new rcube_sieve_script($txt, array(), array('variables'));
+echo $s->as_text();
+
+?>
+--EXPECT--
+require ["include"];
+include "script.sieve";
+# rule:[two]
+if true
+{
+	include :optional "second.sieve";
+}
diff --git a/plugins/managesieve/tests/parser_kep14.phpt b/plugins/managesieve/tests/parser_kep14.phpt
index 06beaed..dcdbd48 100644
--- a/plugins/managesieve/tests/parser_kep14.phpt
+++ b/plugins/managesieve/tests/parser_kep14.phpt
@@ -10,7 +10,7 @@
 # EDITOR_VERSION 123
 ';
 
-$s = new rcube_sieve_script($txt, array());
+$s = new rcube_sieve_script($txt, array('body'));
 echo $s->as_text();
 
 ?>
diff --git a/plugins/managesieve/tests/parser_prefix.phpt b/plugins/managesieve/tests/parser_prefix.phpt
new file mode 100644
index 0000000..c87e965
--- /dev/null
+++ b/plugins/managesieve/tests/parser_prefix.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Test of prefix comments handling
+--SKIPIF--
+--FILE--
+<?php
+include '../lib/rcube_sieve_script.php';
+
+$txt = '
+# this is a comment
+# and the second line
+
+require ["variables"];
+set "b" "c";
+';
+
+$s = new rcube_sieve_script($txt, array(), array('variables'));
+echo $s->as_text();
+
+?>
+--EXPECT--
+# this is a comment
+# and the second line
+
+require ["variables"];
+set "b" "c";
diff --git a/plugins/managesieve/tests/parser_relational.phpt b/plugins/managesieve/tests/parser_relational.phpt
new file mode 100644
index 0000000..6b6f29f
--- /dev/null
+++ b/plugins/managesieve/tests/parser_relational.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Test of Sieve relational extension (RFC5231)
+--SKIPIF--
+--FILE--
+<?php
+include '../lib/rcube_sieve_script.php';
+
+$txt = '
+require ["relational","comparator-i;ascii-numeric"];
+# rule:[redirect]
+if header :value "ge" :comparator "i;ascii-numeric"
+    ["X-Spam-score"] ["14"] {redirect "test@test.tld";}
+';
+
+$s = new rcube_sieve_script($txt);
+echo $s->as_text();
+
+?>
+--EXPECT--
+require ["relational","comparator-i;ascii-numeric"];
+# rule:[redirect]
+if header :value "ge" :comparator "i;ascii-numeric" "X-Spam-score" "14"
+{
+	redirect "test@test.tld";
+}
diff --git a/plugins/managesieve/tests/parser_vacation.phpt b/plugins/managesieve/tests/parser_vacation.phpt
new file mode 100644
index 0000000..a603ff6
--- /dev/null
+++ b/plugins/managesieve/tests/parser_vacation.phpt
@@ -0,0 +1,39 @@
+--TEST--
+Test of Sieve vacation extension (RFC5230)
+--SKIPIF--
+--FILE--
+<?php
+include '../lib/rcube_sieve_script.php';
+
+$txt = '
+require ["vacation"];
+# rule:[test-vacation]
+if anyof (header :contains "Subject" "vacation")
+{
+	vacation :days 1 text:
+# test
+test test /* test */
+test
+.
+;
+	stop;
+}
+';
+
+$s = new rcube_sieve_script($txt);
+echo $s->as_text();
+
+?>
+--EXPECT--
+require ["vacation"];
+# rule:[test-vacation]
+if header :contains "Subject" "vacation"
+{
+	vacation :days 1 text:
+# test
+test test /* test */
+test
+.
+;
+	stop;
+}
diff --git a/plugins/managesieve/tests/parser_variables.phpt b/plugins/managesieve/tests/parser_variables.phpt
new file mode 100644
index 0000000..cf1f8fc
--- /dev/null
+++ b/plugins/managesieve/tests/parser_variables.phpt
@@ -0,0 +1,39 @@
+--TEST--
+Test of Sieve variables extension
+--SKIPIF--
+--FILE--
+<?php
+include '../lib/rcube_sieve_script.php';
+
+$txt = '
+require ["variables"];
+set "honorific" "Mr";
+set "vacation" text:
+Dear ${HONORIFIC} ${last_name},
+I am out, please leave a message after the meep.
+.
+;
+set :length "b" "${a}";
+set :lower "b" "${a}";
+set :upperfirst "b" "${a}";
+set :upperfirst :lower "b" "${a}";
+set :quotewildcard "b" "Rock*";
+';
+
+$s = new rcube_sieve_script($txt, array(), array('variables'));
+echo $s->as_text();
+
+?>
+--EXPECT--
+require ["variables"];
+set "honorific" "Mr";
+set "vacation" text:
+Dear ${HONORIFIC} ${last_name},
+I am out, please leave a message after the meep.
+.
+;
+set :length "b" "${a}";
+set :lower "b" "${a}";
+set :upperfirst "b" "${a}";
+set :upperfirst :lower "b" "${a}";
+set :quotewildcard "b" "Rock*";
diff --git a/plugins/managesieve/tests/parset_subaddress.phpt b/plugins/managesieve/tests/parset_subaddress.phpt
new file mode 100644
index 0000000..6d4d03c
--- /dev/null
+++ b/plugins/managesieve/tests/parset_subaddress.phpt
@@ -0,0 +1,38 @@
+--TEST--
+Test of Sieve subaddress extension (RFC5233)
+--SKIPIF--
+--FILE--
+<?php
+include '../lib/rcube_sieve_script.php';
+
+$txt = '
+require ["envelope","subaddress","fileinto"];
+if envelope :user "To" "postmaster"
+{
+	fileinto "postmaster";
+	stop;
+}
+if envelope :detail :is "To" "mta-filters"
+{
+	fileinto "mta-filters";
+	stop;
+}
+';
+
+$s = new rcube_sieve_script($txt);
+echo $s->as_text();
+
+// -------------------------------------------------------------------------------
+?>
+--EXPECT--
+require ["envelope","subaddress","fileinto"];
+if envelope :user "To" "postmaster"
+{
+	fileinto "postmaster";
+	stop;
+}
+if envelope :detail :is "To" "mta-filters"
+{
+	fileinto "mta-filters";
+	stop;
+}
diff --git a/plugins/newmail_notifier/newmail_notifier.php b/plugins/newmail_notifier/newmail_notifier.php
index 1f1df9e..01e2598 100644
--- a/plugins/newmail_notifier/newmail_notifier.php
+++ b/plugins/newmail_notifier/newmail_notifier.php
@@ -132,7 +132,7 @@
      */
     function notify($args)
     {
-        if ($this->notified) {
+        if ($this->notified || !empty($_GET['_refresh'])) {
             return $args;
         }
 
diff --git a/program/include/rcube_imap.php b/program/include/rcube_imap.php
index 002730f..a0a5f81 100644
--- a/program/include/rcube_imap.php
+++ b/program/include/rcube_imap.php
@@ -3178,7 +3178,7 @@
                 if (is_array($ns)) {
                     foreach ($ns as $ns_data) {
                         if (strlen($ns_data[0])) {
-                            $search = $ns_data[0];
+                            $search[] = $ns_data[0];
                         }
                     }
                 }
diff --git a/program/js/app.js b/program/js/app.js
index 5902b1d..8b98a42 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -5738,7 +5738,7 @@
   this.plain2html = function(plainText, id)
   {
     var lock = this.set_busy(true, 'converting');
-    $(document.getElementById(id)).val('<pre>'+plainText+'</pre>');
+    $(document.getElementById(id)).val(plainText ? '<pre>'+plainText+'</pre>' : '');
     this.set_busy(false, null, lock);
   };
 
diff --git a/program/js/googiespell.js b/program/js/googiespell.js
index e3fdf7d..96d612c 100644
--- a/program/js/googiespell.js
+++ b/program/js/googiespell.js
@@ -1,6 +1,8 @@
 /*
  SpellCheck
     jQuery'fied spell checker based on GoogieSpell 4.0
+      (which was published under GPL "version 2 or any later version")
+
  Copyright (C) 2006 Amir Salihefendic
  Copyright (C) 2009 Aleksander Machniak
  Copyright (C) 2011 Kolab Systems AG

--
Gitblit v1.9.1