git subrepo clone https://github.com/mailcow/mailcow-dockerized.git mailcow/src/mailcow-dockerized

subrepo: subdir:   "mailcow/src/mailcow-dockerized"
  merged:   "a832becb"
upstream: origin:   "https://github.com/mailcow/mailcow-dockerized.git"
  branch:   "master"
  commit:   "a832becb"
git-subrepo: version:  "0.4.3"
  origin:   "???"
  commit:   "???"
Change-Id: If5be2d621a211e164c9b6577adaa7884449f16b5
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveDumpable.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveDumpable.php
new file mode 100644
index 0000000..af82d6a
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveDumpable.php
@@ -0,0 +1,7 @@
+<?php namespace Sieve;
+
+interface SieveDumpable
+{
+	function dump();
+	function text();
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveException.php
new file mode 100644
index 0000000..da7fe60
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveException.php
@@ -0,0 +1,47 @@
+<?php namespace Sieve;
+
+require_once('SieveToken.php');
+
+use Exception;
+
+class SieveException extends Exception
+{
+    protected $token_;
+
+    public function __construct(SieveToken $token, $arg)
+    {
+        $message = 'undefined sieve exception';
+        $this->token_ = $token;
+
+        if (is_string($arg))
+        {
+            $message = $arg;
+        }
+        else
+        {
+            if (is_array($arg))
+            {
+                $type = SieveToken::typeString(array_shift($arg));
+                foreach($arg as $t)
+                {
+                    $type .= ' or '. SieveToken::typeString($t);
+                }
+            }
+            else
+            {
+                $type = SieveToken::typeString($arg);
+            }
+
+            $tokenType = SieveToken::typeString($token->type);
+            $message = "$tokenType where $type expected near ". $token->text;
+        }
+
+        parent::__construct('line '. $token->line .": $message");
+    }
+
+    public function getLineNo()
+    {
+        return $this->token_->line;
+    }
+
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveKeywordRegistry.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveKeywordRegistry.php
new file mode 100644
index 0000000..b901dbc
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveKeywordRegistry.php
@@ -0,0 +1,233 @@
+<?php namespace Sieve;
+
+class SieveKeywordRegistry
+{
+    protected $registry_ = array();
+    protected $matchTypes_ = array();
+    protected $comparators_ = array();
+    protected $addressParts_ = array();
+    protected $commands_ = array();
+    protected $tests_ = array();
+    protected $arguments_ = array();
+
+    protected static $refcount = 0;
+    protected static $instance = null;
+
+    protected function __construct()
+    {
+        $keywords = simplexml_load_file(dirname(__FILE__) .'/keywords.xml');
+        foreach ($keywords->children() as $keyword)
+        {
+            switch ($keyword->getName())
+            {
+            case 'matchtype':
+                $type =& $this->matchTypes_;
+                break;
+            case 'comparator':
+                $type =& $this->comparators_;
+                break;
+            case 'addresspart':
+                $type =& $this->addressParts_;
+                break;
+            case 'test':
+                $type =& $this->tests_;
+                break;
+            case 'command':
+                $type =& $this->commands_;
+                break;
+            default:
+                trigger_error('Unsupported keyword type "'. $keyword->getName()
+                    . '" in file "keywords/'. basename($file) .'"');
+                return;
+            }
+
+            $name = (string) $keyword['name'];
+            if (array_key_exists($name, $type))
+                trigger_error("redefinition of $type $name - skipping");
+            else
+                $type[$name] = $keyword->children();
+        }
+
+        foreach (glob(dirname(__FILE__) .'/extensions/*.xml') as $file)
+        {
+            $extension = simplexml_load_file($file);
+            $name = (string) $extension['name'];
+
+            if (array_key_exists($name, $this->registry_))
+            {
+                trigger_error('overwriting extension "'. $name .'"');
+            }
+            $this->registry_[$name] = $extension;
+        }
+    }
+
+    public static function get()
+    {
+        if (self::$instance == null)
+        {
+            self::$instance = new SieveKeywordRegistry();
+        }
+
+        self::$refcount++;
+
+        return self::$instance;
+    }
+
+    public function put()
+    {
+        if (--self::$refcount == 0)
+        {
+            self::$instance = null;
+        }
+    }
+
+    public function activate($extension)
+    {
+        if (!isset($this->registry_[$extension]))
+        {
+            return;
+        }
+
+        $xml = $this->registry_[$extension];
+
+        foreach ($xml->children() as $e)
+        {
+            switch ($e->getName())
+            {
+            case 'matchtype':
+                $type =& $this->matchTypes_;
+                break;
+            case 'comparator':
+                $type =& $this->comparators_;
+                break;
+            case 'addresspart':
+                $type =& $this->addressParts_;
+                break;
+            case 'test':
+                $type =& $this->tests_;
+                break;
+            case 'command':
+                $type =& $this->commands_;
+                break;
+            case 'tagged-argument':
+                $xml = $e->parameter[0];
+                $this->arguments_[(string) $xml['name']] = array(
+                    'extends' => (string) $e['extends'],
+                    'rules'   => $xml
+                );
+                continue;
+            default:
+                trigger_error('Unsupported extension type \''.
+                    $e->getName() ."' in extension '$extension'");
+                return;
+            }
+
+            $name = (string) $e['name'];
+            if (!isset($type[$name]) ||
+                (string) $e['overrides'] == 'true')
+            {
+                $type[$name] = $e->children();
+            }
+        }
+    }
+
+    public function isTest($name)
+    {
+        return (isset($this->tests_[$name]) ? true : false);
+    }
+
+    public function isCommand($name)
+    {
+        return (isset($this->commands_[$name]) ? true : false);
+    }
+
+    public function matchtype($name)
+    {
+        if (isset($this->matchTypes_[$name]))
+        {
+            return $this->matchTypes_[$name];
+        }
+        return null;
+    }
+
+    public function addresspart($name)
+    {
+        if (isset($this->addressParts_[$name]))
+        {
+            return $this->addressParts_[$name];
+        }
+        return null;
+    }
+
+    public function comparator($name)
+    {
+        if (isset($this->comparators_[$name]))
+        {
+            return $this->comparators_[$name];
+        }
+        return null;
+    }
+
+    public function test($name)
+    {
+        if (isset($this->tests_[$name]))
+        {
+            return $this->tests_[$name];
+        }
+        return null;
+    }
+
+    public function command($name)
+    {
+        if (isset($this->commands_[$name]))
+        {
+            return $this->commands_[$name];
+        }
+        return null;
+    }
+
+    public function arguments($command)
+    {
+        $res = array();
+        foreach ($this->arguments_ as $arg)
+        {
+            if (preg_match('/'.$arg['extends'].'/', $command))
+                array_push($res, $arg['rules']);
+        }
+        return $res;
+    }
+
+    public function argument($name)
+    {
+        if (isset($this->arguments_[$name]))
+        {
+            return $this->arguments_[$name]['rules'];
+        }
+        return null;
+    }
+
+    public function requireStrings()
+    {
+        return array_keys($this->registry_);
+    }
+    public function matchTypes()
+    {
+        return array_keys($this->matchTypes_);
+    }
+    public function comparators()
+    {
+        return array_keys($this->comparators_);
+    }
+    public function addressParts()
+    {
+        return array_keys($this->addressParts_);
+    }
+    public function tests()
+    {
+        return array_keys($this->tests_);
+    }
+    public function commands()
+    {
+        return array_keys($this->commands_);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveParser.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveParser.php
new file mode 100644
index 0000000..3d81c14
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveParser.php
@@ -0,0 +1,255 @@
+<?php namespace Sieve;
+
+include_once 'SieveTree.php';
+include_once 'SieveScanner.php';
+include_once 'SieveSemantics.php';
+include_once 'SieveException.php';
+
+class SieveParser
+{
+    protected $scanner_;
+    protected $script_;
+    protected $tree_;
+    protected $status_;
+
+    public function __construct($script = null)
+    {
+        if (isset($script))
+            $this->parse($script);
+    }
+
+    public function GetParseTree()
+    {
+        return $this->tree_;
+    }
+
+    public function dumpParseTree()
+    {
+        return $this->tree_->dump();
+    }
+
+    public function getScriptText()
+    {
+        return $this->tree_->getText();
+    }
+
+    protected function getPrevToken_($parent_id)
+    {
+        $childs = $this->tree_->getChilds($parent_id);
+
+        for ($i = count($childs); $i > 0; --$i)
+        {
+            $prev = $this->tree_->getNode($childs[$i-1]);
+            if ($prev->is(SieveToken::Comment|SieveToken::Whitespace))
+                continue;
+
+            // use command owning a block or list instead of previous
+            if ($prev->is(SieveToken::BlockStart|SieveToken::Comma|SieveToken::LeftParenthesis))
+                $prev = $this->tree_->getNode($parent_id);
+
+            return $prev;
+        }
+
+        return $this->tree_->getNode($parent_id);
+    }
+
+    /*******************************************************************************
+     * methods for recursive descent start below
+     */
+    public function passthroughWhitespaceComment($token)
+    {
+        return 0;
+    }
+
+    public function passthroughFunction($token)
+    {
+        $this->tree_->addChild($token);
+    }
+
+    public function parse($script)
+    {
+        $this->script_ = $script;
+
+        $this->scanner_ = new SieveScanner($this->script_);
+
+        // Define what happens with passthrough tokens like whitespacs and comments
+        $this->scanner_->setPassthroughFunc(
+            array(
+                $this, 'passthroughWhitespaceComment'
+            )
+        );
+
+        $this->tree_ = new SieveTree('tree');
+
+        $this->commands_($this->tree_->getRoot());
+
+        if (!$this->scanner_->nextTokenIs(SieveToken::ScriptEnd)) {
+            $token = $this->scanner_->nextToken();
+            throw new SieveException($token, SieveToken::ScriptEnd);
+        }
+    }
+
+    protected function commands_($parent_id)
+    {
+        while (true)
+        {
+            if (!$this->scanner_->nextTokenIs(SieveToken::Identifier))
+                break;
+
+            // Get and check a command token
+            $token = $this->scanner_->nextToken();
+            $semantics = new SieveSemantics($token, $this->getPrevToken_($parent_id));
+
+            // Process eventual arguments
+            $this_node = $this->tree_->addChildTo($parent_id, $token);
+            $this->arguments_($this_node, $semantics);
+
+            $token = $this->scanner_->nextToken();
+            if (!$token->is(SieveToken::Semicolon))
+            {
+                // TODO: check if/when semcheck is needed here
+                $semantics->validateToken($token);
+
+                if ($token->is(SieveToken::BlockStart))
+                {
+                    $this->tree_->addChildTo($this_node, $token);
+                    $this->block_($this_node, $semantics);
+                    continue;
+                }
+
+                throw new SieveException($token, SieveToken::Semicolon);
+            }
+
+            $semantics->done($token);
+            $this->tree_->addChildTo($this_node, $token);
+        }
+    }
+
+    protected function arguments_($parent_id, &$semantics)
+    {
+        while (true)
+        {
+            if ($this->scanner_->nextTokenIs(SieveToken::Number|SieveToken::Tag))
+            {
+                // Check if semantics allow a number or tag
+                $token = $this->scanner_->nextToken();
+                $semantics->validateToken($token);
+                $this->tree_->addChildTo($parent_id, $token);
+            }
+            else if ($this->scanner_->nextTokenIs(SieveToken::StringList))
+            {
+                $this->stringlist_($parent_id, $semantics);
+            }
+            else
+            {
+                break;
+            }
+        }
+
+        if ($this->scanner_->nextTokenIs(SieveToken::TestList))
+        {
+            $this->testlist_($parent_id, $semantics);
+        }
+    }
+
+    protected function stringlist_($parent_id, &$semantics)
+    {
+        if (!$this->scanner_->nextTokenIs(SieveToken::LeftBracket))
+        {
+            $this->string_($parent_id, $semantics);
+            return;
+        }
+
+        $token = $this->scanner_->nextToken();
+        $semantics->startStringList($token);
+        $this->tree_->addChildTo($parent_id, $token);
+        
+        if($this->scanner_->nextTokenIs(SieveToken::RightBracket)) {
+            //allow empty lists
+            $token = $this->scanner_->nextToken();
+            $this->tree_->addChildTo($parent_id, $token);
+            $semantics->endStringList();
+            return;
+        }
+
+        do
+        {
+            $this->string_($parent_id, $semantics);
+            $token = $this->scanner_->nextToken();
+
+            if (!$token->is(SieveToken::Comma|SieveToken::RightBracket))
+                throw new SieveException($token, array(SieveToken::Comma, SieveToken::RightBracket));
+
+            if ($token->is(SieveToken::Comma))
+                $semantics->continueStringList();
+
+            $this->tree_->addChildTo($parent_id, $token);
+        }
+        while (!$token->is(SieveToken::RightBracket));
+
+        $semantics->endStringList();
+    }
+
+    protected function string_($parent_id, &$semantics)
+    {
+        $token = $this->scanner_->nextToken();
+        $semantics->validateToken($token);
+        $this->tree_->addChildTo($parent_id, $token);
+    }
+
+    protected function testlist_($parent_id, &$semantics)
+    {
+        if (!$this->scanner_->nextTokenIs(SieveToken::LeftParenthesis))
+        {
+            $this->test_($parent_id, $semantics);
+            return;
+        }
+
+        $token = $this->scanner_->nextToken();
+        $semantics->validateToken($token);
+        $this->tree_->addChildTo($parent_id, $token);
+
+        do
+        {
+            $this->test_($parent_id, $semantics);
+
+            $token = $this->scanner_->nextToken();
+            if (!$token->is(SieveToken::Comma|SieveToken::RightParenthesis))
+            {
+                throw new SieveException($token, array(SieveToken::Comma, SieveToken::RightParenthesis));
+            }
+            $this->tree_->addChildTo($parent_id, $token);
+        }
+        while (!$token->is(SieveToken::RightParenthesis));
+    }
+
+    protected function test_($parent_id, &$semantics)
+    {
+        // Check if semantics allow an identifier
+        $token = $this->scanner_->nextToken();
+        $semantics->validateToken($token);
+
+        // Get semantics for this test command
+        $this_semantics = new SieveSemantics($token, $this->getPrevToken_($parent_id));
+        $this_node = $this->tree_->addChildTo($parent_id, $token);
+
+        // Consume eventual argument tokens
+        $this->arguments_($this_node, $this_semantics);
+
+        // Check that all required arguments were there
+        $token = $this->scanner_->peekNextToken();
+        $this_semantics->done($token);
+    }
+
+    protected function block_($parent_id, &$semantics)
+    {
+        $this->commands_($parent_id, $semantics);
+
+        $token = $this->scanner_->nextToken();
+        if (!$token->is(SieveToken::BlockEnd))
+        {
+            throw new SieveException($token, SieveToken::BlockEnd);
+        }
+        $this->tree_->addChildTo($parent_id, $token);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveScanner.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveScanner.php
new file mode 100644
index 0000000..a0fa57a
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveScanner.php
@@ -0,0 +1,145 @@
+<?php namespace Sieve;
+
+include_once('SieveToken.php');
+
+class SieveScanner
+{
+    public function __construct(&$script)
+    {
+        if ($script === null)
+            return;
+
+        $this->tokenize($script);
+    }
+
+    public function setPassthroughFunc($callback)
+    {
+        if ($callback == null || is_callable($callback))
+            $this->ptFn_ = $callback;
+    }
+
+    public function tokenize(&$script)
+    {
+        $pos = 0;
+        $line = 1;
+
+        $scriptLength = mb_strlen($script);
+
+        $unprocessedScript = $script;
+
+
+        //create one regex to find the right match
+        //avoids looping over all possible tokens: increases performance
+        $nameToType = [];
+        $regex = [];
+        // chr(65) == 'A'
+        $i = 65;
+
+        foreach ($this->tokenMatch_ as $type => $subregex) {
+            $nameToType[chr($i)] = $type;
+            $regex[] = "(?P<". chr($i) . ">^$subregex)";
+            $i++;
+        }
+
+        $regex = '/' . join('|', $regex) . '/';
+
+        while ($pos < $scriptLength)
+        {
+            if (preg_match($regex, $unprocessedScript, $match)) {
+
+                // only keep the group that match and we only want matches with group names
+                // we can use the group name to find the token type using nameToType
+                $filterMatch = array_filter(array_filter($match), 'is_string', ARRAY_FILTER_USE_KEY);
+
+                // the first element in filterMatch will contain the matched group and the key will be the name
+                $type = $nameToType[key($filterMatch)];
+                $currentMatch = current($filterMatch);
+
+                //create the token
+                $token = new SieveToken($type, $currentMatch, $line);
+                $this->tokens_[] = $token;
+
+                if ($type == SieveToken::Unknown)
+                    return;
+
+                // just remove the part that we parsed: don't extract the new substring using script length
+                // as mb_strlen is \theta(pos)  (it's linear in the position)
+                $matchLength = mb_strlen($currentMatch);
+                $unprocessedScript = mb_substr($unprocessedScript, $matchLength);
+
+                $pos += $matchLength;
+                $line += mb_substr_count($currentMatch, "\n");
+            } else {
+                $this->tokens_[] = new SieveToken(SieveToken::Unknown, '', $line);
+                return;
+            }
+
+        }
+
+        $this->tokens_[] = new SieveToken(SieveToken::ScriptEnd, '', $line);
+    }
+
+    public function nextTokenIs($type)
+    {
+        return $this->peekNextToken()->is($type);
+    }
+
+    public function peekNextToken()
+    {
+        $offset = 0;
+        do {
+            $next = $this->tokens_[$this->tokenPos_ + $offset++];
+        } while ($next->is(SieveToken::Comment|SieveToken::Whitespace));
+
+        return $next;
+    }
+
+    public function nextToken()
+    {
+        $token = $this->tokens_[$this->tokenPos_++];
+
+        while ($token->is(SieveToken::Comment|SieveToken::Whitespace))
+        {
+            if ($this->ptFn_ != null)
+                call_user_func($this->ptFn_, $token);
+
+            $token = $this->tokens_[$this->tokenPos_++];
+        }
+
+        return $token;
+    }
+
+    protected $ptFn_ = null;
+    protected $tokenPos_ = 0;
+    protected $tokens_ = array();
+    protected $tokenMatch_ = array (
+        SieveToken::LeftBracket       =>  '\[',
+        SieveToken::RightBracket      =>  '\]',
+        SieveToken::BlockStart        =>  '\{',
+        SieveToken::BlockEnd          =>  '\}',
+        SieveToken::LeftParenthesis   =>  '\(',
+        SieveToken::RightParenthesis  =>  '\)',
+        SieveToken::Comma             =>  ',',
+        SieveToken::Semicolon         =>  ';',
+        SieveToken::Whitespace        =>  '[ \r\n\t]+',
+        SieveToken::Tag               =>  ':[[:alpha:]_][[:alnum:]_]*(?=\b)',
+        /*
+        "                           # match a quotation mark
+        (                           # start matching parts that include an escaped quotation mark
+        ([^"]*[^"\\\\])             # match a string without quotation marks and not ending with a backlash
+        ?                           # this also includes the empty string
+        (\\\\\\\\)*                 # match any groups of even number of backslashes
+                                    # (thus the character after these groups are not escaped)
+        \\\\"                       # match an escaped quotation mark
+        )*                          # accept any number of strings that end with an escaped quotation mark
+        [^"]*                       # accept any trailing part that does not contain any quotation marks
+        "                           # end of the quoted string
+        */
+        SieveToken::QuotedString      =>  '"(([^"]*[^"\\\\])?(\\\\\\\\)*\\\\")*[^"]*"',
+        SieveToken::Number            =>  '[[:digit:]]+(?:[KMG])?(?=\b)',
+        SieveToken::Comment           =>  '(?:\/\*(?:[^\*]|\*(?=[^\/]))*\*\/|#[^\r\n]*\r?(\n|$))',
+        SieveToken::MultilineString   =>  'text:[ \t]*(?:#[^\r\n]*)?\r?\n(\.[^\r\n]+\r?\n|[^\.][^\r\n]*\r?\n)*\.\r?(\n|$)',
+        SieveToken::Identifier        =>  '[[:alpha:]_][[:alnum:]_]*(?=\b)',
+        SieveToken::Unknown           =>  '[^ \r\n\t]+'
+    );
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveScript.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveScript.php
new file mode 100644
index 0000000..befd99a
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveScript.php
@@ -0,0 +1,6 @@
+<?php namespace Sieve;
+
+class SieveScript
+{
+    // TODO: implement
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveSemantics.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveSemantics.php
new file mode 100644
index 0000000..e131d5b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveSemantics.php
@@ -0,0 +1,611 @@
+<?php namespace Sieve;
+
+require_once('SieveKeywordRegistry.php');
+require_once('SieveToken.php');
+require_once('SieveException.php');
+
+class SieveSemantics
+{
+    protected static $requiredExtensions_ = array();
+
+    protected $comparator_;
+    protected $matchType_;
+    protected $addressPart_;
+    protected $tags_ = array();
+    protected $arguments_;
+    protected $deps_ = array();
+    protected $followupToken_;
+
+    public function __construct($token, $prevToken)
+    {
+        $this->registry_ = SieveKeywordRegistry::get();
+        $command = strtolower($token->text);
+
+        // Check the registry for $command
+        if ($this->registry_->isCommand($command))
+        {
+            $xml = $this->registry_->command($command);
+            $this->arguments_ = $this->makeArguments_($xml);
+            $this->followupToken_ = SieveToken::Semicolon;
+        }
+        else if ($this->registry_->isTest($command))
+        {
+            $xml = $this->registry_->test($command);
+            $this->arguments_ = $this->makeArguments_($xml);
+            $this->followupToken_ = SieveToken::BlockStart;
+        }
+        else
+        {
+            throw new SieveException($token, 'unknown command '. $command);
+        }
+
+        // Check if command may appear at this position within the script
+        if ($this->registry_->isTest($command))
+        {
+            if (is_null($prevToken))
+                throw new SieveException($token, $command .' may not appear as first command');
+
+            if (!preg_match('/^(if|elsif|anyof|allof|not)$/i', $prevToken->text))
+                throw new SieveException($token, $command .' may not appear after '. $prevToken->text);
+        }
+        else if (isset($prevToken))
+        {
+            switch ($command)
+            {
+            case 'require':
+                $valid_after = 'require';
+                break;
+            case 'elsif':
+            case 'else':
+                $valid_after = '(if|elsif)';
+                break;
+            default:
+                $valid_after = $this->commandsRegex_();
+            }
+
+            if (!preg_match('/^'. $valid_after .'$/i', $prevToken->text))
+                throw new SieveException($token, $command .' may not appear after '. $prevToken->text);
+        }
+
+        // Check for extension arguments to add to the command
+        foreach ($this->registry_->arguments($command) as $arg)
+        {
+            switch ((string) $arg['type'])
+            {
+            case 'tag':
+                array_unshift($this->arguments_, array(
+                    'type'       => SieveToken::Tag,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => $this->regex_($arg),
+                    'call'       => 'tagHook_',
+                    'name'       => $this->name_($arg),
+                    'subArgs'    => $this->makeArguments_($arg->children())
+                ));
+                break;
+            }
+        }
+    }
+
+    public function __destruct()
+    {
+        $this->registry_->put();
+    }
+
+    // TODO: the *Regex functions could possibly also be static properties
+    protected function requireStringsRegex_()
+    {
+        return '('. implode('|', $this->registry_->requireStrings()) .')';
+    }
+
+    protected function matchTypeRegex_()
+    {
+        return '('. implode('|', $this->registry_->matchTypes()) .')';
+    }
+
+    protected function addressPartRegex_()
+    {
+        return '('. implode('|', $this->registry_->addressParts()) .')';
+    }
+
+    protected function commandsRegex_()
+    {
+        return '('. implode('|', $this->registry_->commands()) .')';
+    }
+
+    protected function testsRegex_()
+    {
+        return '('. implode('|', $this->registry_->tests()) .')';
+    }
+
+    protected function comparatorRegex_()
+    {
+        return '('. implode('|', $this->registry_->comparators()) .')';
+    }
+
+    protected function occurrence_($arg)
+    {
+        if (isset($arg['occurrence']))
+        {
+            switch ((string) $arg['occurrence'])
+            {
+            case 'optional':
+                return '?';
+            case 'any':
+                return '*';
+            case 'some':
+                return '+';
+            }
+        }
+        return '1';
+    }
+
+    protected function name_($arg)
+    {
+        if (isset($arg['name']))
+        {
+            return (string) $arg['name'];
+        }
+        return (string) $arg['type'];
+    }
+
+    protected function regex_($arg)
+    {
+        if (isset($arg['regex']))
+        {
+            return (string) $arg['regex'];
+        }
+        return '.*';
+    }
+
+    protected function case_($arg)
+    {
+        if (isset($arg['case']))
+        {
+            return (string) $arg['case'];
+        }
+        return 'adhere';
+    }
+
+    protected function follows_($arg)
+    {
+        if (isset($arg['follows']))
+        {
+            return (string) $arg['follows'];
+        }
+        return '.*';
+    }
+
+    protected function makeValue_($arg)
+    {
+        if (isset($arg->value))
+        {
+            $res = $this->makeArguments_($arg->value);
+            return array_shift($res);
+        }
+        return null;
+    }
+
+    /**
+     * Convert an extension (test) commands parameters from XML to
+     * a PHP array the {@see Semantics} class understands.
+     * @param array(SimpleXMLElement) $parameters
+     * @return array
+     */
+    protected function makeArguments_($parameters)
+    {
+        $arguments = array();
+
+        foreach ($parameters as $arg)
+        {
+            // Ignore anything not a <parameter>
+            if ($arg->getName() != 'parameter')
+                continue;
+
+            switch ((string) $arg['type'])
+            {
+            case 'addresspart':
+                array_push($arguments, array(
+                    'type'       => SieveToken::Tag,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => $this->addressPartRegex_(),
+                    'call'       => 'addressPartHook_',
+                    'name'       => 'address part',
+                    'subArgs'    => $this->makeArguments_($arg)
+                ));
+                break;
+
+            case 'block':
+                array_push($arguments, array(
+                    'type'       => SieveToken::BlockStart,
+                    'occurrence' => '1',
+                    'regex'      => '{',
+                    'name'       => 'block',
+                    'subArgs'    => $this->makeArguments_($arg)
+                ));
+                break;
+
+            case 'comparator':
+                array_push($arguments, array(
+                    'type'       => SieveToken::Tag,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => 'comparator',
+                    'name'       => 'comparator',
+                    'subArgs'    => array( array(
+                        'type'       => SieveToken::String,
+                        'occurrence' => '1',
+                        'call'       => 'comparatorHook_',
+                        'case'       => 'adhere',
+                        'regex'      => $this->comparatorRegex_(),
+                        'name'       => 'comparator string',
+                        'follows'    => 'comparator'
+                    ))
+                ));
+                break;
+
+            case 'matchtype':
+                array_push($arguments, array(
+                    'type'       => SieveToken::Tag,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => $this->matchTypeRegex_(),
+                    'call'       => 'matchTypeHook_',
+                    'name'       => 'match type',
+                    'subArgs'    => $this->makeArguments_($arg)
+                ));
+                break;
+
+            case 'number':
+                array_push($arguments, array(
+                    'type'       => SieveToken::Number,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => $this->regex_($arg),
+                    'name'       => $this->name_($arg),
+                    'follows'    => $this->follows_($arg)
+                ));
+                break;
+
+            case 'requirestrings':
+                array_push($arguments, array(
+                    'type'       => SieveToken::StringList,
+                    'occurrence' => $this->occurrence_($arg),
+                    'call'       => 'setRequire_',
+                    'case'       => 'adhere',
+                    'regex'      => $this->requireStringsRegex_(),
+                    'name'       => $this->name_($arg)
+                ));
+                break;
+
+            case 'string':
+                array_push($arguments, array(
+                    'type'       => SieveToken::String,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => $this->regex_($arg),
+                    'case'       => $this->case_($arg),
+                    'name'       => $this->name_($arg),
+                    'follows'    => $this->follows_($arg)
+                ));
+                break;
+
+            case 'stringlist':
+                array_push($arguments, array(
+                    'type'       => SieveToken::StringList,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => $this->regex_($arg),
+                    'case'       => $this->case_($arg),
+                    'name'       => $this->name_($arg),
+                    'follows'    => $this->follows_($arg)
+                ));
+                break;
+
+            case 'tag':
+                array_push($arguments, array(
+                    'type'       => SieveToken::Tag,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => $this->regex_($arg),
+                    'call'       => 'tagHook_',
+                    'name'       => $this->name_($arg),
+                    'subArgs'    => $this->makeArguments_($arg->children()),
+                    'follows'    => $this->follows_($arg)
+                ));
+                break;
+
+            case 'test':
+                array_push($arguments, array(
+                    'type'       => SieveToken::Identifier,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => $this->testsRegex_(),
+                    'name'       => $this->name_($arg),
+                    'subArgs'    => $this->makeArguments_($arg->children())
+                ));
+                break;
+
+            case 'testlist':
+                array_push($arguments, array(
+                    'type'       => SieveToken::LeftParenthesis,
+                    'occurrence' => '1',
+                    'regex'      => '\(',
+                    'name'       => $this->name_($arg),
+                    'subArgs'    => null
+                ));
+                array_push($arguments, array(
+                    'type'       => SieveToken::Identifier,
+                    'occurrence' => '+',
+                    'regex'      => $this->testsRegex_(),
+                    'name'       => $this->name_($arg),
+                    'subArgs'    => $this->makeArguments_($arg->children())
+                ));
+                break;
+            }
+        }
+
+        return $arguments;
+    }
+
+    /**
+     * Add argument(s) expected / allowed to appear next.
+     * @param array $value
+     */
+    protected function addArguments_($identifier, $subArgs)
+    {
+        for ($i = count($subArgs); $i > 0; $i--)
+        {
+            $arg = $subArgs[$i-1];
+            if (preg_match('/^'. $arg['follows'] .'$/si', $identifier))
+                array_unshift($this->arguments_, $arg);
+        }
+    }
+
+    /**
+     * Add dependency that is expected to be fullfilled when parsing
+     * of the current command is {@see done}.
+     * @param array $dependency
+     */
+    protected function addDependency_($type, $name, $dependencies)
+    {
+        foreach ($dependencies as $d)
+        {
+            array_push($this->deps_, array(
+                'o_type' => $type,
+                'o_name' => $name,
+                'type'   => $d['type'],
+                'name'   => $d['name'],
+                'regex'  => $d['regex']
+            ));
+        }
+    }
+
+    protected function invoke_($token, $func, $arg = array())
+    {
+        if (!is_array($arg))
+            $arg = array($arg);
+
+        $err = call_user_func_array(array(&$this, $func), $arg);
+
+        if ($err)
+            throw new SieveException($token, $err);
+    }
+
+    protected function setRequire_($extension)
+    {
+        array_push(self::$requiredExtensions_, $extension);
+        $this->registry_->activate($extension);
+    }
+
+    /**
+     * Hook function that is called after a address part match was found
+     * in a command. The kind of address part is remembered in case it's
+     * needed later {@see done}. For address parts from a extension
+     * dependency information and valid values are looked up as well.
+     * @param string $addresspart
+     */
+    protected function addressPartHook_($addresspart)
+    {
+        $this->addressPart_ = $addresspart;
+        $xml = $this->registry_->addresspart($this->addressPart_);
+
+        if (isset($xml))
+        {
+            // Add possible value and dependancy
+            $this->addArguments_($this->addressPart_, $this->makeArguments_($xml));
+            $this->addDependency_('address part', $this->addressPart_, $xml->requires);
+        }
+    }
+
+    /**
+     * Hook function that is called after a match type was found in a
+     * command. The kind of match type is remembered in case it's
+     * needed later {@see done}. For a match type from extensions
+     * dependency information and valid values are looked up as well.
+     * @param string $matchtype
+     */
+    protected function matchTypeHook_($matchtype)
+    {
+        $this->matchType_ = $matchtype;
+        $xml = $this->registry_->matchtype($this->matchType_);
+
+        if (isset($xml))
+        {
+            // Add possible value and dependancy
+            $this->addArguments_($this->matchType_, $this->makeArguments_($xml));
+            $this->addDependency_('match type', $this->matchType_, $xml->requires);
+        }
+    }
+
+    /**
+     * Hook function that is called after a comparator was found in
+     * a command. The comparator is remembered in case it's needed for
+     * comparsion later {@see done}. For a comparator from extensions
+     * dependency information is looked up as well.
+     * @param string $comparator
+     */
+    protected function comparatorHook_($comparator)
+    {
+        $this->comparator_ = $comparator;
+        $xml = $this->registry_->comparator($this->comparator_);
+
+        if (isset($xml))
+        {
+            // Add possible dependancy
+            $this->addDependency_('comparator', $this->comparator_, $xml->requires);
+        }
+    }
+
+    /**
+     * Hook function that is called after a tag was found in
+     * a command. The tag is remembered in case it's needed for
+     * comparsion later {@see done}. For a tags from extensions
+     * dependency information is looked up as well.
+     * @param string $tag
+     */
+    protected function tagHook_($tag)
+    {
+        array_push($this->tags_, $tag);
+        $xml = $this->registry_->argument($tag);
+
+        // Add possible dependancies
+        if (isset($xml))
+            $this->addDependency_('tag', $tag, $xml->requires);
+    }
+
+    protected function validType_($token)
+    {
+        foreach ($this->arguments_ as $arg)
+        {
+            if ($arg['occurrence'] == '0')
+            {
+                array_shift($this->arguments_);
+                continue;
+            }
+
+            if ($token->is($arg['type']))
+                return;
+
+            // Is the argument required
+            if ($arg['occurrence'] != '?' && $arg['occurrence'] != '*')
+                throw new SieveException($token, $arg['type']);
+
+            array_shift($this->arguments_);
+        }
+
+        // Check if command expects any (more) arguments
+        if (empty($this->arguments_))
+            throw new SieveException($token, $this->followupToken_);
+
+        throw new SieveException($token, 'unexpected '. SieveToken::typeString($token->type) .' '. $token->text);
+    }
+
+    public function startStringList($token)
+    {
+        $this->validType_($token);
+        $this->arguments_[0]['type'] = SieveToken::String;
+        $this->arguments_[0]['occurrence'] = '+';
+    }
+
+    public function continueStringList()
+    {
+        $this->arguments_[0]['occurrence'] = '+';
+    }
+
+    public function endStringList()
+    {
+        array_shift($this->arguments_);
+    }
+
+    public function validateToken($token)
+    {
+        // Make sure the argument has a valid type
+        $this->validType_($token);
+
+        foreach ($this->arguments_ as &$arg)
+        {
+            // Build regular expression according to argument type
+            switch ($arg['type'])
+            {
+            case SieveToken::String:
+            case SieveToken::StringList:
+                $regex = '/^(?:text:[^\n]*\n(?P<one>'. $arg['regex'] .')\.\r?\n?|"(?P<two>'. $arg['regex'] .')")$/'
+                       . ($arg['case'] == 'ignore' ? 'si' : 's');
+                break;
+            case SieveToken::Tag:
+                $regex = '/^:(?P<one>'. $arg['regex'] .')$/si';
+                break;
+            default:
+                $regex = '/^(?P<one>'. $arg['regex'] .')$/si';
+            }
+
+            if (preg_match($regex, $token->text, $match))
+            {
+                $text = ($match['one'] ? $match['one'] : $match['two']);
+
+                // Add argument(s) that may now appear after this one
+                if (isset($arg['subArgs']))
+                    $this->addArguments_($text, $arg['subArgs']);
+
+                // Call extra processing function if defined
+                if (isset($arg['call']))
+                    $this->invoke_($token, $arg['call'], $text);
+
+                // Check if a possible value of this argument may occur
+                if ($arg['occurrence'] == '?' || $arg['occurrence'] == '1')
+                {
+                    $arg['occurrence'] = '0';
+                }
+                else if ($arg['occurrence'] == '+')
+                {
+                    $arg['occurrence'] = '*';
+                }
+
+                return;
+            }
+
+            if ($token->is($arg['type']) && $arg['occurrence'] == 1)
+            {
+                throw new SieveException($token,
+                    SieveToken::typeString($token->type) ." $token->text where ". $arg['name'] .' expected');
+            }
+        }
+
+        throw new SieveException($token, 'unexpected '. SieveToken::typeString($token->type) .' '. $token->text);
+    }
+
+    public function done($token)
+    {
+        // Check if there are required arguments left
+        foreach ($this->arguments_ as $arg)
+        {
+            if ($arg['occurrence'] == '+' || $arg['occurrence'] == '1')
+                throw new SieveException($token, $arg['type']);
+        }
+
+        // Check if the command depends on use of a certain tag
+        foreach ($this->deps_ as $d)
+        {
+            switch ($d['type'])
+            {
+            case 'addresspart':
+                $values = array($this->addressPart_);
+                break;
+
+            case 'matchtype':
+                $values = array($this->matchType_);
+                break;
+
+            case 'comparator':
+                $values = array($this->comparator_);
+                break;
+
+            case 'tag':
+                $values = $this->tags_;
+                break;
+            }
+
+            foreach ($values as $value)
+            {
+                if (preg_match('/^'. $d['regex'] .'$/mi', $value))
+                    break 2;
+            }
+
+            throw new SieveException($token,
+                $d['o_type'] .' '. $d['o_name'] .' requires use of '. $d['type'] .' '. $d['name']);
+        }
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveToken.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveToken.php
new file mode 100644
index 0000000..459f45b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveToken.php
@@ -0,0 +1,88 @@
+<?php namespace Sieve;
+
+include_once('SieveDumpable.php');
+
+class SieveToken implements SieveDumpable
+{
+    const Unknown          = 0x0000;
+    const ScriptEnd        = 0x0001;
+    const LeftBracket      = 0x0002;
+    const RightBracket     = 0x0004;
+    const BlockStart       = 0x0008;
+    const BlockEnd         = 0x0010;
+    const LeftParenthesis  = 0x0020;
+    const RightParenthesis = 0x0040;
+    const Comma            = 0x0080;
+    const Semicolon        = 0x0100;
+    const Whitespace       = 0x0200;
+    const Tag              = 0x0400;
+    const QuotedString     = 0x0800;
+    const Number           = 0x1000;
+    const Comment          = 0x2000;
+    const MultilineString  = 0x4000;
+    const Identifier       = 0x8000;
+
+    const String        = 0x4800; // Quoted | Multiline
+    const StringList    = 0x4802; // Quoted | Multiline | LeftBracket
+    const StringListSep = 0x0084; // Comma | RightBracket
+    const Unparsed      = 0x2200; // Comment | Whitespace
+    const TestList      = 0x8020; // Identifier | LeftParenthesis
+
+    public $type;
+    public $text;
+    public $line;
+
+    public function __construct($type, $text, $line)
+    {
+        $this->text = $text;
+        $this->type = $type;
+        $this->line = intval($line);
+    }
+
+    public function dump()
+    {
+        return '<'. SieveToken::escape($this->text) .'> type:'. SieveToken::typeString($this->type) .' line:'. $this->line;
+    }
+
+    public function text()
+    {
+        return $this->text;
+    }
+
+    public function is($type)
+    {
+        return (bool)($this->type & $type);
+    }
+
+    public static function typeString($type)
+    {
+        switch ($type)
+        {
+        case SieveToken::Identifier: return 'identifier';
+        case SieveToken::Whitespace: return 'whitespace';
+        case SieveToken::QuotedString: return 'quoted string';
+        case SieveToken::Tag: return 'tag';
+        case SieveToken::Semicolon: return 'semicolon';
+        case SieveToken::LeftBracket: return 'left bracket';
+        case SieveToken::RightBracket: return 'right bracket';
+        case SieveToken::BlockStart: return 'block start';
+        case SieveToken::BlockEnd: return 'block end';
+        case SieveToken::LeftParenthesis: return 'left parenthesis';
+        case SieveToken::RightParenthesis: return 'right parenthesis';
+        case SieveToken::Comma: return 'comma';
+        case SieveToken::Number: return 'number';
+        case SieveToken::Comment: return 'comment';
+        case SieveToken::MultilineString: return 'multiline string';
+        case SieveToken::ScriptEnd: return 'script end';
+        case SieveToken::String: return 'string';
+        case SieveToken::StringList: return 'string list';
+        default: return 'unknown token';
+        }
+    }
+
+    protected static $tr_ = array("\r" => '\r', "\n" => '\n', "\t" => '\t');
+    public static function escape($val)
+    {
+        return strtr($val, self::$tr_);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveTree.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveTree.php
new file mode 100644
index 0000000..49c7349
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/SieveTree.php
@@ -0,0 +1,117 @@
+<?php namespace Sieve;
+
+class SieveTree
+{
+    protected $childs_;
+    protected $parents_;
+    protected $nodes_;
+    protected $max_id_;
+    protected $dump_;
+
+    public function __construct($name = 'tree')
+    {
+        $this->childs_ = array();
+        $this->parents_ = array();
+        $this->nodes_ = array();
+        $this->max_id_ = 0;
+
+        $this->parents_[0] = null;
+        $this->nodes_[0] = $name;
+    }
+
+    public function addChild(SieveDumpable $child)
+    {
+        return $this->addChildTo($this->max_id_, $child);
+    }
+
+    public function addChildTo($parent_id, SieveDumpable $child)
+    {
+        if (!is_int($parent_id)
+         || !isset($this->nodes_[$parent_id]))
+            return null;
+
+        if (!isset($this->childs_[$parent_id]))
+            $this->childs_[$parent_id] = array();
+
+        $child_id = ++$this->max_id_;
+        $this->nodes_[$child_id] = $child;
+        $this->parents_[$child_id] = $parent_id;
+        array_push($this->childs_[$parent_id], $child_id);
+
+        return $child_id;
+    }
+
+    public function getRoot()
+    {
+        return 0;
+    }
+
+    public function getChilds($node_id)
+    {
+        if (!is_int($node_id)
+        || !isset($this->nodes_[$node_id]))
+            return null;
+
+        if (!isset($this->childs_[$node_id]))
+            return array();
+
+        return $this->childs_[$node_id];
+    }
+
+    public function getNode($node_id)
+    {
+        if ($node_id == 0 || !is_int($node_id)
+         || !isset($this->nodes_[$node_id]))
+            return null;
+
+        return $this->nodes_[$node_id];
+    }
+
+    public function dump()
+    {
+        $this->dump_ = $this->nodes_[$this->getRoot()] ."\n";
+        $this->dumpChilds_($this->getRoot(), ' ');
+        return $this->dump_;
+    }
+
+    protected function dumpChilds_($parent_id, $prefix)
+    {
+        if (!isset($this->childs_[$parent_id]))
+            return;
+
+        $childs = $this->childs_[$parent_id];
+        $last_child = count($childs);
+
+        for ($i=1; $i <= $last_child; ++$i)
+        {
+            $child_node = $this->nodes_[$childs[$i-1]];
+            $infix = ($i == $last_child ? '`--- ' : '|--- ');
+            $this->dump_ .= $prefix . $infix . $child_node->dump() . " (id:" . $childs[$i-1] . ")\n";
+
+            $next_prefix = $prefix . ($i == $last_child ? '   ' : '|  ');
+            $this->dumpChilds_($childs[$i-1], $next_prefix);
+        }
+    }
+
+    public function getText()
+    {
+        $this->dump_ = '';
+        $this->childText_($this->getRoot());
+        return $this->dump_;
+    }
+
+    protected function childText_($parent_id)
+    {
+        if (!isset($this->childs_[$parent_id]))
+            return;
+
+        $childs = $this->childs_[$parent_id];
+
+        for ($i = 0; $i < count($childs); ++$i)
+        {
+            $child_node = $this->nodes_[$childs[$i]];
+            $this->dump_ .= $child_node->text();
+            $this->childText_($childs[$i]);
+        }
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/body.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/body.xml
new file mode 100644
index 0000000..657b845
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/body.xml
@@ -0,0 +1,14 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="body">
+
+	<test name="body">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="tag" name="body transform" regex="(raw|content|text)" occurrence="optional">
+			<parameter type="stringlist" name="content types" follows="content" />
+		</parameter>
+		<parameter type="stringlist" name="key list" />
+	</test>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/comparator-ascii-numeric.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/comparator-ascii-numeric.xml
new file mode 100644
index 0000000..6f96f8d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/comparator-ascii-numeric.xml
@@ -0,0 +1,7 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="comparator-i;ascii-numeric">
+
+	<comparator name="i;ascii-numeric" />
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/copy.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/copy.xml
new file mode 100644
index 0000000..4e3f902
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/copy.xml
@@ -0,0 +1,9 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="copy">
+
+	<tagged-argument extends="(fileinto|redirect)">
+		<parameter type="tag" name="copy" regex="copy" occurrence="optional" />
+	</tagged-argument>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/date.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/date.xml
new file mode 100644
index 0000000..08f6540
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/date.xml
@@ -0,0 +1,28 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="date">
+
+	<test name="date">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="tag" name="zone" regex="(zone|originalzone)" occurrence="optional">
+			<parameter type="string" name="time-zone" follows="zone" />
+		</parameter>
+		<parameter type="string" name="header-name" />
+		<parameter type="string" case="ignore" name="date-part"
+		 regex="(year|month|day|date|julian|hour|minute|second|time|iso8601|std11|zone|weekday)" />
+		<parameter type="stringlist" name="key-list" />
+	</test>
+
+	<test name="currentdate">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="tag" name="zone" regex="zone" occurrence="optional">
+			<parameter type="string" name="time-zone" />
+		</parameter>
+		<parameter type="string" case="ignore" name="date-part"
+		 regex="(year|month|day|date|julian|hour|minute|second|time|iso8601|std11|zone|weekday)" />
+		<parameter type="stringlist" name="key-list" />
+	</test>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/duplicate.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/duplicate.xml
new file mode 100644
index 0000000..1108d2c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/duplicate.xml
@@ -0,0 +1,9 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="duplicate">
+
+	<test name="duplicate">
+
+	</test>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/editheader.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/editheader.xml
new file mode 100644
index 0000000..5224482
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/editheader.xml
@@ -0,0 +1,22 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="editheader">
+
+	<command name="addheader">
+		<parameter type="tag" name="last" regex="last" occurrence="optional" />
+		<parameter type="string" name="field name" />
+		<parameter type="string" name="value" />
+	</command>
+
+	<command name="deleteheader">
+		<parameter type="tag" name="index" regex="index" occurrence="optional">
+			<parameter type="number" name="field number" />
+			<parameter type="tag" name="last" regex="last" occurrence="optional" />
+		</parameter>
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="string" name="field name" />
+		<parameter type="stringlist" name="value patterns" occurrence="optional" />
+	</command>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/envelope.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/envelope.xml
new file mode 100644
index 0000000..ce88ada
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/envelope.xml
@@ -0,0 +1,13 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="envelope">
+
+	<test name="envelope">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="addresspart" occurrence="optional" />
+		<parameter type="stringlist" name="envelope-part" />
+		<parameter type="stringlist" name="key" />
+	</test>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/environment.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/environment.xml
new file mode 100644
index 0000000..edaab8d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/environment.xml
@@ -0,0 +1,13 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="environment">
+
+	<test name="environment">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="string" name="name"
+		 regex="(domain|host|location|name|phase|remote-host|remote-ip|version|vnd\..+)" />
+		<parameter type="stringlist" name="key-list" />
+	</test>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/ereject.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/ereject.xml
new file mode 100644
index 0000000..f723019
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/ereject.xml
@@ -0,0 +1,11 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="ereject">
+
+	<command name="ereject">
+
+		<parameter type="string" name="reason" />
+
+	</command>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/fileinto.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/fileinto.xml
new file mode 100644
index 0000000..de3974c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/fileinto.xml
@@ -0,0 +1,10 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="fileinto">
+
+	<command name="fileinto">
+    <parameter type="tag" name="create" regex="create" occurrence="optional" />
+		<parameter type="string" name="folder" />
+	</command>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/imap4flags.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/imap4flags.xml
new file mode 100644
index 0000000..5f6d176
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/imap4flags.xml
@@ -0,0 +1,29 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="imap4flags">
+
+	<command name="setflag">
+		<parameter type="stringlist" name="flag list" />
+	</command>
+
+	<command name="addflag">
+		<parameter type="stringlist" name="flag list" />
+	</command>
+
+	<command name="removeflag">
+		<parameter type="stringlist" name="flag list" />
+	</command>
+
+	<test name="hasflag">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="stringlist" name="flag list" />
+	</test>
+
+	<tagged-argument extends="(fileinto|keep)">
+		<parameter type="tag" name="flags" regex="flags" occurrence="optional">
+			<parameter type="stringlist" name="flag list" />
+		</parameter>
+	</tagged-argument>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/imapflags.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/imapflags.xml
new file mode 100644
index 0000000..4b78cc8
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/imapflags.xml
@@ -0,0 +1,21 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="imapflags">
+
+	<command name="mark" />
+
+	<command name="unmark" />
+
+	<command name="setflag">
+		<parameter type="stringlist" name="flag list" />
+	</command>
+
+	<command name="addflag">
+		<parameter type="stringlist" name="flag list" />
+	</command>
+
+	<command name="removeflag">
+		<parameter type="stringlist" name="flag list" />
+	</command>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/index.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/index.xml
new file mode 100644
index 0000000..f81055c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/index.xml
@@ -0,0 +1,17 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="index">
+
+	<tagged-argument extends="(header|address|date)">
+		<parameter type="tag" name="index" regex="index" occurrence="optional">
+			<parameter type="number" name="field number" />
+		</parameter>
+	</tagged-argument>
+
+	<tagged-argument extends="(header|address|date)">
+		<parameter type="tag" name="last" regex="last" occurrence="optional">
+			<requires type="tag" name="index" regex="index" />
+		</parameter>
+	</tagged-argument>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/mailbox.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/mailbox.xml
new file mode 100644
index 0000000..c21960f
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/mailbox.xml
@@ -0,0 +1,8 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="mailbox">
+
+	<test name="mailboxexists">
+    <parameter type="string" name="folder" />
+	</test>
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/notify.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/notify.xml
new file mode 100644
index 0000000..e1702e9
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/notify.xml
@@ -0,0 +1,29 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="notify">
+
+	<command name="notify">
+		<parameter type="tag" name="method" regex="method" occurrence="optional">
+			<parameter type="string" name="method-name" />
+		</parameter>
+
+		<parameter type="tag" name="id" regex="id" occurrence="optional">
+			<parameter type="string" name="message-id" />
+		</parameter>
+
+		<parameter type="tag" name="priority" regex="(low|normal|high)" occurrence="optional" />
+
+		<parameter type="tag" name="message" regex="message" occurrence="optional">
+			<parameter type="string" name="message-text" />
+		</parameter>
+	</command>
+
+	<command name="denotify">
+		<parameter type="matchtype" occurrence="optional">
+			<parameter type="string" name="message-id" />
+		</parameter>
+
+		<parameter type="tag" name="priority" regex="(low|normal|high)" occurrence="optional" />
+	</command>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/regex.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/regex.xml
new file mode 100644
index 0000000..79d67fc
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/regex.xml
@@ -0,0 +1,11 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="regex">
+
+	<matchtype name="regex" />
+
+	<tagged-argument extends="set">
+		<parameter type="tag" name="modifier" regex="quoteregex" occurrence="optional" />
+	</tagged-argument>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/reject.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/reject.xml
new file mode 100644
index 0000000..33d2573
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/reject.xml
@@ -0,0 +1,11 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="reject">
+
+	<command name="reject">
+
+		<parameter type="string" name="reason" />
+
+	</command>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/relational.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/relational.xml
new file mode 100644
index 0000000..b9e2b39
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/relational.xml
@@ -0,0 +1,14 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="relational">
+
+	<matchtype name="count">
+		<requires type="comparator" name="i;ascii-numeric" regex="i;ascii-numeric" />
+		<parameter type="string" name="relation string" regex="(lt|le|eq|ge|gt|ne)" />
+	</matchtype>
+
+	<matchtype name="value">
+		<parameter type="string" name="relation string" regex="(lt|le|eq|ge|gt|ne)" />
+	</matchtype>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/spamtest.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/spamtest.xml
new file mode 100644
index 0000000..06c8c3b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/spamtest.xml
@@ -0,0 +1,11 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="spamtest">
+
+	<test name="spamtest">
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="string" name="value" />
+	</test>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/spamtestplus.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/spamtestplus.xml
new file mode 100644
index 0000000..c7b768d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/spamtestplus.xml
@@ -0,0 +1,12 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="spamtestplus">
+
+	<test name="spamtest" overrides="true">
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="tag" name="percent" regex="percent" occurrence="optional" />
+		<parameter type="string" name="value" />
+	</test>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/subaddress.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/subaddress.xml
new file mode 100644
index 0000000..a668fdf
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/subaddress.xml
@@ -0,0 +1,8 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="subaddress">
+
+	<addresspart name="user" />
+	<addresspart name="detail" />
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/vacation-seconds.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/vacation-seconds.xml
new file mode 100644
index 0000000..6418ef8
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/vacation-seconds.xml
@@ -0,0 +1,32 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="vacation-seconds">
+
+	<command name="vacation">
+
+		<parameter type="tag" name="seconds" occurrence="optional" regex="seconds">
+			<parameter type="number" name="period" />
+		</parameter>
+
+		<parameter type="tag" name="addresses" occurrence="optional" regex="addresses">
+			<parameter type="stringlist" name="address strings" />
+		</parameter>
+
+		<parameter type="tag" name="subject" occurrence="optional" regex="subject">
+			<parameter type="string" name="subject string" />
+		</parameter>
+
+		<parameter type="tag" name="from" occurrence="optional" regex="from">
+			<parameter type="string" name="from string" />
+		</parameter>
+
+		<parameter type="tag" name="handle" occurrence="optional" regex="handle">
+			<parameter type="string" name="handle string" />
+		</parameter>
+
+		<parameter type="tag" name="mime" occurrence="optional" regex="mime" />
+
+		<parameter type="string" name="reason" />
+	</command>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/vacation.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/vacation.xml
new file mode 100644
index 0000000..dbd6992
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/vacation.xml
@@ -0,0 +1,31 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="vacation">
+
+	<command name="vacation">
+		<parameter type="tag" name="days" occurrence="optional" regex="days">
+			<parameter type="number" name="period" />
+		</parameter>
+
+		<parameter type="tag" name="addresses" occurrence="optional" regex="addresses">
+			<parameter type="stringlist" name="address strings" />
+		</parameter>
+
+		<parameter type="tag" name="subject" occurrence="optional" regex="subject">
+			<parameter type="string" name="subject string" />
+		</parameter>
+
+		<parameter type="tag" name="from" occurrence="optional" regex="from">
+			<parameter type="string" name="from string" />
+		</parameter>
+
+		<parameter type="tag" name="handle" occurrence="optional" regex="handle">
+			<parameter type="string" name="handle string" />
+		</parameter>
+
+		<parameter type="tag" name="mime" occurrence="optional" regex="mime" />
+
+		<parameter type="string" name="reason" />
+	</command>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/variables.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/variables.xml
new file mode 100644
index 0000000..d9ff000
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/variables.xml
@@ -0,0 +1,21 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="variables">
+
+	<command name="set">
+		<parameter type="tag" name="modifier" regex="(lower|upper)" occurrence="optional" />
+		<parameter type="tag" name="modifier" regex="(lower|upper)first" occurrence="optional" />
+		<parameter type="tag" name="modifier" regex="quotewildcard" occurrence="optional" />
+		<parameter type="tag" name="modifier" regex="length" occurrence="optional" />
+		<parameter type="string" name="name" regex="[[:alpha:]_][[:alnum:]_]*" />
+		<parameter type="string" name="value" />
+	</command>
+
+	<test name="string">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="stringlist" name="source" />
+		<parameter type="stringlist" name="key list" />
+	</test>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/virustest.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/virustest.xml
new file mode 100644
index 0000000..6dac8e8
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/extensions/virustest.xml
@@ -0,0 +1,11 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="virustest">
+
+	<test name="virustest">
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="string" name="value" />
+	</test>
+
+</extension>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/keywords.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/keywords.xml
new file mode 100644
index 0000000..1ab7c4d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/sieve/keywords.xml
@@ -0,0 +1,91 @@
+<?xml version='1.0' standalone='yes'?>
+
+<keywords>
+
+	<matchtype name="is" />
+	<matchtype name="contains" />
+	<matchtype name="matches" />
+	<matchtype name="value">
+		<parameter type="string" name="operator" regex="(gt|ge|eq|le|lt)" />
+	</matchtype>
+
+
+	<comparator name="i;octet" />
+	<comparator name="i;ascii-casemap" />
+	<comparator name="i;unicode-casemap" />
+
+	<addresspart name="all" />
+	<addresspart name="localpart" />
+	<addresspart name="domain" />
+
+
+	<command name="discard" />
+
+	<command name="elsif">
+		<parameter type="test" name="test command" />
+		<parameter type="block" />
+	</command>
+
+	<command name="else">
+		<parameter type="block" />
+	</command>
+
+	<command name="if">
+		<parameter type="test" name="test command" />
+		<parameter type="block" />
+	</command>
+
+	<command name="keep" />
+
+	<command name="redirect">
+		<parameter type="string" name="address string" />
+	</command>
+
+	<command name="require">
+		<parameter type="requirestrings" name="require string" />
+	</command>
+
+	<command name="stop" />
+
+
+	<test name="address">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="addresspart" occurrence="optional" />
+		<parameter type="stringlist" name="header list" />
+		<parameter type="stringlist" name="key list" />
+	</test>
+
+	<test name="allof">
+		<parameter type="testlist" name="test" />
+	</test>
+
+	<test name="anyof">
+		<parameter type="testlist" name="test" />
+	</test>
+
+	<test name="exists">
+		<parameter type="stringlist" name="header names" />
+	</test>
+
+	<test name="false" />
+	
+	<test name="header">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="stringlist" name="header names" />
+		<parameter type="stringlist" name="key list" />
+	</test>
+
+	<test name="not">
+		<parameter type="test" />
+	</test>
+
+	<test name="size">
+		<parameter type="tag" regex="(over|under)" />
+		<parameter type="number" name="limit" />
+	</test>
+
+	<test name="true" />
+
+</keywords>