]> git.donarmstrong.com Git - roundcube.git/blob - plugins/managesieve/lib/rcube_sieve.php
Imported Upstream version 0.3.1
[roundcube.git] / plugins / managesieve / lib / rcube_sieve.php
1 <?php
2
3 /*
4   Classes for managesieve operations (using PEAR::Net_Sieve)
5
6   Author: Aleksander Machniak <alec@alec.pl>
7
8   $Id$
9
10 */
11
12 //  Sieve Language Basics: http://www.ietf.org/rfc/rfc5228.txt
13
14 define('SIEVE_ERROR_CONNECTION', 1);
15 define('SIEVE_ERROR_LOGIN', 2);
16 define('SIEVE_ERROR_NOT_EXISTS', 3);    // script not exists
17 define('SIEVE_ERROR_INSTALL', 4);       // script installation
18 define('SIEVE_ERROR_ACTIVATE', 5);      // script activation
19 define('SIEVE_ERROR_OTHER', 255);       // other/unknown error
20
21
22 class rcube_sieve
23 {
24   var $sieve;                           // Net_Sieve object
25   var $error = false;                   // error flag 
26   var $list = array();                  // scripts list 
27
28   public $script;                       // rcube_sieve_script object
29   private $disabled;                    // array of disabled extensions
30
31   /**
32     * Object constructor
33     *
34     * @param  string  Username (to managesieve login)
35     * @param  string  Password (to managesieve login)
36     * @param  string  Managesieve server hostname/address
37     * @param  string  Managesieve server port number
38     * @param  string  Enable/disable TLS use
39     * @param  array   Disabled extensions
40     */
41   public function __construct($username, $password='', $host='localhost', $port=2000, $usetls=true, $disabled=array())
42     {
43       $this->sieve = new Net_Sieve();
44       
45 //      $this->sieve->setDebug();
46       if (PEAR::isError($this->sieve->connect($host, $port, NULL, $usetls)))
47         return $this->_set_error(SIEVE_ERROR_CONNECTION);
48
49       if (PEAR::isError($this->sieve->login($username, $password)))
50         return $this->_set_error(SIEVE_ERROR_LOGIN);
51
52       $this->disabled = $disabled;
53       $this->_get_script();
54     }
55
56   /**
57     * Getter for error code
58     */
59   public function error()
60     {
61       return $this->error ? $this->error : false;
62     }
63                             
64   public function save()
65     {
66       $script = $this->script->as_text();
67
68       if (!$script)
69         $script = '/* empty script */';
70
71       if (PEAR::isError($this->sieve->installScript('roundcube', $script)))
72         return $this->_set_error(SIEVE_ERROR_INSTALL);
73
74       if (PEAR::isError($this->sieve->setActive('roundcube')))
75         return $this->_set_error(SIEVE_ERROR_ACTIVATE);
76
77       return true;
78     }
79
80   public function get_extensions()
81     {
82       if ($this->sieve) {
83         $ext = $this->sieve->getExtensions();
84
85         if ($this->script) {
86           $supported = $this->script->get_extensions();
87           foreach ($ext as $idx => $ext_name)
88             if (!in_array($ext_name, $supported))
89               unset($ext[$idx]);
90         }
91
92         return array_values($ext);
93       }
94     }
95
96   private function _get_script()
97     {
98       if (!$this->sieve)
99         return false;
100     
101       $this->list = $this->sieve->listScripts();
102
103       if (PEAR::isError($this->list))
104         return $this->_set_error(SIEVE_ERROR_OTHER);
105     
106       if (in_array('roundcube', $this->list))
107         {
108           $script = $this->sieve->getScript('roundcube');
109     
110           if (PEAR::isError($script))
111             return $this->_set_error(SIEVE_ERROR_OTHER);
112         }
113       // import scripts from squirrelmail
114       elseif (in_array('phpscript', $this->list))
115         {
116           $script = $this->sieve->getScript('phpscript');
117
118           $script = $this->_convert_from_squirrel_rules($script);
119
120           $this->script = new rcube_sieve_script($script, $this->disabled);
121        
122           $this->save();
123
124           $script = $this->sieve->getScript('roundcube');
125
126           if (PEAR::isError($script))
127             return $this->_set_error(SIEVE_ERROR_OTHER);
128         }
129       else
130         {
131           $this->_set_error(SIEVE_ERROR_NOT_EXISTS);
132           $script = '';
133         }
134
135       $this->script = new rcube_sieve_script($script, $this->disabled);
136     }
137     
138   private function _convert_from_squirrel_rules($script)
139     {
140       $i = 0;
141       $name = array();
142       // tokenize rules
143       if ($tokens = preg_split('/(#START_SIEVE_RULE.*END_SIEVE_RULE)\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE))
144         foreach($tokens as $token)
145           {
146             if (preg_match('/^#START_SIEVE_RULE.*/', $token, $matches))
147               {
148                 $name[$i] = "unnamed rule ".($i+1);
149                 $content .= "# rule:[".$name[$i]."]\n";
150               }
151             elseif (isset($name[$i]))
152               {
153                 $content .= "if ".$token."\n";
154                 $i++;
155               }
156           }
157
158       return $content;
159     }
160
161
162   private function _set_error($error)
163     {
164       $this->error = $error;
165       return false;
166     }    
167 }
168
169 class rcube_sieve_script
170 {
171   var $content = array();       // script rules array   
172
173   private $supported = array(   // extensions supported by class
174     'fileinto',
175     'reject',
176     'ereject',
177     'vacation',         // RFC5230
178     // TODO: (most wanted first) body, imapflags, notify, regex
179     );
180   
181   /**
182     * Object constructor
183     *
184     * @param  string  Script's text content
185     * @param  array   Disabled extensions
186     */
187   public function __construct($script, $disabled=NULL)
188     {
189       if (!empty($disabled))
190         foreach ($disabled as $ext)
191           if (($idx = array_search($ext, $this->supported)) !== false)
192             unset($this->supported[$idx]);
193
194       $this->content = $this->_parse_text($script);
195     }
196
197   /**
198     * Adds script contents as text to the script array (at the end)
199     *
200     * @param    string  Text script contents
201     */
202   public function add_text($script)
203     {
204       $content = $this->_parse_text($script);
205       $result = false;
206       
207       // check existsing script rules names
208       foreach ($this->content as $idx => $elem)
209         $names[$elem['name']] = $idx;
210       
211       foreach ($content as $elem)
212         if (!isset($names[$elem['name']]))
213           {
214             array_push($this->content, $elem);
215             $result = true;
216           }
217
218       return $result;
219     }
220
221   /**
222     * Adds rule to the script (at the end)
223     *
224     * @param    string  Rule name
225     * @param    array   Rule content (as array)
226     */
227   public function add_rule($content)
228     {
229       // TODO: check this->supported
230       array_push($this->content, $content);
231       return sizeof($this->content)-1;
232     }
233
234   public function delete_rule($index)
235     {
236       if(isset($this->content[$index]))
237         {
238           unset($this->content[$index]);
239           return true;
240         }
241       return false;
242     }
243
244   public function size()
245     {
246       return sizeof($this->content);
247     }
248
249   public function update_rule($index, $content)
250     {
251       // TODO: check this->supported
252       if ($this->content[$index])
253         {
254           $this->content[$index] = $content;
255           return $index;
256         }
257       return false;
258     }
259
260   /**
261     * Returns script as text
262     */
263   public function as_text()
264     {
265       $script = '';
266       $exts = array();
267       $idx = 0;
268       
269       // rules
270       foreach ($this->content as $rule)
271         {
272           $extension = '';
273           $tests = array();
274           $i = 0;
275           
276           // header
277           $script .= '# rule:[' . $rule['name'] . "]\n";
278   
279           // constraints expressions
280           foreach ($rule['tests'] as $test)
281             {
282               $tests[$i] = '';
283               switch ($test['test'])
284                 {
285                   case 'size':
286                     $tests[$i] .= ($test['not'] ? 'not ' : '');
287                     $tests[$i] .= 'size :' . ($test['type']=='under' ? 'under ' : 'over ') . $test['arg'];
288                   break;
289                   case 'true':
290                     $tests[$i] .= ($test['not'] ? 'not true' : 'true');
291                   break;
292                   case 'exists':
293                     $tests[$i] .= ($test['not'] ? 'not ' : '');
294                     if (is_array($test['arg']))
295                         $tests[$i] .= 'exists ["' . implode('", "', $this->_escape_string($test['arg'])) . '"]';
296                     else
297                         $tests[$i] .= 'exists "' . $this->_escape_string($test['arg']) . '"';
298                   break;    
299                   case 'header':
300                     $tests[$i] .= ($test['not'] ? 'not ' : '');
301                     $tests[$i] .= 'header :' . $test['type'];
302                     if (is_array($test['arg1']))
303                         $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg1'])) . '"]';
304                     else
305                         $tests[$i] .= ' "' . $this->_escape_string($test['arg1']) . '"';
306                     if (is_array($test['arg2']))
307                         $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg2'])) . '"]';
308                     else
309                         $tests[$i] .= ' "' . $this->_escape_string($test['arg2']) . '"';
310                   break;
311                 }
312               $i++;
313             }
314   
315           $script .= ($idx>0 ? 'els' : '').($rule['join'] ? 'if allof (' : 'if anyof (');
316           if (sizeof($tests) > 1)
317             $script .= implode(",\n\t", $tests);
318           elseif (sizeof($tests))
319             $script .= $tests[0];
320           else
321             $script .= 'true';
322           $script .= ")\n{\n";
323   
324           // action(s)
325           foreach ($rule['actions'] as $action)
326             switch ($action['type'])
327             {
328               case 'fileinto':
329                 $extension = 'fileinto';
330                 $script .= "\tfileinto \"" . $this->_escape_string($action['target']) . "\";\n";
331               break;
332               case 'redirect':
333                 $script .= "\tredirect \"" . $this->_escape_string($action['target']) . "\";\n";
334               break;
335               case 'reject':
336               case 'ereject':
337                 $extension = $action['type'];
338                 if (strpos($action['target'], "\n")!==false)
339                   $script .= "\t".$action['type']." text:\n" . $action['target'] . "\n.\n;\n";
340                 else
341                   $script .= "\t".$action['type']." \"" . $this->_escape_string($action['target']) . "\";\n";
342               break;
343               case 'keep':
344               case 'discard':
345               case 'stop':
346                 $script .= "\t" . $action['type'] .";\n";
347               break;
348               case 'vacation':
349                 $extension = 'vacation';
350                 $script .= "\tvacation";
351                 if ($action['days'])
352                   $script .= " :days " . $action['days'];
353                 if ($action['addresses'])
354                   $script .= " :addresses " . $this->_print_list($action['addresses']);
355                 if ($action['subject'])
356                   $script .= " :subject \"" . $this->_escape_string($action['subject']) . "\"";
357                 if ($action['handle'])
358                   $script .= " :handle \"" . $this->_escape_string($action['handle']) . "\"";
359                 if ($action['from'])
360                   $script .= " :from \"" . $this->_escape_string($action['from']) . "\"";
361                 if ($action['mime'])
362                   $script .= " :mime";
363                 if (strpos($action['reason'], "\n")!==false)
364                   $script .= " text:\n" . $action['reason'] . "\n.\n;\n";
365                 else
366                   $script .= " \"" . $this->_escape_string($action['reason']) . "\";\n";
367               break;
368             }
369           
370           $script .= "}\n";
371           $idx++;
372
373           if ($extension && !isset($exts[$extension]))
374             $exts[$extension] = $extension;
375         }
376       
377       // requires
378       if (sizeof($exts))
379         $script = 'require ["' . implode('","', $exts) . "\"];\n" . $script;
380
381       return $script;
382     }
383
384   /**
385     * Returns script object
386     *
387     */
388   public function as_array()
389     {
390       return $this->content;
391     }
392
393   /**
394     * Returns array of supported extensions
395     *
396     */
397   public function get_extensions()
398     {
399       return array_values($this->supported);
400     }
401
402   /**
403     * Converts text script to rules array
404     *
405     * @param    string  Text script
406     */
407   private function _parse_text($script)
408     {
409       $i = 0;
410       $content = array();
411
412       // remove C comments
413       $script = preg_replace('|/\*.*?\*/|sm', '', $script);
414
415       // tokenize rules
416       if ($tokens = preg_split('/(# rule:\[.*\])\r?\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE))
417         foreach($tokens as $token)
418           {
419             if (preg_match('/^# rule:\[(.*)\]/', $token, $matches))
420               {
421                 $content[$i]['name'] = $matches[1];
422               }
423             elseif (isset($content[$i]['name']) && sizeof($content[$i]) == 1)
424               {
425                 if ($rule = $this->_tokenize_rule($token))
426                   {
427                     $content[$i] = array_merge($content[$i], $rule);
428                     $i++;
429                   }
430                 else // unknown rule format
431                     unset($content[$i]);
432               }
433           }
434
435       return $content;
436     }
437
438   /**
439     * Convert text script fragment to rule object
440     *
441     * @param    string  Text rule
442     */
443   private function _tokenize_rule($content)
444     {
445       $result = NULL;
446     
447       if (preg_match('/^(if|elsif|else)\s+((true|not\s+true|allof|anyof|exists|header|not|size)(.*))\s+\{(.*)\}$/sm', trim($content), $matches))
448         {
449           list($tests, $join) = $this->_parse_tests(trim($matches[2]));
450           $actions = $this->_parse_actions(trim($matches[5]));
451
452           if ($tests && $actions)
453             $result = array(
454                     'tests' => $tests,
455                     'actions' => $actions,
456                     'join' => $join,
457             );
458         }
459
460       return $result;
461     }    
462
463   /**
464     * Parse body of actions section
465     *
466     * @param    string  Text body
467     * @return   array   Array of parsed action type/target pairs
468     */
469   private function _parse_actions($content)
470     {
471       $result = NULL;
472
473       // supported actions
474       $patterns[] = '^\s*discard;';
475       $patterns[] = '^\s*keep;';
476       $patterns[] = '^\s*stop;';
477       $patterns[] = '^\s*redirect\s+(.*?[^\\\]);';
478       if (in_array('fileinto', $this->supported))
479         $patterns[] = '^\s*fileinto\s+(.*?[^\\\]);';
480       if (in_array('reject', $this->supported)) {
481         $patterns[] = '^\s*reject\s+text:(.*)\n\.\n;';
482         $patterns[] = '^\s*reject\s+(.*?[^\\\]);';
483         $patterns[] = '^\s*ereject\s+text:(.*)\n\.\n;';
484         $patterns[] = '^\s*ereject\s+(.*?[^\\\]);';
485       }
486       if (in_array('vacation', $this->supported))
487         $patterns[] = '^\s*vacation\s+(.*?[^\\\]);';
488
489       $pattern = '/(' . implode('$)|(', $patterns) . '$)/ms';
490
491       // parse actions body
492       if (preg_match_all($pattern, $content, $mm, PREG_SET_ORDER))
493       {
494         foreach ($mm as $m)
495         {
496           $content = trim($m[0]);
497           
498           if(preg_match('/^(discard|keep|stop)/', $content, $matches))
499             {
500               $result[] = array('type' => $matches[1]);
501             }
502           elseif(preg_match('/^fileinto/', $content))
503             {
504               $result[] = array('type' => 'fileinto', 'target' => $this->_parse_string($m[sizeof($m)-1])); 
505             }
506           elseif(preg_match('/^redirect/', $content))
507             {
508               $result[] = array('type' => 'redirect', 'target' => $this->_parse_string($m[sizeof($m)-1])); 
509             }
510           elseif(preg_match('/^(reject|ereject)\s+(.*);$/sm', $content, $matches))
511             {
512               $result[] = array('type' => $matches[1], 'target' => $this->_parse_string($matches[2])); 
513             }
514           elseif(preg_match('/^vacation\s+(.*);$/sm', $content, $matches))
515             {
516               $vacation = array('type' => 'vacation');
517
518               if (preg_match('/:(days)\s+([0-9]+)/', $content, $vm)) {
519                 $vacation['days'] = $vm[2];
520                 $content = preg_replace('/:(days)\s+([0-9]+)/', '', $content); 
521               }
522               if (preg_match('/:(subject)\s+(".*?[^\\\]")/', $content, $vm)) {
523                 $vacation['subject'] = $vm[2];
524                 $content = preg_replace('/:(subject)\s+(".*?[^\\\]")/', '', $content); 
525               }
526               if (preg_match('/:(addresses)\s+\[(.*?[^\\\])\]/', $content, $vm)) {
527                 $vacation['addresses'] = $this->_parse_list($vm[2]);
528                 $content = preg_replace('/:(addresses)\s+\[(.*?[^\\\])\]/', '', $content); 
529               }
530               if (preg_match('/:(handle)\s+(".*?[^\\\]")/', $content, $vm)) {
531                 $vacation['handle'] = $vm[2];
532                 $content = preg_replace('/:(handle)\s+(".*?[^\\\]")/', '', $content); 
533               }
534               if (preg_match('/:(from)\s+(".*?[^\\\]")/', $content, $vm)) {
535                 $vacation['from'] = $vm[2];
536                 $content = preg_replace('/:(from)\s+(".*?[^\\\]")/', '', $content); 
537               }
538               $content = preg_replace('/^vacation/', '', $content);          
539               $content = preg_replace('/;$/', '', $content);
540               $content = trim($content);
541               if (preg_match('/^:(mime)/', $content, $vm)) {
542                 $vacation['mime'] = true;
543                 $content = preg_replace('/^:mime/', '', $content); 
544               }
545
546               $vacation['reason'] = $this->_parse_string($content);
547
548               $result[] = $vacation;
549             }
550         }
551       }
552
553       return $result;
554     }    
555     
556    /**
557     * Parse test/conditions section
558     *
559     * @param    string  Text 
560     */
561
562   private function _parse_tests($content)
563     {
564       $result = NULL;
565
566       // lists
567       if (preg_match('/^(allof|anyof)\s+\((.*)\)$/sm', $content, $matches))
568         {
569           $content = $matches[2];
570           $join = $matches[1]=='allof' ? true : false;
571         }
572       else
573           $join = false;
574       
575       // supported tests regular expressions
576       // TODO: comparators, envelope
577       $patterns[] = '(not\s+)?(exists)\s+\[(.*?[^\\\])\]';
578       $patterns[] = '(not\s+)?(exists)\s+(".*?[^\\\]")';
579       $patterns[] = '(not\s+)?(true)';
580       $patterns[] = '(not\s+)?(size)\s+:(under|over)\s+([0-9]+[KGM]{0,1})';
581       $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+\[(.*?[^\\\]")\]\s+\[(.*?[^\\\]")\]';
582       $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+(".*?[^\\\]")\s+(".*?[^\\\]")';
583       $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+\[(.*?[^\\\]")\]\s+(".*?[^\\\]")';
584       $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+(".*?[^\\\]")\s+\[(.*?[^\\\]")\]';
585       
586       // join patterns...
587       $pattern = '/(' . implode(')|(', $patterns) . ')/';
588
589       // ...and parse tests list
590       if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER))
591         {
592           foreach ($matches as $match)
593             {
594               $size = sizeof($match);
595               
596               if (preg_match('/^(not\s+)?size/', $match[0]))
597                 {
598                   $result[] = array(
599                     'test'      => 'size',
600                     'not'       => $match[$size-4] ? true : false,
601                     'type'      => $match[$size-2], // under/over
602                     'arg'       => $match[$size-1], // value
603                   );
604                 }
605               elseif (preg_match('/^(not\s+)?header/', $match[0]))
606                 {
607                   $result[] = array(
608                     'test'      => 'header',
609                     'not'       => $match[$size-5] ? true : false,
610                     'type'      => $match[$size-3], // is/contains/matches
611                     'arg1'      => $this->_parse_list($match[$size-2]), // header(s)
612                     'arg2'      => $this->_parse_list($match[$size-1]), // string(s)
613                   );  
614                 }
615               elseif (preg_match('/^(not\s+)?exists/', $match[0]))
616                 {
617                   $result[] = array(
618                     'test'      => 'exists',
619                     'not'       => $match[$size-3] ? true : false,
620                     'arg'       => $this->_parse_list($match[$size-1]), // header(s)
621                   );
622                 }
623               elseif (preg_match('/^(not\s+)?true/', $match[0]))
624                 {
625                   $result[] = array(
626                     'test'      => 'true',
627                     'not'       => $match[$size-2] ? true : false,
628                   );
629                 }
630             }
631         }
632
633       return array($result, $join);
634     }    
635
636    /**
637     * Parse string value
638     *
639     * @param    string  Text 
640     */
641   private function _parse_string($content)
642     {
643       $text = '';
644       $content = trim($content);
645
646       if (preg_match('/^text:(.*)\.$/sm', $content, $matches))
647         $text = trim($matches[1]);
648       elseif (preg_match('/^"(.*)"$/', $content, $matches))
649         $text = str_replace('\"', '"', $matches[1]);
650
651       return $text;
652     }    
653
654    /**
655     * Escape special chars in string value
656     *
657     * @param    string  Text 
658     */
659   private function _escape_string($content)
660     {
661       $replace['/"/'] = '\\"';
662       
663       if (is_array($content))
664         {
665           for ($x=0, $y=sizeof($content); $x<$y; $x++)
666             $content[$x] = preg_replace(array_keys($replace), array_values($replace), $content[$x]);
667         
668           return $content;
669         }
670       else
671         return preg_replace(array_keys($replace), array_values($replace), $content);
672     }
673
674    /**
675     * Parse string or list of strings to string or array of strings
676     *
677     * @param    string  Text 
678     */
679   private function _parse_list($content)
680     {
681       $result = array();
682       
683       for ($x=0, $len=strlen($content); $x<$len; $x++)
684         {
685           switch ($content[$x])
686             {
687               case '\\':
688                 $str .= $content[++$x];
689               break;
690               case '"':
691                 if (isset($str))
692                   {
693                     $result[] = $str;
694                     unset($str);
695                   }
696                 else
697                   $str = '';
698               break;
699               default:
700                 if(isset($str))
701                   $str .= $content[$x];
702               break;
703             }
704         }
705       
706       if (sizeof($result)>1)
707         return $result;
708       elseif (sizeof($result) == 1)
709         return $result[0];
710       else
711         return NULL;
712     }    
713
714    /**
715     * Convert array of elements to list of strings
716     *
717     * @param    string  Text 
718     */
719   private function _print_list($list)
720     {
721       $list = (array) $list;
722       foreach($list as $idx => $val)
723         $list[$idx] = $this->_escape_string($val);
724     
725       return '["' . implode('","', $list) . '"]';
726     }
727 }
728
729 ?>