alecpl
2009-10-06 ffeab7fe7e0ccf1d458d6a863600359c38769950
commit | author | age
a393c3 1 <?php
A 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
e8d8b6 120           $this->script = new rcube_sieve_script($script, $this->disabled);
a393c3 121        
A 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     */
e8d8b6 187   public function __construct($script, $disabled=NULL)
a393c3 188     {
A 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();
fcc34c 267       $idx = 0;
a393c3 268       
A 269       // rules
fcc34c 270       foreach ($this->content as $rule)
a393c3 271         {
A 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";
fcc34c 371       $idx++;
A 372
a393c3 373       if ($extension && !isset($exts[$extension]))
A 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     
d168b9 447       if (preg_match('/^(if|elsif|else)\s+((true|not\s+true|allof|anyof|exists|header|not|size)(.*))\s+\{(.*)\}$/sm', trim($content), $matches))
a393c3 448         {
A 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     }
d168b9 459
a393c3 460       return $result;
A 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 ?>