[Web] Autodiscover returns given password decoded and trimed; Add sieve pre and post filters to UI; Move ajax called files; Rework log system: 100 entries per default, add more per click; Syncjobs: Do not read log to data attribute

This commit is contained in:
André
2017-11-03 20:37:24 +01:00
parent 1ef10f1358
commit 85d1ee2f49
65 changed files with 3460 additions and 709 deletions

View File

@@ -0,0 +1,7 @@
<?php namespace Sieve;
interface SieveDumpable
{
function dump();
function text();
}

View File

@@ -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;
}
}

View File

@@ -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_);
}
}

View File

@@ -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);
}
}

View File

@@ -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]+'
);
}

View File

@@ -0,0 +1,6 @@
<?php namespace Sieve;
class SieveScript
{
// TODO: implement
}

View File

@@ -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']);
}
}
}

View File

@@ -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_);
}
}

View File

@@ -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]);
}
}
}

View File

@@ -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>

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' standalone='yes'?>
<extension name="comparator-i;ascii-numeric">
<comparator name="i;ascii-numeric" />
</extension>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,11 @@
<?xml version='1.0' standalone='yes'?>
<extension name="ereject">
<command name="ereject">
<parameter type="string" name="reason" />
</command>
</extension>

View File

@@ -0,0 +1,9 @@
<?xml version='1.0' standalone='yes'?>
<extension name="fileinto">
<command name="fileinto">
<parameter type="string" name="folder" />
</command>
</extension>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,11 @@
<?xml version='1.0' standalone='yes'?>
<extension name="reject">
<command name="reject">
<parameter type="string" name="reason" />
</command>
</extension>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,8 @@
<?xml version='1.0' standalone='yes'?>
<extension name="subaddress">
<addresspart name="user" />
<addresspart name="detail" />
</extension>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>