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>