Code

Moved sieve stuff into special folder
authorhickert <hickert@594d385d-05f5-0310-b6e9-bd551577e9d8>
Thu, 15 Feb 2007 11:27:08 +0000 (11:27 +0000)
committerhickert <hickert@594d385d-05f5-0310-b6e9-bd551577e9d8>
Thu, 15 Feb 2007 11:27:08 +0000 (11:27 +0000)
git-svn-id: https://oss.gonicus.de/repositories/gosa/trunk@5722 594d385d-05f5-0310-b6e9-bd551577e9d8

include/class_sieve.inc [deleted file]
include/sieve/class_parser.inc [new file with mode: 0644]
include/sieve/class_scanner.inc [new file with mode: 0644]
include/sieve/class_semantics.inc [new file with mode: 0644]
include/sieve/class_sieve.inc [new file with mode: 0644]
include/sieve/class_sievescript.php [new file with mode: 0644]
include/sieve/class_tree.inc [new file with mode: 0644]
include/sieve/libsieve.php [new file with mode: 0644]
plugins/personal/mail/class_mailAccount.inc

diff --git a/include/class_sieve.inc b/include/class_sieve.inc
deleted file mode 100644 (file)
index bdbf7d6..0000000
+++ /dev/null
@@ -1,514 +0,0 @@
-<?
-
-/*
- * $Id: class_sieve.inc,v 1.1 2005/02/21 09:33:01 cajus Exp $ 
- *
- * Copyright 2001 Dan Ellis <danellis@rushmore.com>
- *
- * See the enclosed file COPYING for license information (GPL).  If you
- * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
- */
-
-// TODO before next release:   remove ::status() and dependencies
-
-
-define ("F_NO", 0);            
-define ("F_OK", 1);
-define ("F_DATA", 2);
-define ("F_HEAD", 3);
-
-define ("EC_NOT_LOGGED_IN", 0);
-define ("EC_QUOTA", 10);
-define ("EC_NOSCRIPTS", 20);
-define ("EC_UNKNOWN", 255);
-/*
-
-SIEVE-PHP.LIB VERSION 0.0.8
-
-(C) 2001 Dan Ellis.
-
-PLEASE READ THE README FILE FOR MORE INFORMATION.
-
-Basically, this is the first re-release.  Things are much better than before.
-
-Notes:
-This program/libary has bugs.
-       .       This was quickly hacked out, so please let me know what is wrong and if you feel ambitious submit
-               a patch :).
-
-Todo:
-       .       Provide better error diagnostics.                       (mostly done with ver 0.0.5)
-       .       Allow other auth mechanisms besides plain               (in progress)
-       .       Have timing mechanism when port problems arise.         (not done yet)
-       .       Maybe add the NOOP function.                            (not done yet)
-       .       Other top secret stuff....                              (some done, believe me?)
-
-Dan Ellis (danellis@rushmore.com)
-
-This program is released under the GNU Public License.
-
-You should have received a copy of the GNU Public
- License along with this package; if not, write to the
- Free Software Foundation, Inc., 59 Temple Place - Suite 330,
- Boston, MA 02111-1307, USA.        
-
-See CHANGES for updates since last release
-
-Contributers of patches:
-       Atif Ghaffar
-       Andrew Sterling Hanenkamp <sterling@hanenkamp.com>
-*/
-
-
-class sieve
-{
-  var $host;
-  var $port;
-  var $user;
-  var $pass;
-  var $auth_types;             /* a comma seperated list of allowed auth types, in order of preference */
-  var $auth_in_use;            /* type of authentication attempted */
-  
-  var $line;
-  var $fp;
-  var $retval;
-  var $tmpfile;
-  var $fh;
-  var $len;
-  var $script;
-
-  var $loggedin;
-  var $capabilities;
-  var $error;
-  var $error_raw;
-  var $responses;
-
-  //maybe we should add an errorlvl that the user will pass to new sieve = sieve(,,,,E_WARN)
-  //so we can decide how to handle certain errors?!?
-
-  //also add a connection type, like PLAIN, MD5, etc...
-
-
-  function get_response()
-  {
-    if($this->loggedin == false or feof($this->fp)){
-        $this->error = EC_NOT_LOGGED_IN;
-        $this->error_raw = "You are not logged in.";
-        return false;
-    }
-
-    unset($this->response);
-    unset($this->error);
-    unset($this->error_raw);
-
-    $this->line=fgets($this->fp,1024);
-    $this->token = split(" ", $this->line, 2);
-
-    if($this->token[0] == "NO"){
-        /* we need to try and extract the error code from here.  There are two possibilites: one, that it will take the form of:
-           NO ("yyyyy") "zzzzzzz" or, two, NO {yyyyy} "zzzzzzzzzzz" */
-        $this->x = 0;
-        list($this->ltoken, $this->mtoken, $this->rtoken) = split(" ", $this->line." ", 3);
-        if($this->mtoken[0] == "{"){
-            while($this->mtoken[$this->x] != "}" or $this->err_len < 1){
-                $this->err_len = substr($this->mtoken, 1, $this->x);
-                $this->x++;    
-            }
-            //print "<br>Trying to receive $this->err_len bytes for result<br>";
-            $this->line = fgets($this->fp,$this->err_len);
-            $this->error_raw[]=substr($this->line, 0, strlen($this->line) -2);    //we want to be nice and strip crlf's
-            $this->err_recv = strlen($this->line);
-
-            while($this->err_recv < $this->err_len){
-                //print "<br>Trying to receive ".($this->err_len-$this->err_recv)." bytes for result<br>";
-                $this->line = fgets($this->fp, ($this->err_len-$this->err_recv));
-                $this->error_raw[]=substr($this->line, 0, strlen($this->line) -2);    //we want to be nice and strip crlf's
-                $this->err_recv += strlen($this->line);
-            } /* end while */
-            $this->line = fgets($this->fp, 1024);      //we need to grab the last crlf, i think.  this may be a bug...
-            $this->error=EC_UNKNOWN;
-      
-        } /* end if */
-        elseif($this->mtoken[0] == "("){
-            switch($this->mtoken){
-                case "(\"QUOTA\")":
-                    $this->error = EC_QUOTA;
-                    $this->error_raw=$this->rtoken;
-                    break;
-                default:
-                    $this->error = EC_UNKNOWN;
-                    $this->error_raw=$this->rtoken;
-                    break;
-            } /* end switch */
-        } /* end elseif */
-        else{
-            $this->error = EC_UNKNOWN;
-            $this->error_raw = $this->line;
-        }     
-        return false;
-
-    } /* end if */
-    elseif(substr($this->token[0],0,-2) == "OK"){
-         return true;
-    } /* end elseif */
-    elseif($this->token[0][0] == "{"){
-        
-        /* Unable wild assumption:  that the only function that gets here is the get_script(), doesn't really matter though */       
-
-        /* the first line is the len field {xx}, which we don't care about at this point */
-        $this->line = fgets($this->fp,1024);
-        while(substr($this->line,0,2) != "OK" and substr($this->line,0,2) != "NO"){
-            $this->response[]=$this->line;
-            $this->line = fgets($this->fp, 1024);
-        }
-        if(substr($this->line,0,2) == "OK")
-            return true;
-        else
-            return false;
-    } /* end elseif */
-    elseif($this->token[0][0] == "\""){
-
-        /* I'm going under the _assumption_ that the only function that will get here is the listscripts().
-           I could very well be mistaken here, if I am, this part needs some rework */
-
-        $this->found_script=false;        
-
-        while(substr($this->line,0,2) != "OK" and substr($this->line,0,2) != "NO"){
-            $this->found_script=true;
-            list($this->ltoken, $this->rtoken) = explode(" ", $this->line." ",2);
-               //hmmm, a bug in php, if there is no space on explode line, a warning is generated...
-           
-            if(strcmp(rtrim($this->rtoken), "ACTIVE")==0){
-                $this->response["ACTIVE"] = substr(rtrim($this->ltoken),1,-1);  
-            }
-            else
-                $this->response[] = substr(rtrim($this->ltoken),1,-1);
-            $this->line = fgets($this->fp, 1024);
-        } /* end while */
-        
-        return true;
-        
-    } /* end elseif */
-    else{
-            $this->error = EC_UNKNOWN;
-            $this->error_raw = $this->line;
-            print "<b><i>UNKNOWN ERROR (Please report this line to danellis@rushmore.com to include in future releases): $this->line</i></b><br>";
-            return false;
-    } /* end else */   
-  } /* end get_response() */
-
-  function sieve($host, $port, $user, $pass, $auth="", $auth_types="PLAIN DIGEST-MD5")
-  {
-    $this->host=$host;
-    $this->port=$port;
-    $this->user=$user;
-    $this->pass=$pass;
-    if(!strcmp($auth, ""))             /* If there is no auth user, we deem the user itself to be the auth'd user */
-        $this->auth = $this->user;
-    else
-        $this->auth = $auth;
-    $this->auth_types=$auth_types;     /* Allowed authentication types */
-    $this->fp=0;
-    $this->line="";
-    $this->retval="";
-    $this->tmpfile="";
-    $this->fh=0;
-    $this->len=0;
-    $this->capabilities="";
-    $this->loggedin=false;
-    $this->error= "";
-    $this->error_raw="";
-  }
-
-  function parse_for_quotes($string)
-  {
-      /* This function tokenizes a line of input by quote marks and returns them as an array */
-
-      $start = -1;
-      $index = 0;
-
-      for($ptr = 0; $ptr < strlen($string); $ptr++){
-          if($string[$ptr] == '"' and $string[$ptr] != '\\'){
-              if($start == -1){
-                  $start = $ptr;
-              } /* end if */
-              else{
-                  $token[$index++] = substr($string, $start + 1, $ptr - $start - 1);
-                  $found = true;
-                  $start = -1;
-              } /* end else */
-
-          } /* end if */  
-
-      } /* end for */
-
-      if(isset($token))
-          return $token;
-      else
-          return false;
-  } /* end function */            
-
-  function status($string)
-  {
-      //this should probably be replaced by a smarter parser.
-
-      /*  Need to remove this and all dependencies from the class */
-
-      switch (substr($string, 0,2)){
-          case "NO":
-              return F_NO;             //there should be some function to extract the error code from this line
-                                       //NO ("quota") "You are oly allowed x number of scripts"
-              break;
-          case "OK":
-              return F_OK;
-              break;
-          default:
-              switch ($string[0]){
-                  case "{":
-                      //do parse here for {}'s  maybe modify parse_for_quotes to handle any parse delimiter?
-                      return F_HEAD;
-                      break;
-                  default:
-                      return F_DATA;
-                      break;
-              } /* end switch */
-        } /* end switch */
-  } /* end status() */
-
-  function sieve_login()
-  {
-
-    $this->fp=fsockopen($this->host,$this->port);
-    if($this->fp == false)
-        return false;
-    $this->line=fgets($this->fp,1024);
-
-    //Hack for older versions of Sieve Server.  They do not respond with the Cyrus v2. standard
-    //response.  They repsond as follows: "Cyrus timsieved v1.0.0" "SASL={PLAIN,........}"
-    //So, if we see IMLEMENTATION in the first line, then we are done.
-
-    if(ereg("IMPLEMENTATION",$this->line))
-    {
-      //we're on the Cyrus V2 sieve server
-      while(sieve::status($this->line) == F_DATA){
-
-          $this->item = sieve::parse_for_quotes($this->line);
-
-          if(strcmp($this->item[0], "IMPLEMENTATION") == 0)
-              $this->capabilities["implementation"] = $this->item[1];
-        
-          elseif(strcmp($this->item[0], "SIEVE") == 0 or strcmp($this->item[0], "SASL") == 0){
-
-              if(strcmp($this->item[0], "SIEVE") == 0)
-                  $this->cap_type="modules";
-              else
-                  $this->cap_type="auth";            
-
-              $this->modules = split(" ", $this->item[1]);
-              if(is_array($this->modules)){
-                  foreach($this->modules as $this->module)
-                      $this->capabilities[$this->cap_type][$this->module]=true;
-              } /* end if */
-              elseif(is_string($this->modules))
-                  $this->capabilites[$this->cap_type][$this->modules]=true;
-          }    
-          else{ 
-              $this->capabilities["unknown"][]=$this->line;
-          }    
-      $this->line=fgets($this->fp,1024);
-
-       }// end while
-    }
-    else
-    {
-        //we're on the older Cyrus V1. server  
-        //this version does not support module reporting.  We only have auth types.
-        $this->cap_type="auth";
-       
-        //break apart at the "Cyrus timsieve...." "SASL={......}"
-        $this->item = sieve::parse_for_quotes($this->line);
-
-        $this->capabilities["implementation"] = $this->item[0];
-
-        //we should have "SASL={..........}" now.  Break out the {xx,yyy,zzzz}
-        $this->modules = substr($this->item[1], strpos($this->item[1], "{"),strlen($this->item[1])-1);
-
-        //then split again at the ", " stuff.
-        $this->modules = split($this->modules, ", ");
-        //fill up our $this->modules property
-        if(is_array($this->modules)){
-            foreach($this->modules as $this->module)
-                $this->capabilities[$this->cap_type][$this->module]=true;
-        } /* end if */
-        elseif(is_string($this->modules))
-            $this->capabilites[$this->cap_type][$this->module]=true;
-    }
-
-
-
-
-    if(sieve::status($this->line) == F_NO){            //here we should do some returning of error codes?
-        $this->error=EC_UNKNOWN;
-        $this->error_raw = "Server not allowing connections.";
-        return false;
-    }
-
-    /* decision login to decide what type of authentication to use... */
-
-     /* Loop through each allowed authentication type and see if the server allows the type */
-     foreach(split(" ",$this->auth_types) as $auth_type)
-     {
-        if ($this->capabilities["auth"][$auth_type])
-        {
-            /* We found an auth type that is allowed. */
-            $this->auth_in_use = $auth_type;
-            break;
-        }
-     }
-    
-    /* Fill error message if no auth types are present */
-    if (!isset($this->capabilities["auth"])){
-        $this->error=EC_UNKNOWN;
-       $this->error_raw = "No authentication methods found - please check your sieve setup for missing sasl modules";
-       return false;
-    }
-
-     /* call our authentication program */  
-    return sieve::authenticate();
-
-  }
-
-  function sieve_logout()
-  {
-    if($this->loggedin==false)
-        return false;
-
-    fputs($this->fp,"LOGOUT\r\n");
-    fclose($this->fp);
-    $this->loggedin=false;
-    return true;
-  }
-
-  function sieve_sendscript($scriptname, $script)
-  {
-    if($this->loggedin==false)
-        return false;
-    $this->script=stripslashes($script);
-    $len=strlen($this->script);
-    fputs($this->fp, "PUTSCRIPT \"$scriptname\" {".$len."+}\r\n");
-    fputs($this->fp, "$this->script\r\n");
-  
-    return sieve::get_response();
-
-  }  
-  
-  //it appears the timsieved does not honor the NUMBER type.  see lex.c in timsieved src.
-  //don't expect this function to work yet.  I might have messed something up here, too.
-  function sieve_havespace($scriptname, $scriptsize)
-  {
-    if($this->loggedin==false)
-        return false;
-    fputs($this->fp, "HAVESPACE \"$scriptname\" $scriptsize\r\n");
-    return sieve::get_response();
-
-  }  
-
-  function sieve_setactivescript($scriptname)
-  {
-    if($this->loggedin==false)
-        return false;
-
-    fputs($this->fp, "SETACTIVE \"$scriptname\"\r\n");   
-    return sieve::get_response();
-
-  }
-  
-  function sieve_getscript($scriptname)
-  {
-    unset($this->script);
-    if($this->loggedin==false)
-        return false;
-
-    fputs($this->fp, "GETSCRIPT \"$scriptname\"\r\n");
-    return sieve::get_response();
-   
-  }
-
-
-  function sieve_deletescript($scriptname)
-  {
-    if($this->loggedin==false)
-        return false;
-
-    fputs($this->fp, "DELETESCRIPT \"$scriptname\"\r\n");    
-
-    return sieve::get_response();
-  }
-
-  
-  function sieve_listscripts() 
-   { 
-     fputs($this->fp, "LISTSCRIPTS\r\n"); 
-     sieve::get_response();            //should always return true, even if there are no scripts...
-     if(isset($this->found_script) and $this->found_script)
-         return true;
-     else{
-         $this->error=EC_NOSCRIPTS;    //sieve::getresponse has no way of telling wether a script was found...
-         $this->error_raw="No scripts found for this account.";
-         return false;
-     }
-   }
-
-  function sieve_alive()
-  {
-      if(!isset($this->fp) or $this->fp==0){
-          $this->error = EC_NOT_LOGGED_IN;
-          return false;
-      }
-      elseif(feof($this->fp)){                 
-          $this->error = EC_NOT_LOGGED_IN;
-          return false;
-      }
-      else
-          return true;
-  }
-
-  function authenticate()
-  {
-
-    switch ($this->auth_in_use) {
-    
-        case "PLAIN":
-            $auth=base64_encode("$this->user\0$this->auth\0$this->pass");
-   
-            $this->len=strlen($auth);                  
-            fputs($this->fp, "AUTHENTICATE \"PLAIN\" {".$this->len."+}\r\n");
-            fputs($this->fp, "$auth\r\n");
-
-            $this->line=fgets($this->fp,1024);         
-            while(sieve::status($this->line) == F_DATA)
-               $this->line=fgets($this->fp,1024);
-
-             if(sieve::status($this->line) == F_NO)
-               return false;
-             $this->loggedin=true;
-               return true;    
-             break;
-
-        default:
-            return false;
-            break;
-
-    }//end switch
-
-
-  }//end authenticate()
-
-
-}
-
-
-
-?>
diff --git a/include/sieve/class_parser.inc b/include/sieve/class_parser.inc
new file mode 100644 (file)
index 0000000..7ca1f9e
--- /dev/null
@@ -0,0 +1,319 @@
+<?php
+class Parser
+{
+       var $scanner_;
+       var $script_;
+       var $tree_;
+       var $status_;
+
+       var $status_text;
+
+       function parse($script)
+       {
+               $this->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 (file)
index 0000000..3e22bb1
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+
+class Scanner
+{
+       function Scanner(&$script)
+       {
+               $this->_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 (file)
index 0000000..e4b1f4c
--- /dev/null
@@ -0,0 +1,553 @@
+<?php
+
+$requires_ = array();
+
+class Semantics
+{
+       var $command_;
+       var $comparator_;
+       var $matchType_;
+       var $s_;
+       var $unknown;
+       var $message;
+       var $testCommands_ = '(address|envelope|header|size|allof|anyof|exists|not|true|false)';
+       var $requireStrings_ = '(envelope|fileinto|reject|vacation|relational|subaddress)';
+
+       function Semantics($command)
+       {
+               $this->command_ = $command;
+               $this->unknown = false;
+               switch ($command)
+               {
+
+               /********************
+                * control commands
+                */
+               case 'require':
+                       /* require <capabilities: string-list> */
+                       $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 <test> <block> */
+                       $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 <test> <block> */
+                       $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 <block> */
+                       $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 <folder: string> */
+                       $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 <address: string> */
+                       $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 <reason: string> */
+                       $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"] <reason: string> */
+                       $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] <header-list: string-list> <key-list: string-list> */
+                       $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 <tests: test-list>
+                          anyof <tests: test-list> */
+                       $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] <envelope-part: string-list> <key-list: string-list> */
+                       $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 <header-names: string-list> */
+                       $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] <header-names: string-list> <key-list: string-list> */
+                       $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 <test> */
+                       $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"> <limit: number> */
+                       $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/sieve/class_sieve.inc b/include/sieve/class_sieve.inc
new file mode 100644 (file)
index 0000000..bdbf7d6
--- /dev/null
@@ -0,0 +1,514 @@
+<?
+
+/*
+ * $Id: class_sieve.inc,v 1.1 2005/02/21 09:33:01 cajus Exp $ 
+ *
+ * Copyright 2001 Dan Ellis <danellis@rushmore.com>
+ *
+ * See the enclosed file COPYING for license information (GPL).  If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ */
+
+// TODO before next release:   remove ::status() and dependencies
+
+
+define ("F_NO", 0);            
+define ("F_OK", 1);
+define ("F_DATA", 2);
+define ("F_HEAD", 3);
+
+define ("EC_NOT_LOGGED_IN", 0);
+define ("EC_QUOTA", 10);
+define ("EC_NOSCRIPTS", 20);
+define ("EC_UNKNOWN", 255);
+/*
+
+SIEVE-PHP.LIB VERSION 0.0.8
+
+(C) 2001 Dan Ellis.
+
+PLEASE READ THE README FILE FOR MORE INFORMATION.
+
+Basically, this is the first re-release.  Things are much better than before.
+
+Notes:
+This program/libary has bugs.
+       .       This was quickly hacked out, so please let me know what is wrong and if you feel ambitious submit
+               a patch :).
+
+Todo:
+       .       Provide better error diagnostics.                       (mostly done with ver 0.0.5)
+       .       Allow other auth mechanisms besides plain               (in progress)
+       .       Have timing mechanism when port problems arise.         (not done yet)
+       .       Maybe add the NOOP function.                            (not done yet)
+       .       Other top secret stuff....                              (some done, believe me?)
+
+Dan Ellis (danellis@rushmore.com)
+
+This program is released under the GNU Public License.
+
+You should have received a copy of the GNU Public
+ License along with this package; if not, write to the
+ Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ Boston, MA 02111-1307, USA.        
+
+See CHANGES for updates since last release
+
+Contributers of patches:
+       Atif Ghaffar
+       Andrew Sterling Hanenkamp <sterling@hanenkamp.com>
+*/
+
+
+class sieve
+{
+  var $host;
+  var $port;
+  var $user;
+  var $pass;
+  var $auth_types;             /* a comma seperated list of allowed auth types, in order of preference */
+  var $auth_in_use;            /* type of authentication attempted */
+  
+  var $line;
+  var $fp;
+  var $retval;
+  var $tmpfile;
+  var $fh;
+  var $len;
+  var $script;
+
+  var $loggedin;
+  var $capabilities;
+  var $error;
+  var $error_raw;
+  var $responses;
+
+  //maybe we should add an errorlvl that the user will pass to new sieve = sieve(,,,,E_WARN)
+  //so we can decide how to handle certain errors?!?
+
+  //also add a connection type, like PLAIN, MD5, etc...
+
+
+  function get_response()
+  {
+    if($this->loggedin == false or feof($this->fp)){
+        $this->error = EC_NOT_LOGGED_IN;
+        $this->error_raw = "You are not logged in.";
+        return false;
+    }
+
+    unset($this->response);
+    unset($this->error);
+    unset($this->error_raw);
+
+    $this->line=fgets($this->fp,1024);
+    $this->token = split(" ", $this->line, 2);
+
+    if($this->token[0] == "NO"){
+        /* we need to try and extract the error code from here.  There are two possibilites: one, that it will take the form of:
+           NO ("yyyyy") "zzzzzzz" or, two, NO {yyyyy} "zzzzzzzzzzz" */
+        $this->x = 0;
+        list($this->ltoken, $this->mtoken, $this->rtoken) = split(" ", $this->line." ", 3);
+        if($this->mtoken[0] == "{"){
+            while($this->mtoken[$this->x] != "}" or $this->err_len < 1){
+                $this->err_len = substr($this->mtoken, 1, $this->x);
+                $this->x++;    
+            }
+            //print "<br>Trying to receive $this->err_len bytes for result<br>";
+            $this->line = fgets($this->fp,$this->err_len);
+            $this->error_raw[]=substr($this->line, 0, strlen($this->line) -2);    //we want to be nice and strip crlf's
+            $this->err_recv = strlen($this->line);
+
+            while($this->err_recv < $this->err_len){
+                //print "<br>Trying to receive ".($this->err_len-$this->err_recv)." bytes for result<br>";
+                $this->line = fgets($this->fp, ($this->err_len-$this->err_recv));
+                $this->error_raw[]=substr($this->line, 0, strlen($this->line) -2);    //we want to be nice and strip crlf's
+                $this->err_recv += strlen($this->line);
+            } /* end while */
+            $this->line = fgets($this->fp, 1024);      //we need to grab the last crlf, i think.  this may be a bug...
+            $this->error=EC_UNKNOWN;
+      
+        } /* end if */
+        elseif($this->mtoken[0] == "("){
+            switch($this->mtoken){
+                case "(\"QUOTA\")":
+                    $this->error = EC_QUOTA;
+                    $this->error_raw=$this->rtoken;
+                    break;
+                default:
+                    $this->error = EC_UNKNOWN;
+                    $this->error_raw=$this->rtoken;
+                    break;
+            } /* end switch */
+        } /* end elseif */
+        else{
+            $this->error = EC_UNKNOWN;
+            $this->error_raw = $this->line;
+        }     
+        return false;
+
+    } /* end if */
+    elseif(substr($this->token[0],0,-2) == "OK"){
+         return true;
+    } /* end elseif */
+    elseif($this->token[0][0] == "{"){
+        
+        /* Unable wild assumption:  that the only function that gets here is the get_script(), doesn't really matter though */       
+
+        /* the first line is the len field {xx}, which we don't care about at this point */
+        $this->line = fgets($this->fp,1024);
+        while(substr($this->line,0,2) != "OK" and substr($this->line,0,2) != "NO"){
+            $this->response[]=$this->line;
+            $this->line = fgets($this->fp, 1024);
+        }
+        if(substr($this->line,0,2) == "OK")
+            return true;
+        else
+            return false;
+    } /* end elseif */
+    elseif($this->token[0][0] == "\""){
+
+        /* I'm going under the _assumption_ that the only function that will get here is the listscripts().
+           I could very well be mistaken here, if I am, this part needs some rework */
+
+        $this->found_script=false;        
+
+        while(substr($this->line,0,2) != "OK" and substr($this->line,0,2) != "NO"){
+            $this->found_script=true;
+            list($this->ltoken, $this->rtoken) = explode(" ", $this->line." ",2);
+               //hmmm, a bug in php, if there is no space on explode line, a warning is generated...
+           
+            if(strcmp(rtrim($this->rtoken), "ACTIVE")==0){
+                $this->response["ACTIVE"] = substr(rtrim($this->ltoken),1,-1);  
+            }
+            else
+                $this->response[] = substr(rtrim($this->ltoken),1,-1);
+            $this->line = fgets($this->fp, 1024);
+        } /* end while */
+        
+        return true;
+        
+    } /* end elseif */
+    else{
+            $this->error = EC_UNKNOWN;
+            $this->error_raw = $this->line;
+            print "<b><i>UNKNOWN ERROR (Please report this line to danellis@rushmore.com to include in future releases): $this->line</i></b><br>";
+            return false;
+    } /* end else */   
+  } /* end get_response() */
+
+  function sieve($host, $port, $user, $pass, $auth="", $auth_types="PLAIN DIGEST-MD5")
+  {
+    $this->host=$host;
+    $this->port=$port;
+    $this->user=$user;
+    $this->pass=$pass;
+    if(!strcmp($auth, ""))             /* If there is no auth user, we deem the user itself to be the auth'd user */
+        $this->auth = $this->user;
+    else
+        $this->auth = $auth;
+    $this->auth_types=$auth_types;     /* Allowed authentication types */
+    $this->fp=0;
+    $this->line="";
+    $this->retval="";
+    $this->tmpfile="";
+    $this->fh=0;
+    $this->len=0;
+    $this->capabilities="";
+    $this->loggedin=false;
+    $this->error= "";
+    $this->error_raw="";
+  }
+
+  function parse_for_quotes($string)
+  {
+      /* This function tokenizes a line of input by quote marks and returns them as an array */
+
+      $start = -1;
+      $index = 0;
+
+      for($ptr = 0; $ptr < strlen($string); $ptr++){
+          if($string[$ptr] == '"' and $string[$ptr] != '\\'){
+              if($start == -1){
+                  $start = $ptr;
+              } /* end if */
+              else{
+                  $token[$index++] = substr($string, $start + 1, $ptr - $start - 1);
+                  $found = true;
+                  $start = -1;
+              } /* end else */
+
+          } /* end if */  
+
+      } /* end for */
+
+      if(isset($token))
+          return $token;
+      else
+          return false;
+  } /* end function */            
+
+  function status($string)
+  {
+      //this should probably be replaced by a smarter parser.
+
+      /*  Need to remove this and all dependencies from the class */
+
+      switch (substr($string, 0,2)){
+          case "NO":
+              return F_NO;             //there should be some function to extract the error code from this line
+                                       //NO ("quota") "You are oly allowed x number of scripts"
+              break;
+          case "OK":
+              return F_OK;
+              break;
+          default:
+              switch ($string[0]){
+                  case "{":
+                      //do parse here for {}'s  maybe modify parse_for_quotes to handle any parse delimiter?
+                      return F_HEAD;
+                      break;
+                  default:
+                      return F_DATA;
+                      break;
+              } /* end switch */
+        } /* end switch */
+  } /* end status() */
+
+  function sieve_login()
+  {
+
+    $this->fp=fsockopen($this->host,$this->port);
+    if($this->fp == false)
+        return false;
+    $this->line=fgets($this->fp,1024);
+
+    //Hack for older versions of Sieve Server.  They do not respond with the Cyrus v2. standard
+    //response.  They repsond as follows: "Cyrus timsieved v1.0.0" "SASL={PLAIN,........}"
+    //So, if we see IMLEMENTATION in the first line, then we are done.
+
+    if(ereg("IMPLEMENTATION",$this->line))
+    {
+      //we're on the Cyrus V2 sieve server
+      while(sieve::status($this->line) == F_DATA){
+
+          $this->item = sieve::parse_for_quotes($this->line);
+
+          if(strcmp($this->item[0], "IMPLEMENTATION") == 0)
+              $this->capabilities["implementation"] = $this->item[1];
+        
+          elseif(strcmp($this->item[0], "SIEVE") == 0 or strcmp($this->item[0], "SASL") == 0){
+
+              if(strcmp($this->item[0], "SIEVE") == 0)
+                  $this->cap_type="modules";
+              else
+                  $this->cap_type="auth";            
+
+              $this->modules = split(" ", $this->item[1]);
+              if(is_array($this->modules)){
+                  foreach($this->modules as $this->module)
+                      $this->capabilities[$this->cap_type][$this->module]=true;
+              } /* end if */
+              elseif(is_string($this->modules))
+                  $this->capabilites[$this->cap_type][$this->modules]=true;
+          }    
+          else{ 
+              $this->capabilities["unknown"][]=$this->line;
+          }    
+      $this->line=fgets($this->fp,1024);
+
+       }// end while
+    }
+    else
+    {
+        //we're on the older Cyrus V1. server  
+        //this version does not support module reporting.  We only have auth types.
+        $this->cap_type="auth";
+       
+        //break apart at the "Cyrus timsieve...." "SASL={......}"
+        $this->item = sieve::parse_for_quotes($this->line);
+
+        $this->capabilities["implementation"] = $this->item[0];
+
+        //we should have "SASL={..........}" now.  Break out the {xx,yyy,zzzz}
+        $this->modules = substr($this->item[1], strpos($this->item[1], "{"),strlen($this->item[1])-1);
+
+        //then split again at the ", " stuff.
+        $this->modules = split($this->modules, ", ");
+        //fill up our $this->modules property
+        if(is_array($this->modules)){
+            foreach($this->modules as $this->module)
+                $this->capabilities[$this->cap_type][$this->module]=true;
+        } /* end if */
+        elseif(is_string($this->modules))
+            $this->capabilites[$this->cap_type][$this->module]=true;
+    }
+
+
+
+
+    if(sieve::status($this->line) == F_NO){            //here we should do some returning of error codes?
+        $this->error=EC_UNKNOWN;
+        $this->error_raw = "Server not allowing connections.";
+        return false;
+    }
+
+    /* decision login to decide what type of authentication to use... */
+
+     /* Loop through each allowed authentication type and see if the server allows the type */
+     foreach(split(" ",$this->auth_types) as $auth_type)
+     {
+        if ($this->capabilities["auth"][$auth_type])
+        {
+            /* We found an auth type that is allowed. */
+            $this->auth_in_use = $auth_type;
+            break;
+        }
+     }
+    
+    /* Fill error message if no auth types are present */
+    if (!isset($this->capabilities["auth"])){
+        $this->error=EC_UNKNOWN;
+       $this->error_raw = "No authentication methods found - please check your sieve setup for missing sasl modules";
+       return false;
+    }
+
+     /* call our authentication program */  
+    return sieve::authenticate();
+
+  }
+
+  function sieve_logout()
+  {
+    if($this->loggedin==false)
+        return false;
+
+    fputs($this->fp,"LOGOUT\r\n");
+    fclose($this->fp);
+    $this->loggedin=false;
+    return true;
+  }
+
+  function sieve_sendscript($scriptname, $script)
+  {
+    if($this->loggedin==false)
+        return false;
+    $this->script=stripslashes($script);
+    $len=strlen($this->script);
+    fputs($this->fp, "PUTSCRIPT \"$scriptname\" {".$len."+}\r\n");
+    fputs($this->fp, "$this->script\r\n");
+  
+    return sieve::get_response();
+
+  }  
+  
+  //it appears the timsieved does not honor the NUMBER type.  see lex.c in timsieved src.
+  //don't expect this function to work yet.  I might have messed something up here, too.
+  function sieve_havespace($scriptname, $scriptsize)
+  {
+    if($this->loggedin==false)
+        return false;
+    fputs($this->fp, "HAVESPACE \"$scriptname\" $scriptsize\r\n");
+    return sieve::get_response();
+
+  }  
+
+  function sieve_setactivescript($scriptname)
+  {
+    if($this->loggedin==false)
+        return false;
+
+    fputs($this->fp, "SETACTIVE \"$scriptname\"\r\n");   
+    return sieve::get_response();
+
+  }
+  
+  function sieve_getscript($scriptname)
+  {
+    unset($this->script);
+    if($this->loggedin==false)
+        return false;
+
+    fputs($this->fp, "GETSCRIPT \"$scriptname\"\r\n");
+    return sieve::get_response();
+   
+  }
+
+
+  function sieve_deletescript($scriptname)
+  {
+    if($this->loggedin==false)
+        return false;
+
+    fputs($this->fp, "DELETESCRIPT \"$scriptname\"\r\n");    
+
+    return sieve::get_response();
+  }
+
+  
+  function sieve_listscripts() 
+   { 
+     fputs($this->fp, "LISTSCRIPTS\r\n"); 
+     sieve::get_response();            //should always return true, even if there are no scripts...
+     if(isset($this->found_script) and $this->found_script)
+         return true;
+     else{
+         $this->error=EC_NOSCRIPTS;    //sieve::getresponse has no way of telling wether a script was found...
+         $this->error_raw="No scripts found for this account.";
+         return false;
+     }
+   }
+
+  function sieve_alive()
+  {
+      if(!isset($this->fp) or $this->fp==0){
+          $this->error = EC_NOT_LOGGED_IN;
+          return false;
+      }
+      elseif(feof($this->fp)){                 
+          $this->error = EC_NOT_LOGGED_IN;
+          return false;
+      }
+      else
+          return true;
+  }
+
+  function authenticate()
+  {
+
+    switch ($this->auth_in_use) {
+    
+        case "PLAIN":
+            $auth=base64_encode("$this->user\0$this->auth\0$this->pass");
+   
+            $this->len=strlen($auth);                  
+            fputs($this->fp, "AUTHENTICATE \"PLAIN\" {".$this->len."+}\r\n");
+            fputs($this->fp, "$auth\r\n");
+
+            $this->line=fgets($this->fp,1024);         
+            while(sieve::status($this->line) == F_DATA)
+               $this->line=fgets($this->fp,1024);
+
+             if(sieve::status($this->line) == F_NO)
+               return false;
+             $this->loggedin=true;
+               return true;    
+             break;
+
+        default:
+            return false;
+            break;
+
+    }//end switch
+
+
+  }//end authenticate()
+
+
+}
+
+
+
+?>
diff --git a/include/sieve/class_sievescript.php b/include/sieve/class_sievescript.php
new file mode 100644 (file)
index 0000000..a2e831c
--- /dev/null
@@ -0,0 +1,321 @@
+<?php
+
+/*
+       requirestring: envelope, fileinto, reject, comparator-*
+       address-part: localpart, domain, all*
+       match-type: is*, contains, matches
+       comperator: i;octet, i;ascii-casemap
+
+       <if>            <test:+>                <block:1>
+       <elsif>         <test:+>                <block:1>
+       <else>          <block:1>
+       <require>       <requirestring:+>
+       <reject>        <string:?>
+       <fileinto>      <string:1>
+       <redirect>      <string:1>
+       <stop>
+       <keep>
+       <discard>
+
+       <address>       <address-part,comperator,match-type:?>  <string:+>      <string:+>
+       <envelope>      <address-part,comperator,match-type:?>  <string:+>      <string:+>
+       <header>        <comperator,match-type:?>       <string:+>      <string:+>
+       <size>          <:over|:under:1>        <number:1>
+       <allof>         <test:+>
+       <anyof>         <test:+>
+       <exists>        <string:+>
+       <not>           <test:1>
+       <true>
+       <false>
+*/
+
+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 = "&lt;" . chop(mb_substr($this->_script, $token['pos'], $token['len'])) . "&gt; ";
+                       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 (file)
index 0000000..96bc7a8
--- /dev/null
@@ -0,0 +1,175 @@
+<?php
+
+class Tree
+{
+       var $childs_;
+       var $parents_;
+       var $nodes_;
+       var $maxId_;
+       var $dumpFn_;
+       var $dump_;
+
+       function Tree(&$root)
+       {
+               $this->_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; $i<count($childs); ++$i)
+               {
+                       $c_last = false;
+                       if ($i+1 == count($childs))
+                       {
+                               $c_last = true;
+                       }
+
+                       $this->doDump_($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 (file)
index 0000000..c438a9c
--- /dev/null
@@ -0,0 +1,7 @@
+<?php
+require_once 'class_parser.inc';
+require_once 'class_scanner.inc';
+require_once 'class_semantics.inc';
+require_once 'class_tree.inc';
+require_once 'class_sieve.inc';
+?>
index 468e4ab7e6544ad68f6a08f5f641013b8e671c99..9dcd068225cfef119a4dbbd2907c104b45bbdc78 100644 (file)
@@ -10,7 +10,7 @@
  */
 
 /* Load sieve support */
-require_once ("class_sieve.inc");
+require_once ("sieve/libsieve.php");
 
 /* Load mail methods */
 global $BASE_DIR;