From 6fb6d241eee7748e95ffd593bd605f94f6417072 Mon Sep 17 00:00:00 2001 From: hickert Date: Thu, 15 Feb 2007 11:27:08 +0000 Subject: [PATCH] Moved sieve stuff into special folder git-svn-id: https://oss.gonicus.de/repositories/gosa/trunk@5722 594d385d-05f5-0310-b6e9-bd551577e9d8 --- include/sieve/class_parser.inc | 319 +++++++++++ include/sieve/class_scanner.inc | 140 +++++ include/sieve/class_semantics.inc | 553 ++++++++++++++++++++ include/{ => sieve}/class_sieve.inc | 0 include/sieve/class_sievescript.php | 321 ++++++++++++ include/sieve/class_tree.inc | 175 +++++++ include/sieve/libsieve.php | 7 + plugins/personal/mail/class_mailAccount.inc | 2 +- 8 files changed, 1516 insertions(+), 1 deletion(-) create mode 100644 include/sieve/class_parser.inc create mode 100644 include/sieve/class_scanner.inc create mode 100644 include/sieve/class_semantics.inc rename include/{ => sieve}/class_sieve.inc (100%) create mode 100644 include/sieve/class_sievescript.php create mode 100644 include/sieve/class_tree.inc create mode 100644 include/sieve/libsieve.php diff --git a/include/sieve/class_parser.inc b/include/sieve/class_parser.inc new file mode 100644 index 000000000..7ca1f9e3b --- /dev/null +++ b/include/sieve/class_parser.inc @@ -0,0 +1,319 @@ +status_text = "incomplete"; + + $this->script_ = $script; + $this->tree_ = new Tree(Scanner::scriptStart()); + $this->tree_->setDumpFunc(array(&$this, 'dumpToken_')); + $this->scanner_ = new Scanner($this->script_); + $this->scanner_->setCommentFunc(array($this, 'comment_')); + + if ($this->commands_($this->tree_->getRoot()) && + $this->scanner_->nextTokenIs('script-end')) + { + return $this->success_('success'); + } + + return $this->status_; + } + + function dumpParseTree() + { + return $this->tree_->dump(); + } + + function dumpToken_(&$token) + { + if (is_array($token)) + { + $str = "<" . $token['text'] . "> "; + foreach ($token as $k => $v) + { + $str .= " $k:$v"; + } + return $str; + } + + return strval($token); + } + + function success_($text = null) + { + if ($text != null) + { + $this->status_text = $text; + } + + return $this->status_ = true; + } + + function error_($text, $token = null) + { + if ($token != null) + { + $text = 'line '. $token['line'] .': '. $token['class'] . " where $text expected near ". $token['text']; + } + + $this->status_text = $text; + return $this->status_ = false; + } + + function done_() + { + $this->status_ = true; + return false; + } + + function comment_($token) + { + $this->tree_->addChild($token); + } + + function commands_($parent_id) + { + while ($this->command_($parent_id)) + ; + + return $this->status_; + } + + function command_($parent_id) + { + if (!$this->scanner_->nextTokenIs('identifier')) + { + if ($this->scanner_->nextTokenIs(array('block-end', 'script-end'))) + { + return $this->done_(); + } + return $this->error_('identifier', $this->scanner_->peekNextToken()); + } + + // Get and check a command token + $token = $this->scanner_->nextToken(); + $semantics = new Semantics($token['text']); + if ($semantics->unknown) + { + return $this->error_('unknown command: '. $token['text']); + } + + $last = $this->tree_->getLastNode($parent_id); + if (!$semantics->validAfter($last['text'])) + { + return $this->error_('"'. $token['text'] .'" may not appear after "'. $last['text'] .'"'); + } + + // Process eventual arguments + $this_node = $this->tree_->addChildTo($parent_id, $token); + if ($this->arguments_($this_node, $semantics) == false) + { + return false; + } + + $token = $this->scanner_->nextToken(); + if ($token['class'] != 'semicolon') + { + if (!$semantics->validToken($token['class'], $token['text'], $token['line'])) + { + return $this->error_($semantics->message); + } + + if ($token['class'] == 'block-start') + { + $this->tree_->addChildTo($this_node, $token); + $ret = $this->block_($this_node, $semantics); + return $ret; + } + + return $this->error_('semicolon', $token); + } + + $this->tree_->addChildTo($this_node, $token); + return $this->success_(); + } + + function arguments_($parent_id, &$semantics) + { + while ($this->argument_($parent_id, &$semantics)) + ; + + if ($this->status_ == true) + { + $this->testlist_($parent_id, $semantics); + } + + return $this->status_; + } + + function argument_($parent_id, &$semantics) + { + if ($this->scanner_->nextTokenIs(array('number', 'tag'))) + { + // Check if semantics allow a number or tag + $token = $this->scanner_->nextToken(); + if (!$semantics->validToken($token['class'], $token['text'], $token['line'])) + { + return $this->error_($semantics->message); + } + + $this->tree_->addChildTo($parent_id, $token); + return $this->success_(); + } + + return $this->stringlist_($parent_id, &$semantics); + } + + function stringlist_($parent_id, &$semantics) + { + if (!$this->scanner_->nextTokenIs('left-bracket')) + { + return $this->string_($parent_id, &$semantics); + } + + $token = $this->scanner_->nextToken(); + if (!$semantics->startStringList($token['line'])) + { + return $this->error_($semantics->message); + } + $this->tree_->addChildTo($parent_id, $token); + + while ($token['class'] != 'right-bracket') + { + if (!$this->string_($parent_id, &$semantics)) + { + return $this->status_; + } + + $token = $this->scanner_->nextToken(); + + if ($token['class'] != 'comma' && $token['class'] != 'right-bracket') + { + return $this->error_('comma or closing bracket', $token); + } + + $this->tree_->addChildTo($parent_id, $token); + } + + $semantics->endStringList(); + return $this->success_(); + } + + function string_($parent_id, &$semantics) + { + if (!$this->scanner_->nextTokenIs(array('quoted-string', 'multi-line'))) + { + return $this->done_(); + } + + $token = $this->scanner_->nextToken(); + if (!$semantics->validToken('string', $token['text'], $token['line'])) + { + return $this->error_($semantics->message); + } + + $this->tree_->addChildTo($parent_id, $token); + return $this->success_(); + } + + function testlist_($parent_id, &$semantics) + { + if (!$this->scanner_->nextTokenIs('left-parant')) + { + return $this->test_($parent_id, $semantics); + } + + $token = $this->scanner_->nextToken(); + if (!$semantics->validToken($token['class'], $token['text'], $token['line'])) + { + return $this->error_($semantics->message); + } + $this->tree_->addChildTo($parent_id, $token); + + while ($token['class'] != 'right-parant') + { + if (!$this->test_($parent_id, $semantics)) + { + return $this->status_; + } + + $token = $this->scanner_->nextToken(); + + if ($token['class'] != 'comma' && $token['class'] != 'right-parant') + { + return $this->error_('comma or closing paranthesis', $token); + } + + $this->tree_->addChildTo($parent_id, $token); + } + + return $this->success_(); + } + + function test_($parent_id, &$semantics) + { + if (!$this->scanner_->nextTokenIs('identifier')) + { + // There is no test + return $this->done_(); + } + + // Check if semantics allow an identifier + $token = $this->scanner_->nextToken(); + if (!$semantics->validToken($token['class'], $token['text'], $token['line'])) + { + return $this->error_($semantics->message); + } + + // Get semantics for this test command + $this_semantics = new Semantics($token['text']); + if ($this_semantics->unknown) + { + return $this->error_('unknown test: '. $token['text']); + } + + $this_node = $this->tree_->addChildTo($parent_id, $token); + + // Consume eventual argument tokens + if (!$this->arguments_($this_node, $this_semantics)) + { + return false; + } + + // Check if arguments were all there + $token = $this->scanner_->peekNextToken(); + if (!$this_semantics->done($token['class'], $token['text'], $token['line'])) + { + return $this->error_($this_semantics->message); + } + + return true; + } + + function block_($parent_id, &$semantics) + { + if ($this->commands_($parent_id, $semantics)) + { + $token = $this->scanner_->nextToken(); + + if ($token['class'] != 'block-end') + { + return $this->error_('closing curly brace', $token); + } + + $this->tree_->addChildTo($parent_id, $token); + return $this->success_(); + } + return $this->status_; + } +} + +?> diff --git a/include/sieve/class_scanner.inc b/include/sieve/class_scanner.inc new file mode 100644 index 000000000..3e22bb1d0 --- /dev/null +++ b/include/sieve/class_scanner.inc @@ -0,0 +1,140 @@ +_construct($script); + } + + function _construct(&$script) + { + if ($script === null) + { + return; + } + + $this->tokenize($script); + } + + function setCommentFunc($callback) + { + if ($callback == null || is_callable($callback)) + { + $this->commentFn_ = $callback; + } + } + + function tokenize(&$script) + { + $pos = 0; + $line = 1; + $script_length = mb_strlen($script); + + while ($pos < $script_length) + { + foreach ($this->token_match_ as $class => $regex) + { + if (preg_match('/^'. $regex .'/', mb_substr($script, $pos), $match)) + { + $length = mb_strlen($match[0]); + + if ($class != 'whitespace') + { + array_push($this->tokens_, array( + 'class' => $class, + 'text' => chop(mb_substr($script, $pos, $length)), + 'line' => $line, + )); + } + if ($class == 'unknown') + { + return; + } + + $pos += $length; + $line += mb_substr_count($match[0], "\n"); + break; + } + } + } + + array_push($this->tokens_, array( + 'class' => 'script-end', + 'text' => 'script-end', + 'line' => $line, + )); + } + + function nextTokenIs($class) + { + $offset = 0; + do + { + $next = $this->tokens_[$this->tokenPos_ + $offset++]['class']; + } + while ($next == 'comment'); + + if (is_array($class)) + { + return in_array($next, $class); + } + else if (is_string($class)) + { + return (strcmp($next, $class) == 0); + } + return false; + } + + function peekNextToken() + { + return $this->tokens_[$this->tokenPos_]; + } + + function nextToken() + { + $token = $this->tokens_[$this->tokenPos_++]; + while ($token['class'] == 'comment') + { + if ($this->commentFn_ != null) + { + call_user_func($this->commentFn_, $token); + } + $token = $this->tokens_[$this->tokenPos_++]; + } + return $token; + } + + function scriptStart() + { + return array( + 'class' => 'script-start', + 'text' => 'script-start', + 'line' => 1, + ); + } + + var $commentFn_ = null; + var $tokenPos_ = 0; + var $tokens_ = array(); + var $token_match_ = array ( + 'left-bracket' => '\[', + 'right-bracket' => '\]', + 'block-start' => '\{', + 'block-end' => '\}', + 'left-parant' => '\(', + 'right-parant' => '\)', + 'comma' => ',', + 'semicolon' => ';', + 'whitespace' => '[ \r\n\t]+', + 'tag' => ':[[:alpha:]_][[:alnum:]_]*(?=\b)', + 'quoted-string' => '"(?:\\[\\"]|[^\x00"])*"', + 'number' => '[[:digit:]]+(?:[KMG])?(?=\b)', + 'comment' => '(?:\/\*(?:[^\*]|\*(?=[^\/]))*\*\/|#[^\r\n]*\r?\n)', + 'multi-line' => 'text:[ \t]*(?:#[^\r\n]*)?\r?\n(\.[^\r\n]+\r?\n|[^\.]*\r?\n)*\.\r?\n', + 'identifier' => '[[:alpha:]_][[:alnum:]_]*(?=\b)', + 'unknown token' => '[^ \r\n\t]+' + ); +} + +?> \ No newline at end of file diff --git a/include/sieve/class_semantics.inc b/include/sieve/class_semantics.inc new file mode 100644 index 000000000..e4b1f4c19 --- /dev/null +++ b/include/sieve/class_semantics.inc @@ -0,0 +1,553 @@ +command_ = $command; + $this->unknown = false; + switch ($command) + { + + /******************** + * control commands + */ + case 'require': + /* require */ + $this->s_ = array( + 'valid_after' => array('script-start', 'require'), + 'arguments' => array( + array('class' => 'string', 'list' => true, 'name' => 'require-string', 'occurrences' => '1', 'call' => 'setRequire_', 'values' => array( + array('occurrences' => '+', 'regex' => '"'. $this->requireStrings_ .'"'), + array('occurrences' => '+', 'regex' => '"comparator-i;(octet|ascii-casemap|ascii-numeric)"') + )) + ) + ); + break; + + case 'if': + /* if */ + $this->s_ = array( + 'valid_after' => array('script-start', 'require', 'if', 'elsif', 'else', + 'reject', 'fileinto', 'redirect', 'stop', 'keep', 'discard'), + 'arguments' => array( + array('class' => 'identifier', 'occurrences' => '1', 'values' => array( + array('occurrences' => '1', 'regex' => $this->testCommands_, 'name' => 'test') + )), + array('class' => 'block-start', 'occurrences' => '1', 'values' => array( + array('occurrences' => '1', 'regex' => '{', 'name' => 'block') + )) + ) + ); + break; + + case 'elsif': + /* elsif */ + $this->s_ = array( + 'valid_after' => array('if', 'elsif'), + 'arguments' => array( + array('class' => 'identifier', 'occurrences' => '1', 'values' => array( + array('occurrences' => '1', 'regex' => $this->testCommands_, 'name' => 'test') + )), + array('class' => 'block-start', 'occurrences' => '1', 'values' => array( + array('occurrences' => '1', 'regex' => '{', 'name' => 'block') + )) + ) + ); + break; + + case 'else': + /* else */ + $this->s_ = array( + 'valid_after' => array('if', 'elsif'), + 'arguments' => array( + array('class' => 'block-start', 'occurrences' => '1', 'values' => array( + array('occurrences' => '1', 'regex' => '{', 'name' => 'block') + )) + ) + ); + break; + + + /******************* + * action commands + */ + case 'keep': + case 'stop': + case 'discard': + /* keep / stop / discard */ + $this->s_ = array( + 'valid_after' => array('script-start', 'require', 'if', 'elsif', 'else', + 'reject', 'fileinto', 'redirect', 'stop', 'keep', 'discard') + ); + break; + + case 'fileinto': + /* fileinto */ + $this->s_ = array( + 'requires' => 'fileinto', + 'valid_after' => array('require', 'if', 'elsif', 'else', 'reject', 'fileinto', 'redirect', 'stop', 'keep', 'discard'), + 'arguments' => array( + array('class' => 'string', 'occurrences' => '1', 'values' => array( + array('occurrences' => '1', 'regex' => '".*"', 'name' => 'folder') + )) + ) + ); + break; + + case 'redirect': + /* redirect */ + $this->s_ = array( + 'valid_after' => array('script-start', 'require', 'if', 'elsif', 'else', 'reject', 'fileinto', 'redirect', 'stop', 'keep', 'discard'), + 'arguments' => array( + array('class' => 'string', 'occurrences' => '1', 'values' => array( + array('occurrences' => '1', 'regex' => '".*"', 'name' => 'address') + )) + ) + ); + break; + + case 'reject': + /* reject */ + $this->s_ = array( + 'requires' => 'reject', + 'valid_after' => array('require', 'if', 'elsif', 'else', 'reject', 'fileinto', 'redirect', 'stop', 'keep', 'discard'), + 'arguments' => array( + array('class' => 'string', 'occurrences' => '1', 'values' => array( + array('occurrences' => '1', 'regex' => '".*"', 'name' => 'reason') + )) + ) + ); + break; + + case 'vacation': + /* vacation [":days" number] [":addresses" string-list] [":subject" string] [":mime"] */ + $this->s_ = array( + 'requires' => 'vacation', + 'valid_after' => array('require', 'if', 'elsif', 'else', 'reject', 'fileinto', 'redirect', 'stop', 'keep', 'discard'), + 'arguments' => array( + array('class' => 'tag', 'occurrences' => '*', 'values' => array( + array('occurrences' => '?', 'regex' => ':days', 'name' => 'days', + 'add' => array( + array('class' => 'number', 'occurrences' => '1', 'values' => array( + array('occurrences' => '1', 'regex' => '.*', 'name' => 'period') + )) + ) + ), + array('occurrences' => '?', 'regex' => ':addresses', 'name' => 'addresses', + 'add' => array( + array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array( + array('occurrences' => '+', 'regex' => '".*"', 'name' => 'address') + )) + ) + ), + array('occurrences' => '?', 'regex' => ':subject', 'name' => 'subject', + 'add' => array( + array('class' => 'string', 'occurrences' => '1', 'values' => array( + array('occurrences' => '1', 'regex' => '".*"', 'name' => 'subject') + )) + ) + ), + array('occurrences' => '?', 'regex' => ':mime', 'name' => 'mime') + )), + array('class' => 'string', 'occurrences' => '1', 'values' => array( + array('occurrences' => '1', 'regex' => '".*"', 'name' => 'reason') + )) + ) + ); + break; + + + /***************** + * test commands + */ + case 'address': + /* address [address-part: tag] [comparator: tag] [match-type: tag] */ + $this->s_ = array( + 'valid_after' => array('if', 'elsif', 'anyof', 'allof', 'not'), + 'arguments' => array( + array('class' => 'tag', 'occurrences' => '*', 'post-call' => 'checkTags_', 'values' => array( + array('occurrences' => '?', 'regex' => ':(is|contains|matches|count|value)', 'call' => 'setMatchType_', 'name' => 'match-type'), + array('occurrences' => '?', 'regex' => ':(all|localpart|domain|user|detail)', 'call' => 'checkAddrPart_', 'name' => 'address-part'), + array('occurrences' => '?', 'regex' => ':comparator', 'name' => 'comparator', + 'add' => array( + array('class' => 'string', 'occurrences' => '1', 'call' => 'setComparator_', 'values' => array( + array('occurrences' => '1', 'regex' => '"i;(octet|ascii-casemap)"', 'name' => 'comparator-string'), + array('occurrences' => '1', 'regex' => '"i;ascii-numeric"', 'requires' => 'comparator-i;ascii-numeric', 'name' => 'comparator-string') + )) + ) + ) + )), + array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array( + array('occurrences' => '+', 'regex' => '".*"', 'name' => 'header') + )), + array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array( + array('occurrences' => '+', 'regex' => '".*"', 'name' => 'key') + )) + ) + ); + break; + + case 'allof': + case 'anyof': + /* allof + anyof */ + $this->s_ = array( + 'valid_after' => array('if', 'elsif', 'anyof', 'allof', 'not'), + 'arguments' => array( + array('class' => 'left-parant', 'occurrences' => '1', 'values' => array( + array('occurrences' => '1', 'regex' => '\(', 'name' => 'test-list') + )), + array('class' => 'identifier', 'occurrences' => '+', 'values' => array( + array('occurrences' => '+', 'regex' => $this->testCommands_, 'name' => 'test') + )) + ) + ); + break; + + case 'envelope': + /* envelope [address-part: tag] [comparator: tag] [match-type: tag] */ + $this->s_ = array( + 'requires' => 'envelope', + 'valid_after' => array('if', 'elsif', 'anyof', 'allof', 'not'), + 'arguments' => array( + array('class' => 'tag', 'occurrences' => '*', 'post-call' => 'checkTags_', 'values' => array( + array('occurrences' => '?', 'regex' => ':(is|contains|matches|count|value)', 'call' => 'setMatchType_', 'name' => 'match-type'), + array('occurrences' => '?', 'regex' => ':(all|localpart|domain|user|detail)', 'call' => 'checkAddrPart_', 'name' => 'address-part'), + array('occurrences' => '?', 'regex' => ':comparator', 'name' => 'comparator', + 'add' => array( + array('class' => 'string', 'occurrences' => '1', 'call' => 'setComparator_', 'values' => array( + array('occurrences' => '1', 'regex' => '"i;(octet|ascii-casemap)"', 'name' => 'comparator-string'), + array('occurrences' => '1', 'regex' => '"i;ascii-numeric"', 'requires' => 'comparator-i;ascii-numeric', 'name' => 'comparator-string') + )) + ) + ) + )), + array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array( + array('occurrences' => '+', 'regex' => '".*"', 'name' => 'envelope-part') + )), + array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array( + array('occurrences' => '+', 'regex' => '".*"', 'name' => 'key') + )) + ) + ); + break; + + case 'exists': + /* exists */ + $this->s_ = array( + 'valid_after' => array('if', 'elsif', 'anyof', 'allof', 'not'), + 'arguments' => array( + array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array( + array('occurrences' => '+', 'regex' => '".*"', 'name' => 'header') + )) + ) + ); + break; + + case 'header': + /* header [comparator: tag] [match-type: tag] */ + $this->s_ = array( + 'valid_after' => array('if', 'elsif', 'anyof', 'allof', 'not'), + 'arguments' => array( + array('class' => 'tag', 'occurrences' => '*', 'post-call' => 'checkTags_', 'values' => array( + array('occurrences' => '?', 'regex' => ':(is|contains|matches|count|value)', 'call' => 'setMatchType_', 'name' => 'match-type'), + array('occurrences' => '?', 'regex' => ':comparator', 'name' => 'comparator', + 'add' => array( + array('class' => 'string', 'occurrences' => '1', 'call' => 'setComparator_', 'values' => array( + array('occurrences' => '1', 'regex' => '"i;(octet|ascii-casemap)"', 'name' => 'comparator-string'), + array('occurrences' => '1', 'regex' => '"i;ascii-numeric"', 'requires' => 'comparator-i;ascii-numeric', 'name' => 'comparator-string') + )) + ) + ) + )), + array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array( + array('occurrences' => '+', 'regex' => '".*"', 'name' => 'header') + )), + array('class' => 'string', 'list' => true, 'occurrences' => '1', 'values' => array( + array('occurrences' => '+', 'regex' => '".*"', 'name' => 'key') + )) + ) + ); + break; + + case 'not': + /* not */ + $this->s_ = array( + 'valid_after' => array('if', 'elsif', 'anyof', 'allof', 'not'), + 'arguments' => array( + array('class' => 'identifier', 'occurrences' => '1', 'values' => array( + array('occurrences' => '1', 'regex' => $this->testCommands_, 'name' => 'test') + )) + ) + ); + break; + + case 'size': + /* size <":over" / ":under"> */ + $this->s_ = array( + 'valid_after' => array('if', 'elsif', 'anyof', 'allof', 'not'), + 'arguments' => array( + array('class' => 'tag', 'occurrences' => '1', 'values' => array( + array('occurrences' => '1', 'regex' => ':(over|under)', 'name' => 'size-type') + )), + array('class' => 'number', 'occurrences' => '1', 'values' => array( + array('occurrences' => '1', 'regex' => '.*', 'name' => 'limit') + )) + ) + ); + break; + + case 'true': + case 'false': + /* true / false */ + $this->s_ = array( + 'valid_after' => array('if', 'elsif', 'anyof', 'allof', 'not') + ); + break; + + + /******************** + * unknown commands + */ + default: + $this->unknown = true; + } + } + + function setRequire_($text) + { + global $requires_; + array_push($requires_, $text); + return true; + } + + function setMatchType_($text) + { + // Do special processing for relational test extension + if ($text == ':count' || $text == ':value') + { + global $requires_; + if (!in_array('"relational"', $requires_)) + { + $this->message = 'missing require for match-type '. $text; + return false; + } + + array_unshift($this->s_['arguments'], + array('class' => 'string', 'occurrences' => '1', 'values' => array( + array('occurrences' => '1', 'regex' => '"(lt|le|eq|ge|gt|ne)"', 'name' => 'relation-string'), + )) + ); + } + $this->matchType_ = $text; + return true; + } + + function setComparator_($text) + { + $this->comparator_ = $text; + return true; + } + + function checkAddrPart_($text) + { + if ($text == ':user' || $text == ':detail') + { + global $requires_; + if (!in_array('"subaddress"', $requires_)) + { + $this->message = 'missing require for tag '. $text; + return false; + } + } + return true; + } + + function checkTags_() + { + if (isset($this->matchType_) && + $this->matchType_ == ':count' && + $this->comparator_ != '"i;ascii-numeric"') + { + $this->message = 'match-type :count needs comparator i;ascii-numeric'; + return false; + } + return true; + } + + function validAfter($prev) + { + return in_array($prev, $this->s_['valid_after']); + } + + function validClass_($class, $id) + { + // Check if command expects any arguments + if (!isset($this->s_['arguments'])) + { + $this->message = $id .' where semicolon expected'; + return false; + } + + foreach ($this->s_['arguments'] as $arg) + { + if ($class == $arg['class']) + { + return true; + } + + // Is the argument required + if ($arg['occurrences'] != '?' && $arg['occurrences'] != '*') + { + $this->message = $id .' where '. $arg['class'] .' expected'; + return false; + } + + if (isset($arg['post-call']) && + !call_user_func(array(&$this, $arg['post-call']))) + { + return false; + } + array_shift($this->s_['arguments']); + } + + $this->message = 'unexpected '. $id; + return false; + } + + function startStringList($line) + { + if (!$this->validClass_('string', 'string')) + { + $this->message = 'line '. $line .': '. $this->message; + return false; + } + else if (!isset($this->s_['arguments'][0]['list'])) + { + $this->message = 'line '. $line .': '. 'left bracket where '. $this->s_['arguments'][0]['class'] .' expected'; + return false; + } + + $this->s_['arguments'][0]['occurrences'] = '+'; + return true; + } + + function endStringList() + { + array_shift($this->s_['arguments']); + } + + function validToken($class, &$text, &$line) + { + $name = $class . ($class != $text ? " $text" : ''); + + // Check if the command needs to be required + // TODO: move this to somewhere more appropriate + global $requires_; + if (isset($this->s_['requires']) && + !in_array('"'.$this->s_['requires'].'"', $requires_)) + { + $this->message = 'line '. $line .': missing require for '. $this->command_; + return false; + } + + // Make sure the argument has a valid class + if (!$this->validClass_($class, $name)) + { + $this->message = 'line '. $line .': '. $this->message; + return false; + } + + $arg = &$this->s_['arguments'][0]; + foreach ($arg['values'] as $val) + { + if (preg_match('/^'. $val['regex'] .'$/m', $text)) + { + // Check if the argument value needs a 'require' + if (isset($val['requires']) && + !in_array('"'.$val['requires'].'"', $requires_)) + { + $this->message = 'line '. $line .': missing require for '. $val['name'] .' '. $text; + return false; + } + + // Check if a possible value of this argument may occur + if ($val['occurrences'] == '?' || $val['occurrences'] == '1') + { + $val['occurrences'] = '0'; + } + else if ($val['occurrences'] == '+') + { + $val['occurrences'] = '*'; + } + else if ($val['occurrences'] == '0') + { + $this->message = 'line '. $line .': too many '. $val['name'] .' '. $class .'s near '. $text; + return false; + } + + // Call extra processing function if defined + if (isset($val['call']) && !call_user_func(array(&$this, $val['call']), $text) || + isset($arg['call']) && !call_user_func(array(&$this, $arg['call']), $text)) + { + $this->message = 'line '. $line .': '. $this->message; + return false; + } + + // Set occurrences appropriately + if ($arg['occurrences'] == '?' || $arg['occurrences'] == '1') + { + array_shift($this->s_['arguments']); + } + else + { + $arg['occurrences'] = '*'; + } + + // Add argument(s) expected to follow right after this one + if (isset($val['add'])) + { + while ($add_arg = array_pop($val['add'])) + { + array_unshift($this->s_['arguments'], $add_arg); + } + } + + return true; + } + } + + $this->message = 'line '. $line .': unexpected '. $name; + return false; + } + + function done($class, $text, $line) + { + if (isset($this->s_['arguments'])) + { + foreach ($this->s_['arguments'] as $arg) + { + if ($arg['occurrences'] == '+' || $arg['occurrences'] == '1') + { + $this->message = 'line '. $line .': '. $class .' '. $text .' where '. $arg['class'] .' expected'; + return false; + } + } + } + return true; + } +} + +?> \ No newline at end of file diff --git a/include/class_sieve.inc b/include/sieve/class_sieve.inc similarity index 100% rename from include/class_sieve.inc rename to include/sieve/class_sieve.inc diff --git a/include/sieve/class_sievescript.php b/include/sieve/class_sievescript.php new file mode 100644 index 000000000..a2e831c80 --- /dev/null +++ b/include/sieve/class_sievescript.php @@ -0,0 +1,321 @@ + + + + + + + + + + + +
+ +
+ <:over|:under:1> + + + + + + +*/ + +class SieveScript +{ + var $_scanner; + var $_script; + var $_tree; + var $_status; + var $_noMoreRequires; + + var $_test_identifier = array('address', 'allof', 'anyof', 'envelope', 'exists', 'false', 'header', 'not', 'size', 'true'); + + var $status_text; + + function parse($script) + { + $this->status_text = "incomplete"; + + $root = 'script_start'; + $this->_noMoreRequires = false; + $this->_script = $script; + $this->_tree = new Tree($root); + $this->_tree->setDumpFunc(array($this, '_dumpToken')); + $this->_scanner = new Scanner($this->_script); + $this->_scanner->setCommentFunc(array($this, '_comment')); + + if ($this->_commands($this->_tree->getRoot()) && + $this->_scanner->nextTokenIs('script-end')) + { + return $this->_success('success'); + } + + return $this->_status; + } + + function _dumpToken(&$token) + { + if (is_array($token)) + { + $str = "<" . chop(mb_substr($this->_script, $token['pos'], $token['len'])) . "> "; + foreach ($token as $k => $v) + { + $str .= " $k:$v"; + } + return $str; + } + + return strval($token); + } + + function _tokenStringIs($token, $text) + { + return mb_substr($this->_script, $token['pos'], $token['len']) == $text; + } + + function _success($text = null) + { + if ($text != null) + { + $this->status_text = $text; + } + + return $this->_status = true; + } + + function _error($text, $token = null) + { + if ($token != null) + { + $text = 'line '. $token['line'] .': '. $token['class'] . " where $text expected near ". + '"'. mb_substr($this->_script, $token['pos'], $token['len']) .'"'; + } + + $this->status_text = $text; + return $this->_status = false; + } + + function _done() + { + return false; + } + + function _comment($token) + { + $this->_tree->addChild($token); + } + + function _commands($parent_id) + { + while ($this->_command($parent_id)); + + return $this->_status; + } + + function _command($parent_id) + { + if (!$this->_scanner->nextTokenIs('identifier')) + { + if ($this->_scanner->nextTokenIs(array('right-curly', 'script-end'))) + { + return $this->_done(); + } + return $this->_error('identifier', $this->_scanner->peekNextToken()); + } + + // Get and check a command token + $token = $this->_scanner->nextToken(); + $command = mb_substr($this->_script, $token['pos'], $token['len']); + + if (!in_array($command, array('if', 'elsif', 'else', 'require', 'stop', 'reject', 'fileinto', 'redirect', 'keep', 'discard'))) + { + return $this->_error('unknown command: '. $command); + } + + if ($command != 'require') + { + $this->_noMoreRequires = true; + } + else if ($this->_noMoreRequires) + { + return $this->_error('misplaced require'); + } + + $this_node = $this->_tree->addChildTo($parent_id, $token); + + if (in_array($command, array('if', 'elsif', 'require', 'reject', 'fileinto', 'redirect'))) + { + // TODO: handle optional arguments in reject + if ($this->_arguments($this_node) == false) + { + return false; + } + } + + $token = $this->_scanner->nextToken(); + if (in_array($command, array('if', 'elsif', 'else'))) + { + if ($token['class'] != 'left-curly') + { + return $this->_error('block', $token); + } + $this->_tree->addChildTo($this_node, $token); + return $this->_block($this_node); + } + else if ($token['class'] != 'semicolon') + { + return $this->_error('semicolon', $token); + } + + $this->_tree->addChildTo($this_node, $token); + return $this->_success(); + } + + function _block($parent_id) + { + //TODO: test if cmd is ok w/ block + if ($this->_commands($parent_id)) + { + $token = $this->_scanner->nextToken(); + + if ($token['class'] != 'right-curly') + { + return $this->_error('closing curly brace', $token); + } + + $this->_tree->addChildTo($parent_id, $token); + return $this->_success(); + } + return $this->_status; + } + + function _arguments($parent_id) + { + while ($this->_argument($parent_id)); + if ($this->_status == true) + { + $this->_testlist($parent_id); + } + return $this->_status; + } + + function _argument($parent_id) + { + if (!$this->_scanner->nextTokenIs(array('number', 'tag'))) + { + return $this->_stringlist($parent_id); + } + else + { + if ($this->_tokenStringIs($this->_tree->getNode($parent_id), 'require')) + { + return $this->_error('stringlist', $this->_scanner->nextToken()); + } + } + + $token = $this->_scanner->nextToken(); + $this->_tree->addChildTo($parent_id, $token); + + return $this->_success(); + } + + function _test($parent_id) + { + if (!$this->_scanner->nextTokenIs('identifier')) + { + return $this->_done(); + } + + $token = $this->_scanner->nextToken(); + $this_node = $this->_tree->addChildTo($parent_id, $token); + + $this->_arguments($this_node); + + //TODO: check test for validity here + + return $this->_status; + } + + function _testlist($parent_id) + { + if (!$this->_scanner->nextTokenIs('left-parant')) + { + return $this->_test($parent_id); + } + + $token = $this->_scanner->nextToken(); + $this->_tree->addChildTo($parent_id, $token); + + while ($token['class'] != 'right-parant') + { + if (!$this->_test($parent_id)) + { + return $this->_status; + } + + $token = $this->_scanner->nextToken(); + + if ($token['class'] != 'comma' && $token['class'] != 'right-parant') + { + return $this->_error('comma or closing paranthesis', $token); + } + + $this->_tree->addChildTo($parent_id, $token); + } + + return $this->_success(); + } + + function _string($parent_id) + { + if (!$this->_scanner->nextTokenIs(array('quoted-string', 'multi-line'))) + { + return $this->_done(); + } + + $this->_tree->addChildTo($parent_id, $this->_scanner->nextToken()); + + return $this->_success(); + } + + function _stringlist($parent_id) + { + if (!$this->_scanner->nextTokenIs('left-bracket')) + { + return $this->_string($parent_id); + } + + $token = $this->_scanner->nextToken(); + $this->_tree->addChildTo($parent_id, $token); + + while ($token['class'] != 'right-bracket') + { + if (!$this->_string($parent_id)) + { + return $this->_status; + } + + $token = $this->_scanner->nextToken(); + + if ($token['class'] != 'comma' && $token['class'] != 'right-bracket') + { + return $this->_error('comma or closing bracket', $token); + } + + $this->_tree->addChildTo($parent_id, $token); + } + + return $this->_success(); + } +} + +?> diff --git a/include/sieve/class_tree.inc b/include/sieve/class_tree.inc new file mode 100644 index 000000000..96bc7a822 --- /dev/null +++ b/include/sieve/class_tree.inc @@ -0,0 +1,175 @@ +_construct($root); + } + + function _construct(&$root) + { + $this->childs_ = array(); + $this->parents_ = array(); + $this->nodes_ = array(); + $this->maxId_ = 0; + + $this->parents_[0] = null; + $this->nodes_[0] = $root; + } + + function addChild(&$child) + { + return $this->addChildTo($this->maxId_, $child); + } + + function addChildTo($parent_id, &$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->maxId_; + $this->nodes_[$child_id] = $child; + $this->parents_[$child_id] = $parent_id; + array_push($this->childs_[$parent_id], $child_id); + + return $child_id; + } + + function getRoot() + { + if (!isset($this->nodes_[0])) + { + return null; + } + + return 0; + } + + function getParent($node_id) + { + if (!is_int($node_id) || + !isset($this->nodes_[$node_id])) + { + return null; + } + + return $this->parents_[$node_id]; + } + + 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]; + } + + function getNode($node_id) + { + if (!is_int($node_id) || + !isset($this->nodes_[$node_id])) + { + return null; + } + + return $this->nodes_[$node_id]; + } + + function getLastNode($parent_id) + { + $childs = $this->getChilds($parent_id); + + for ($i=count($childs); $i>0; --$i) + { + $node = $this->getNode($childs[$i-1]); + if ($node['text'] == '{') + { + // return command owning the block + return $this->getNode($parent_id); + } + if ($node['class'] != 'comment') + { + return $node; + } + } + + return $this->getNode($parent_id); + } + + function setDumpFunc($callback) + { + if ($callback == NULL || is_callable($callback)) + { + $this->dumpFn_ = $callback; + } + } + + function dump() + { + $this->dump_ = "tree\n"; + $this->doDump_(0, '', true); + return $this->dump_; + } + + function doDump_($node_id, $prefix, $last) + { + if ($last) + { + $infix = '`--- '; + $c_prefix = $prefix . ' '; + } + else + { + $infix = '|--- '; + $c_prefix = $prefix . '| '; + } + + $node = $this->nodes_[$node_id]; + if ($this->dumpFn_ != NULL) + { + $this->dump_ .= $prefix . $infix . call_user_func($this->dumpFn_, $node) . "\n"; + } + else + { + $this->dump_ .= "$prefix$infix$node\n"; + } + + $childs = $this->childs_[$node_id]; + for ($i=0; $idoDump_($childs[$i], $c_prefix, $c_last); + } + } +} + +?> \ No newline at end of file diff --git a/include/sieve/libsieve.php b/include/sieve/libsieve.php new file mode 100644 index 000000000..c438a9ce2 --- /dev/null +++ b/include/sieve/libsieve.php @@ -0,0 +1,7 @@ + diff --git a/plugins/personal/mail/class_mailAccount.inc b/plugins/personal/mail/class_mailAccount.inc index 468e4ab7e..9dcd06822 100644 --- a/plugins/personal/mail/class_mailAccount.inc +++ b/plugins/personal/mail/class_mailAccount.inc @@ -10,7 +10,7 @@ */ /* Load sieve support */ -require_once ("class_sieve.inc"); +require_once ("sieve/libsieve.php"); /* Load mail methods */ global $BASE_DIR; -- 2.30.2