source: subversion/trunk/plugins/managesieve/lib/rcube_sieve.php @ 4241

Last change on this file since 4241 was 4241, checked in by alec, 3 years ago
  • Apply forgotten changes for form errors handling
  • Fix handling of scripts with CRLF line separator
  • Property svn:keywords set to Author Id
File size: 31.7 KB
Line 
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
14define('SIEVE_ERROR_CONNECTION', 1);
15define('SIEVE_ERROR_LOGIN', 2);
16define('SIEVE_ERROR_NOT_EXISTS', 3);    // script not exists
17define('SIEVE_ERROR_INSTALL', 4);       // script installation
18define('SIEVE_ERROR_ACTIVATE', 5);      // script activation
19define('SIEVE_ERROR_DELETE', 6);        // script deletion
20define('SIEVE_ERROR_INTERNAL', 7);      // internal error
21define('SIEVE_ERROR_DEACTIVATE', 8);    // script activation
22define('SIEVE_ERROR_OTHER', 255);       // other/unknown error
23
24
25class rcube_sieve
26{
27    private $sieve;                 // Net_Sieve object
28    private $error = false;         // error flag
29    private $list = array();        // scripts list
30
31    public $script;                 // rcube_sieve_script object
32    public $current;                // name of currently loaded script
33    private $disabled;              // array of disabled extensions
34
35
36    /**
37     * Object constructor
38     *
39     * @param string  Username (for managesieve login)
40     * @param string  Password (for managesieve login)
41     * @param string  Managesieve server hostname/address
42     * @param string  Managesieve server port number
43     * @param string  Managesieve authentication method
44     * @param boolean Enable/disable TLS use
45     * @param array   Disabled extensions
46     * @param boolean Enable/disable debugging
47     * @param string  Proxy authentication identifier
48     * @param string  Proxy authentication password
49     */
50    public function __construct($username, $password='', $host='localhost', $port=2000,
51        $auth_type=null, $usetls=true, $disabled=array(), $debug=false,
52        $auth_cid=null, $auth_pw=null)
53    {
54        $this->sieve = new Net_Sieve();
55
56        if ($debug) {
57            $this->sieve->setDebug(true, array($this, 'debug_handler'));
58        }
59
60        if (PEAR::isError($this->sieve->connect($host, $port, NULL, $usetls))) {
61            return $this->_set_error(SIEVE_ERROR_CONNECTION);
62        }
63
64        if (!empty($auth_cid)) {
65            $authz    = $username;
66            $username = $auth_cid;
67            $password = $auth_pw;
68        }
69
70        if (PEAR::isError($this->sieve->login($username, $password,
71            $auth_type ? strtoupper($auth_type) : null, $authz))
72        ) {
73            return $this->_set_error(SIEVE_ERROR_LOGIN);
74        }
75
76        $this->disabled = $disabled;
77    }
78
79    public function __destruct() {
80        $this->sieve->disconnect();
81    }
82
83    /**
84     * Getter for error code
85     */
86    public function error()
87    {
88        return $this->error ? $this->error : false;
89    }
90
91    /**
92     * Saves current script into server
93     */
94    public function save($name = null)
95    {
96        if (!$this->sieve)
97            return $this->_set_error(SIEVE_ERROR_INTERNAL);
98
99        if (!$this->script)
100            return $this->_set_error(SIEVE_ERROR_INTERNAL);
101
102        if (!$name)
103            $name = $this->current;
104
105        $script = $this->script->as_text();
106
107        if (!$script)
108            $script = '/* empty script */';
109
110        if (PEAR::isError($this->sieve->installScript($name, $script)))
111            return $this->_set_error(SIEVE_ERROR_INSTALL);
112
113        return true;
114    }
115
116    /**
117     * Saves text script into server
118     */
119    public function save_script($name, $content = null)
120    {
121        if (!$this->sieve)
122            return $this->_set_error(SIEVE_ERROR_INTERNAL);
123
124        if (!$content)
125            $content = '/* empty script */';
126
127        if (PEAR::isError($this->sieve->installScript($name, $content)))
128            return $this->_set_error(SIEVE_ERROR_INSTALL);
129
130        return true;
131    }
132
133    /**
134     * Activates specified script
135     */
136    public function activate($name = null)
137    {
138        if (!$this->sieve)
139            return $this->_set_error(SIEVE_ERROR_INTERNAL);
140
141        if (!$name)
142            $name = $this->current;
143
144        if (PEAR::isError($this->sieve->setActive($name)))
145            return $this->_set_error(SIEVE_ERROR_ACTIVATE);
146
147        return true;
148    }
149
150    /**
151     * De-activates specified script
152     */
153    public function deactivate()
154    {
155        if (!$this->sieve)
156            return $this->_set_error(SIEVE_ERROR_INTERNAL);
157
158        if (PEAR::isError($this->sieve->setActive('')))
159            return $this->_set_error(SIEVE_ERROR_DEACTIVATE);
160
161        return true;
162    }
163
164    /**
165     * Removes specified script
166     */
167    public function remove($name = null)
168    {
169        if (!$this->sieve)
170            return $this->_set_error(SIEVE_ERROR_INTERNAL);
171
172        if (!$name)
173            $name = $this->current;
174
175        // script must be deactivated first
176        if ($name == $this->sieve->getActive())
177            if (PEAR::isError($this->sieve->setActive('')))
178                return $this->_set_error(SIEVE_ERROR_DELETE);
179
180        if (PEAR::isError($this->sieve->removeScript($name)))
181            return $this->_set_error(SIEVE_ERROR_DELETE);
182
183        if ($name == $this->current)
184            $this->current = null;
185
186        return true;
187    }
188
189    /**
190     * Gets list of supported by server Sieve extensions
191     */
192    public function get_extensions()
193    {
194        if (!$this->sieve)
195            return $this->_set_error(SIEVE_ERROR_INTERNAL);
196
197        $ext = $this->sieve->getExtensions();
198        // we're working on lower-cased names
199        $ext = array_map('strtolower', (array) $ext);
200
201        if ($this->script) {
202            $supported = $this->script->get_extensions();
203            foreach ($ext as $idx => $ext_name)
204                if (!in_array($ext_name, $supported))
205                    unset($ext[$idx]);
206        }
207
208        return array_values($ext);
209    }
210
211    /**
212     * Gets list of scripts from server
213     */
214    public function get_scripts()
215    {
216        if (!$this->list) {
217
218            if (!$this->sieve)
219                return $this->_set_error(SIEVE_ERROR_INTERNAL);
220
221            $this->list = $this->sieve->listScripts();
222
223            if (PEAR::isError($this->list))
224                return $this->_set_error(SIEVE_ERROR_OTHER);
225        }
226
227        return $this->list;
228    }
229
230    /**
231     * Returns active script name
232     */
233    public function get_active()
234    {
235        if (!$this->sieve)
236            return $this->_set_error(SIEVE_ERROR_INTERNAL);
237
238        return $this->sieve->getActive();
239    }
240
241    /**
242     * Loads script by name
243     */
244    public function load($name)
245    {
246        if (!$this->sieve)
247            return $this->_set_error(SIEVE_ERROR_INTERNAL);
248
249        if ($this->current == $name)
250            return true;
251
252        $script = $this->sieve->getScript($name);
253
254        if (PEAR::isError($script))
255            return $this->_set_error(SIEVE_ERROR_OTHER);
256
257        // try to parse from Roundcube format
258        $this->script = $this->_parse($script);
259
260        $this->current = $name;
261
262        return true;
263    }
264
265    /**
266     * Loads script from text content
267     */
268    public function load_script($script)
269    {
270        if (!$this->sieve)
271            return $this->_set_error(SIEVE_ERROR_INTERNAL);
272
273        // try to parse from Roundcube format
274        $this->script = $this->_parse($script);
275    }
276
277    /**
278     * Creates rcube_sieve_script object from text script
279     */
280    private function _parse($txt)
281    {
282        // try to parse from Roundcube format
283        $script = new rcube_sieve_script($txt, $this->disabled);
284
285        // ... else try to import from different formats
286        if (empty($script->content)) {
287            $script = $this->_import_rules($txt);
288            $script = new rcube_sieve_script($script, $this->disabled);
289        }
290
291        // replace all elsif with if+stop, we support only ifs
292        foreach ($script->content as $idx => $rule) {
293            if (!isset($script->content[$idx+1])
294                || preg_match('/^else|elsif$/', $script->content[$idx+1]['type'])) {
295                // 'stop' not found?
296                if (!preg_match('/^(stop|vacation)$/', $rule['actions'][count($rule['actions'])-1]['type'])) {
297                    $script->content[$idx]['actions'][] = array(
298                        'type' => 'stop'
299                    );
300                }
301            }
302        }
303
304        return $script;
305    }
306
307    /**
308     * Gets specified script as text
309     */
310    public function get_script($name)
311    {
312        if (!$this->sieve)
313            return $this->_set_error(SIEVE_ERROR_INTERNAL);
314
315        $content = $this->sieve->getScript($name);
316
317        if (PEAR::isError($content))
318            return $this->_set_error(SIEVE_ERROR_OTHER);
319
320        return $content;
321    }
322
323    /**
324     * Creates empty script or copy of other script
325     */
326    public function copy($name, $copy)
327    {
328        if (!$this->sieve)
329            return $this->_set_error(SIEVE_ERROR_INTERNAL);
330
331        if ($copy) {
332            $content = $this->sieve->getScript($copy);
333
334            if (PEAR::isError($content))
335                return $this->_set_error(SIEVE_ERROR_OTHER);
336        }
337
338        return $this->save_script($name, $content);
339    }
340
341    private function _import_rules($script)
342    {
343        $i = 0;
344        $name = array();
345
346        // Squirrelmail (Avelsieve)
347        if ($tokens = preg_split('/(#START_SIEVE_RULE.*END_SIEVE_RULE)\r?\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE)) {
348            foreach($tokens as $token) {
349                if (preg_match('/^#START_SIEVE_RULE.*/', $token, $matches)) {
350                    $name[$i] = "unnamed rule ".($i+1);
351                    $content .= "# rule:[".$name[$i]."]\n";
352                }
353                elseif (isset($name[$i])) {
354                    // This preg_replace is added because I've found some Avelsieve scripts
355                    // with rules containing "if" here. I'm not sure it was working
356                    // before without this or not.
357                    $token = preg_replace('/^if\s+/', '', trim($token));
358                    $content .= "if $token\n";
359                    $i++;
360                }
361            }
362        }
363        // Horde (INGO)
364        else if ($tokens = preg_split('/(# .+)\r?\n/i', $script, -1, PREG_SPLIT_DELIM_CAPTURE)) {
365            foreach($tokens as $token) {
366                if (preg_match('/^# (.+)/i', $token, $matches)) {
367                    $name[$i] = $matches[1];
368                    $content .= "# rule:[" . $name[$i] . "]\n";
369                }
370                elseif (isset($name[$i])) {
371                    $token = str_replace(":comparator \"i;ascii-casemap\" ", "", $token);
372                    $content .= $token . "\n";
373                    $i++;
374                }
375            }
376        }
377
378        return $content;
379    }
380
381    private function _set_error($error)
382    {
383        $this->error = $error;
384        return false;
385    }
386
387    /**
388     * This is our own debug handler for connection
389     */
390    public function debug_handler(&$sieve, $message)
391    {
392        write_log('sieve', preg_replace('/\r\n$/', '', $message));
393    }
394}
395
396
397class rcube_sieve_script
398{
399    public $content = array();      // script rules array
400
401    private $supported = array(     // extensions supported by class
402        'fileinto',
403        'reject',
404        'ereject',
405        'copy',                     // RFC3894
406        'vacation',                 // RFC5230
407        'relational',               // RFC3431
408    // TODO: (most wanted first) body, imapflags, notify, regex
409    );
410
411    /**
412     * Object constructor
413     *
414     * @param  string  Script's text content
415     * @param  array   Disabled extensions
416     */
417    public function __construct($script, $disabled=NULL)
418    {
419        if (!empty($disabled))
420            foreach ($disabled as $ext)
421                if (($idx = array_search($ext, $this->supported)) !== false)
422                    unset($this->supported[$idx]);
423
424        $this->content = $this->_parse_text($script);
425    }
426
427    /**
428     * Adds script contents as text to the script array (at the end)
429     *
430     * @param    string    Text script contents
431     */
432    public function add_text($script)
433    {
434        $content = $this->_parse_text($script);
435        $result = false;
436
437        // check existsing script rules names
438        foreach ($this->content as $idx => $elem) {
439            $names[$elem['name']] = $idx;
440        }
441
442        foreach ($content as $elem) {
443            if (!isset($names[$elem['name']])) {
444                array_push($this->content, $elem);
445                $result = true;
446            }
447        }
448
449        return $result;
450    }
451
452    /**
453     * Adds rule to the script (at the end)
454     *
455     * @param string Rule name
456     * @param array  Rule content (as array)
457     */
458    public function add_rule($content)
459    {
460        // TODO: check this->supported
461        array_push($this->content, $content);
462        return sizeof($this->content)-1;
463    }
464
465    public function delete_rule($index)
466    {
467        if(isset($this->content[$index])) {
468            unset($this->content[$index]);
469            return true;
470        }
471        return false;
472    }
473
474    public function size()
475    {
476        return sizeof($this->content);
477    }
478
479    public function update_rule($index, $content)
480    {
481        // TODO: check this->supported
482        if ($this->content[$index]) {
483            $this->content[$index] = $content;
484            return $index;
485        }
486        return false;
487    }
488
489    /**
490     * Returns script as text
491     */
492    public function as_text()
493    {
494        $script = '';
495        $exts = array();
496        $idx = 0;
497
498        // rules
499        foreach ($this->content as $rule) {
500            $extension = '';
501            $tests = array();
502            $i = 0;
503
504            // header
505            $script .= '# rule:[' . $rule['name'] . "]\n";
506
507            // constraints expressions
508            foreach ($rule['tests'] as $test) {
509                $tests[$i] = '';
510                switch ($test['test']) {
511                case 'size':
512                    $tests[$i] .= ($test['not'] ? 'not ' : '');
513                    $tests[$i] .= 'size :' . ($test['type']=='under' ? 'under ' : 'over ') . $test['arg'];
514                    break;
515                case 'true':
516                    $tests[$i] .= ($test['not'] ? 'not true' : 'true');
517                    break;
518                case 'exists':
519                    $tests[$i] .= ($test['not'] ? 'not ' : '');
520                    if (is_array($test['arg']))
521                        $tests[$i] .= 'exists ["' . implode('", "', $this->_escape_string($test['arg'])) . '"]';
522                    else
523                        $tests[$i] .= 'exists "' . $this->_escape_string($test['arg']) . '"';
524                    break;
525                case 'header':
526                    $tests[$i] .= ($test['not'] ? 'not ' : '');
527
528                    // relational operator + comparator
529                                        if (preg_match('/^(value|count)-([gteqnl]{2})/', $test['type'], $m)) {
530                                                array_push($exts, 'relational');
531                                                array_push($exts, 'comparator-i;ascii-numeric');
532                        $tests[$i] .= 'header :' . $m[1] . ' "' . $m[2] . '" :comparator "i;ascii-numeric"';
533                    }
534                    else
535                        $tests[$i] .= 'header :' . $test['type'];
536                   
537                    if (is_array($test['arg1']))
538                        $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg1'])) . '"]';
539                    else
540                        $tests[$i] .= ' "' . $this->_escape_string($test['arg1']) . '"';
541
542                    if (is_array($test['arg2']))
543                        $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg2'])) . '"]';
544                    else
545                        $tests[$i] .= ' "' . $this->_escape_string($test['arg2']) . '"';
546
547                    break;
548                }
549                $i++;
550            }
551
552//          $script .= ($idx>0 ? 'els' : '').($rule['join'] ? 'if allof (' : 'if anyof (');
553            // disabled rule: if false #....
554            $script .= 'if' . ($rule['disabled'] ? ' false #' : '');
555            $script .= $rule['join'] ? ' allof (' : ' anyof (';
556            if (sizeof($tests) > 1)
557                $script .= implode(", ", $tests);
558            else if (sizeof($tests))
559                $script .= $tests[0];
560            else
561                $script .= 'true';
562            $script .= ")\n{\n";
563
564            // action(s)
565            foreach ($rule['actions'] as $action) {
566                switch ($action['type']) {
567                case 'fileinto':
568                    array_push($exts, 'fileinto');
569                    $script .= "\tfileinto ";
570                    if ($action['copy']) {
571                        $script .= ':copy ';
572                        array_push($exts, 'copy');
573                    }
574                    $script .= "\"" . $this->_escape_string($action['target']) . "\";\n";
575                    break;
576                case 'redirect':
577                    $script .= "\tredirect ";
578                    if ($action['copy']) {
579                        $script .= ':copy ';
580                        array_push($exts, 'copy');
581                    }
582                    $script .= "\"" . $this->_escape_string($action['target']) . "\";\n";
583                    break;
584                case 'reject':
585                case 'ereject':
586                    array_push($exts, $action['type']);
587                    if (strpos($action['target'], "\n")!==false)
588                        $script .= "\t".$action['type']." text:\n" . $action['target'] . "\n.\n;\n";
589                    else
590                        $script .= "\t".$action['type']." \"" . $this->_escape_string($action['target']) . "\";\n";
591                    break;
592                case 'keep':
593                case 'discard':
594                case 'stop':
595                    $script .= "\t" . $action['type'] .";\n";
596                    break;
597                case 'vacation':
598                    array_push($exts, 'vacation');
599                    $script .= "\tvacation";
600                    if ($action['days'])
601                        $script .= " :days " . $action['days'];
602                    if ($action['addresses'])
603                        $script .= " :addresses " . $this->_print_list($action['addresses']);
604                    if ($action['subject'])
605                        $script .= " :subject \"" . $this->_escape_string($action['subject']) . "\"";
606                    if ($action['handle'])
607                        $script .= " :handle \"" . $this->_escape_string($action['handle']) . "\"";
608                    if ($action['from'])
609                        $script .= " :from \"" . $this->_escape_string($action['from']) . "\"";
610                    if ($action['mime'])
611                        $script .= " :mime";
612                    if (strpos($action['reason'], "\n")!==false)
613                        $script .= " text:\n" . $action['reason'] . "\n.\n;\n";
614                    else
615                        $script .= " \"" . $this->_escape_string($action['reason']) . "\";\n";
616                    break;
617                }
618            }
619
620            $script .= "}\n";
621            $idx++;
622        }
623
624        // requires
625        if (!empty($exts))
626            $script = 'require ["' . implode('","', array_unique($exts)) . "\"];\n" . $script;
627
628        return $script;
629    }
630
631    /**
632     * Returns script object
633     *
634     */
635    public function as_array()
636    {
637        return $this->content;
638    }
639
640    /**
641     * Returns array of supported extensions
642     *
643     */
644    public function get_extensions()
645    {
646        return array_values($this->supported);
647    }
648
649    /**
650     * Converts text script to rules array
651     *
652     * @param string Text script
653     */
654    private function _parse_text($script)
655    {
656        $i = 0;
657        $content = array();
658
659        // remove C comments
660        $script = preg_replace('|/\*.*?\*/|sm', '', $script);
661
662        // tokenize rules
663        if ($tokens = preg_split('/(# rule:\[.*\])\r?\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE)) {
664            foreach($tokens as $token) {
665                if (preg_match('/^# rule:\[(.*)\]/', $token, $matches)) {
666                    $content[$i]['name'] = $matches[1];
667                }
668                else if (isset($content[$i]['name']) && sizeof($content[$i]) == 1) {
669                    if ($rule = $this->_tokenize_rule($token)) {
670                        $content[$i] = array_merge($content[$i], $rule);
671                        $i++;
672                    }
673                    else // unknown rule format
674                        unset($content[$i]);
675                }
676            }
677        }
678
679        return $content;
680    }
681
682    /**
683     * Convert text script fragment to rule object
684     *
685     * @param string Text rule
686     */
687    private function _tokenize_rule($content)
688    {
689        $result = NULL;
690
691        if (preg_match('/^(if|elsif|else)\s+((true|false|not\s+true|allof|anyof|exists|header|not|size)(.*))\s+\{(.*)\}$/sm',
692            trim($content), $matches)) {
693
694            $tests = trim($matches[2]);
695
696            // disabled rule (false + comment): if false #.....
697            if ($matches[3] == 'false') {
698                $tests = preg_replace('/^false\s+#\s+/', '', $tests);
699                $disabled = true;
700            }
701            else
702                $disabled = false;
703
704            list($tests, $join) = $this->_parse_tests($tests);
705            $actions = $this->_parse_actions(trim($matches[5]));
706
707            if ($tests && $actions)
708                $result = array(
709                    'type'     => $matches[1],
710                    'tests'    => $tests,
711                    'actions'  => $actions,
712                    'join'     => $join,
713                    'disabled' => $disabled,
714            );
715        }
716
717        return $result;
718    }
719
720    /**
721     * Parse body of actions section
722     *
723     * @param string Text body
724     * @return array Array of parsed action type/target pairs
725     */
726    private function _parse_actions($content)
727    {
728        $result = NULL;
729
730        // supported actions
731        $patterns[] = '^\s*discard;';
732        $patterns[] = '^\s*keep;';
733        $patterns[] = '^\s*stop;';
734        $patterns[] = '^\s*redirect\s+(.*?[^\\\]);';
735        if (in_array('fileinto', $this->supported))
736            $patterns[] = '^\s*fileinto\s+(.*?[^\\\]);';
737        if (in_array('reject', $this->supported)) {
738            $patterns[] = '^\s*reject\s+text:(.*)\n\.\n;';
739            $patterns[] = '^\s*reject\s+(.*?[^\\\]);';
740            $patterns[] = '^\s*ereject\s+text:(.*)\n\.\n;';
741            $patterns[] = '^\s*ereject\s+(.*?[^\\\]);';
742        }
743        if (in_array('vacation', $this->supported))
744            $patterns[] = '^\s*vacation\s+(.*?[^\\\]);';
745
746        $pattern = '/(' . implode('\s*$)|(', $patterns) . '$\s*)/ms';
747
748        // parse actions body
749        if (preg_match_all($pattern, $content, $mm, PREG_SET_ORDER)) {
750            foreach ($mm as $m) {
751                $content = trim($m[0]);
752
753                if(preg_match('/^(discard|keep|stop)/', $content, $matches)) {
754                    $result[] = array('type' => $matches[1]);
755                }
756                else if(preg_match('/^fileinto/', $content)) {
757                    $target = $m[sizeof($m)-1];
758                    $copy = false;
759                    if (preg_match('/^:copy\s+/', $target)) {
760                        $target = preg_replace('/^:copy\s+/', '', $target);
761                        $copy = true;
762                    }
763                    $result[] = array('type' => 'fileinto', 'copy' => $copy,
764                        'target' => $this->_parse_string($target));
765                }
766                else if(preg_match('/^redirect/', $content)) {
767                    $target = $m[sizeof($m)-1];
768                    $copy = false;
769                    if (preg_match('/^:copy\s+/', $target)) {
770                        $target = preg_replace('/^:copy\s+/', '', $target);
771                        $copy = true;
772                    }
773                    $result[] = array('type' => 'redirect', 'copy' => $copy,
774                        'target' => $this->_parse_string($target));
775                }
776                else if(preg_match('/^(reject|ereject)\s+(.*);$/sm', $content, $matches)) {
777                    $result[] = array('type' => $matches[1], 'target' => $this->_parse_string($matches[2]));
778                }
779                else if(preg_match('/^vacation\s+(.*);$/sm', $content, $matches)) {
780                    $vacation = array('type' => 'vacation');
781
782                    if (preg_match('/:days\s+([0-9]+)/', $content, $vm)) {
783                        $vacation['days'] = $vm[1];
784                        $content = preg_replace('/:days\s+([0-9]+)/', '', $content);
785                    }
786                    if (preg_match('/:subject\s+"(.*?[^\\\])"/', $content, $vm)) {
787                        $vacation['subject'] = $vm[1];
788                        $content = preg_replace('/:subject\s+"(.*?[^\\\])"/', '', $content);
789                    }
790                    if (preg_match('/:addresses\s+\[(.*?[^\\\])\]/', $content, $vm)) {
791                        $vacation['addresses'] = $this->_parse_list($vm[1]);
792                        $content = preg_replace('/:addresses\s+\[(.*?[^\\\])\]/', '', $content);
793                    }
794                    if (preg_match('/:handle\s+"(.*?[^\\\])"/', $content, $vm)) {
795                        $vacation['handle'] = $vm[1];
796                        $content = preg_replace('/:handle\s+"(.*?[^\\\])"/', '', $content);
797                    }
798                    if (preg_match('/:from\s+"(.*?[^\\\])"/', $content, $vm)) {
799                        $vacation['from'] = $vm[1];
800                        $content = preg_replace('/:from\s+"(.*?[^\\\])"/', '', $content);
801                    }
802
803                    $content = preg_replace('/^vacation/', '', $content);
804                    $content = preg_replace('/;$/', '', $content);
805                    $content = trim($content);
806
807                    if (preg_match('/^:mime/', $content, $vm)) {
808                        $vacation['mime'] = true;
809                        $content = preg_replace('/^:mime/', '', $content);
810                    }
811
812                    $vacation['reason'] = $this->_parse_string($content);
813
814                    $result[] = $vacation;
815                }
816            }
817        }
818
819        return $result;
820    }
821
822    /**
823     * Parse test/conditions section
824     *
825     * @param string Text
826     */
827    private function _parse_tests($content)
828    {
829        $result = NULL;
830
831        // lists
832        if (preg_match('/^(allof|anyof)\s+\((.*)\)$/sm', $content, $matches)) {
833            $content = $matches[2];
834            $join = $matches[1]=='allof' ? true : false;
835        }
836        else
837            $join = false;
838
839        // supported tests regular expressions
840        // TODO: comparators, envelope
841        $patterns[] = '(not\s+)?(exists)\s+\[(.*?[^\\\])\]';
842        $patterns[] = '(not\s+)?(exists)\s+(".*?[^\\\]")';
843        $patterns[] = '(not\s+)?(true)';
844        $patterns[] = '(not\s+)?(size)\s+:(under|over)\s+([0-9]+[KGM]{0,1})';
845        $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)((\s+))\[(.*?[^\\\]")\]\s+\[(.*?[^\\\]")\]';
846        $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)((\s+))(".*?[^\\\]")\s+(".*?[^\\\]")';
847        $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)((\s+))\[(.*?[^\\\]")\]\s+(".*?[^\\\]")';
848        $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)((\s+))(".*?[^\\\]")\s+\[(.*?[^\\\]")\]';
849                $patterns[] = '(not\s+)?(header)\s+:(count\s+"[gtleqn]{2}"|value\s+"[gtleqn]{2}")(\s+:comparator\s+"(.*?[^\\\])")?\s+\[(.*?[^\\\]")\]\s+\[(.*?[^\\\]")\]';
850                $patterns[] = '(not\s+)?(header)\s+:(count\s+"[gtleqn]{2}"|value\s+"[gtleqn]{2}")(\s+:comparator\s+"(.*?[^\\\])")?\s+(".*?[^\\\]")\s+(".*?[^\\\]")';
851                $patterns[] = '(not\s+)?(header)\s+:(count\s+"[gtleqn]{2}"|value\s+"[gtleqn]{2}")(\s+:comparator\s+"(.*?[^\\\])")?\s+\[(.*?[^\\\]")\]\s+(".*?[^\\\]")';
852                $patterns[] = '(not\s+)?(header)\s+:(count\s+"[gtleqn]{2}"|value\s+"[gtleqn]{2}")(\s+:comparator\s+"(.*?[^\\\])")?\s+(".*?[^\\\]")\s+\[(.*?[^\\\]")\]';
853
854        // join patterns...
855        $pattern = '/(' . implode(')|(', $patterns) . ')/';
856
857        // ...and parse tests list
858        if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) {
859            foreach ($matches as $match) {
860                $size = sizeof($match);
861
862                if (preg_match('/^(not\s+)?size/', $match[0])) {
863                    $result[] = array(
864                        'test' => 'size',
865                        'not'  => $match[$size-4] ? true : false,
866                        'type' => $match[$size-2], // under/over
867                        'arg'  => $match[$size-1], // value
868                    );
869                }
870                else if (preg_match('/^(not\s+)?header/', $match[0])) {
871                    $type = $match[$size-5];
872                    if (preg_match('/^(count|value)\s+"([gtleqn]{2})"/', $type, $m))
873                        $type = $m[1] . '-' . $m[2];
874                   
875                    $result[] = array(
876                        'test' => 'header',
877                        'type' => $type, // is/contains/matches
878                                                'not'  => $match[$size-7] ? true : false,
879                        'arg1' => $this->_parse_list($match[$size-2]), // header(s)
880                        'arg2' => $this->_parse_list($match[$size-1]), // string(s)
881                    );
882                }
883                else if (preg_match('/^(not\s+)?exists/', $match[0])) {
884                    $result[] = array(
885                        'test' => 'exists',
886                        'not'  => $match[$size-3] ? true : false,
887                        'arg'  => $this->_parse_list($match[$size-1]), // header(s)
888                    );
889                }
890                else if (preg_match('/^(not\s+)?true/', $match[0])) {
891                    $result[] = array(
892                        'test' => 'true',
893                        'not'  => $match[$size-2] ? true : false,
894                    );
895                }
896            }
897        }
898
899        return array($result, $join);
900    }
901
902    /**
903     * Parse string value
904     *
905     * @param string Text
906     */
907    private function _parse_string($content)
908    {
909        $text = '';
910        $content = trim($content);
911
912        if (preg_match('/^text:(.*)\.$/sm', $content, $matches))
913            $text = trim($matches[1]);
914        else if (preg_match('/^"(.*)"$/', $content, $matches))
915            $text = str_replace('\"', '"', $matches[1]);
916
917        return $text;
918    }
919
920    /**
921     * Escape special chars in string value
922     *
923     * @param string Text
924     */
925    private function _escape_string($content)
926    {
927        $replace['/"/'] = '\\"';
928
929        if (is_array($content)) {
930            for ($x=0, $y=sizeof($content); $x<$y; $x++)
931                $content[$x] = preg_replace(array_keys($replace),
932                    array_values($replace), $content[$x]);
933
934            return $content;
935        }
936        else
937            return preg_replace(array_keys($replace), array_values($replace), $content);
938    }
939
940    /**
941     * Parse string or list of strings to string or array of strings
942     *
943     * @param string Text
944     */
945    private function _parse_list($content)
946    {
947        $result = array();
948
949        for ($x=0, $len=strlen($content); $x<$len; $x++) {
950            switch ($content[$x]) {
951            case '\\':
952                $str .= $content[++$x];
953                break;
954            case '"':
955                if (isset($str)) {
956                    $result[] = $str;
957                    unset($str);
958                }
959                else
960                    $str = '';
961                break;
962            default:
963                if(isset($str))
964                    $str .= $content[$x];
965            break;
966            }
967        }
968
969        if (sizeof($result)>1)
970            return $result;
971        else if (sizeof($result) == 1)
972            return $result[0];
973        else
974            return NULL;
975    }
976
977    /**
978     * Convert array of elements to list of strings
979     *
980     * @param string Text
981     */
982    private function _print_list($list)
983    {
984        $list = (array) $list;
985        foreach($list as $idx => $val)
986            $list[$idx] = $this->_escape_string($val);
987
988        return '["' . implode('","', $list) . '"]';
989    }
990}
Note: See TracBrowser for help on using the repository browser.