4 Classes for managesieve operations (using PEAR::Net_Sieve)
6 Author: Aleksander Machniak <alec@alec.pl>
12 // Sieve Language Basics: http://www.ietf.org/rfc/rfc5228.txt
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
24 var $sieve; // Net_Sieve object
25 var $error = false; // error flag
26 var $list = array(); // scripts list
28 public $script; // rcube_sieve_script object
29 private $disabled; // array of disabled extensions
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
41 public function __construct($username, $password='', $host='localhost', $port=2000, $usetls=true, $disabled=array())
43 $this->sieve = new Net_Sieve();
45 // $this->sieve->setDebug();
46 if (PEAR::isError($this->sieve->connect($host, $port, NULL, $usetls)))
47 return $this->_set_error(SIEVE_ERROR_CONNECTION);
49 if (PEAR::isError($this->sieve->login($username, $password)))
50 return $this->_set_error(SIEVE_ERROR_LOGIN);
52 $this->disabled = $disabled;
57 * Getter for error code
59 public function error()
61 return $this->error ? $this->error : false;
64 public function save()
66 $script = $this->script->as_text();
69 $script = '/* empty script */';
71 if (PEAR::isError($this->sieve->installScript('roundcube', $script)))
72 return $this->_set_error(SIEVE_ERROR_INSTALL);
74 if (PEAR::isError($this->sieve->setActive('roundcube')))
75 return $this->_set_error(SIEVE_ERROR_ACTIVATE);
80 public function get_extensions()
83 $ext = $this->sieve->getExtensions();
86 $supported = $this->script->get_extensions();
87 foreach ($ext as $idx => $ext_name)
88 if (!in_array($ext_name, $supported))
92 return array_values($ext);
96 private function _get_script()
101 $this->list = $this->sieve->listScripts();
103 if (PEAR::isError($this->list))
104 return $this->_set_error(SIEVE_ERROR_OTHER);
106 if (in_array('roundcube', $this->list))
108 $script = $this->sieve->getScript('roundcube');
110 if (PEAR::isError($script))
111 return $this->_set_error(SIEVE_ERROR_OTHER);
113 // import scripts from squirrelmail
114 elseif (in_array('phpscript', $this->list))
116 $script = $this->sieve->getScript('phpscript');
118 $script = $this->_convert_from_squirrel_rules($script);
120 $this->script = new rcube_sieve_script($script);
124 $script = $this->sieve->getScript('roundcube');
126 if (PEAR::isError($script))
127 return $this->_set_error(SIEVE_ERROR_OTHER);
131 $this->_set_error(SIEVE_ERROR_NOT_EXISTS);
135 $this->script = new rcube_sieve_script($script, $this->disabled);
138 private function _convert_from_squirrel_rules($script)
143 if ($tokens = preg_split('/(#START_SIEVE_RULE.*END_SIEVE_RULE)\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE))
144 foreach($tokens as $token)
146 if (preg_match('/^#START_SIEVE_RULE.*/', $token, $matches))
148 $name[$i] = "unnamed rule ".($i+1);
149 $content .= "# rule:[".$name[$i]."]\n";
151 elseif (isset($name[$i]))
153 $content .= "if ".$token."\n";
162 private function _set_error($error)
164 $this->error = $error;
169 class rcube_sieve_script
171 var $content = array(); // script rules array
173 private $supported = array( // extensions supported by class
177 'vacation', // RFC5230
178 // TODO: (most wanted first) body, imapflags, notify, regex
184 * @param string Script's text content
185 * @param array Disabled extensions
187 public function __construct($script, $disabled)
189 if (!empty($disabled))
190 foreach ($disabled as $ext)
191 if (($idx = array_search($ext, $this->supported)) !== false)
192 unset($this->supported[$idx]);
194 $this->content = $this->_parse_text($script);
198 * Adds script contents as text to the script array (at the end)
200 * @param string Text script contents
202 public function add_text($script)
204 $content = $this->_parse_text($script);
207 // check existsing script rules names
208 foreach ($this->content as $idx => $elem)
209 $names[$elem['name']] = $idx;
211 foreach ($content as $elem)
212 if (!isset($names[$elem['name']]))
214 array_push($this->content, $elem);
222 * Adds rule to the script (at the end)
224 * @param string Rule name
225 * @param array Rule content (as array)
227 public function add_rule($content)
229 // TODO: check this->supported
230 array_push($this->content, $content);
231 return sizeof($this->content)-1;
234 public function delete_rule($index)
236 if(isset($this->content[$index]))
238 unset($this->content[$index]);
244 public function size()
246 return sizeof($this->content);
249 public function update_rule($index, $content)
251 // TODO: check this->supported
252 if ($this->content[$index])
254 $this->content[$index] = $content;
261 * Returns script as text
263 public function as_text()
269 foreach ($this->content as $idx => $rule)
276 $script .= '# rule:[' . $rule['name'] . "]\n";
278 // constraints expressions
279 foreach ($rule['tests'] as $test)
282 switch ($test['test'])
285 $tests[$i] .= ($test['not'] ? 'not ' : '');
286 $tests[$i] .= 'size :' . ($test['type']=='under' ? 'under ' : 'over ') . $test['arg'];
289 $tests[$i] .= ($test['not'] ? 'not true' : 'true');
292 $tests[$i] .= ($test['not'] ? 'not ' : '');
293 if (is_array($test['arg']))
294 $tests[$i] .= 'exists ["' . implode('", "', $this->_escape_string($test['arg'])) . '"]';
296 $tests[$i] .= 'exists "' . $this->_escape_string($test['arg']) . '"';
299 $tests[$i] .= ($test['not'] ? 'not ' : '');
300 $tests[$i] .= 'header :' . $test['type'];
301 if (is_array($test['arg1']))
302 $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg1'])) . '"]';
304 $tests[$i] .= ' "' . $this->_escape_string($test['arg1']) . '"';
305 if (is_array($test['arg2']))
306 $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg2'])) . '"]';
308 $tests[$i] .= ' "' . $this->_escape_string($test['arg2']) . '"';
314 $script .= ($idx>0 ? 'els' : '').($rule['join'] ? 'if allof (' : 'if anyof (');
315 if (sizeof($tests) > 1)
316 $script .= implode(",\n\t", $tests);
317 elseif (sizeof($tests))
318 $script .= $tests[0];
324 foreach ($rule['actions'] as $action)
325 switch ($action['type'])
328 $extension = 'fileinto';
329 $script .= "\tfileinto \"" . $this->_escape_string($action['target']) . "\";\n";
332 $script .= "\tredirect \"" . $this->_escape_string($action['target']) . "\";\n";
336 $extension = $action['type'];
337 if (strpos($action['target'], "\n")!==false)
338 $script .= "\t".$action['type']." text:\n" . $action['target'] . "\n.\n;\n";
340 $script .= "\t".$action['type']." \"" . $this->_escape_string($action['target']) . "\";\n";
345 $script .= "\t" . $action['type'] .";\n";
348 $extension = 'vacation';
349 $script .= "\tvacation";
351 $script .= " :days " . $action['days'];
352 if ($action['addresses'])
353 $script .= " :addresses " . $this->_print_list($action['addresses']);
354 if ($action['subject'])
355 $script .= " :subject \"" . $this->_escape_string($action['subject']) . "\"";
356 if ($action['handle'])
357 $script .= " :handle \"" . $this->_escape_string($action['handle']) . "\"";
359 $script .= " :from \"" . $this->_escape_string($action['from']) . "\"";
362 if (strpos($action['reason'], "\n")!==false)
363 $script .= " text:\n" . $action['reason'] . "\n.\n;\n";
365 $script .= " \"" . $this->_escape_string($action['reason']) . "\";\n";
371 if ($extension && !isset($exts[$extension]))
372 $exts[$extension] = $extension;
377 $script = 'require ["' . implode('","', $exts) . "\"];\n" . $script;
383 * Returns script object
386 public function as_array()
388 return $this->content;
392 * Returns array of supported extensions
395 public function get_extensions()
397 return array_values($this->supported);
401 * Converts text script to rules array
403 * @param string Text script
405 private function _parse_text($script)
411 $script = preg_replace('|/\*.*?\*/|sm', '', $script);
414 if ($tokens = preg_split('/(# rule:\[.*\])\r?\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE))
415 foreach($tokens as $token)
417 if (preg_match('/^# rule:\[(.*)\]/', $token, $matches))
419 $content[$i]['name'] = $matches[1];
421 elseif (isset($content[$i]['name']) && sizeof($content[$i]) == 1)
423 if ($rule = $this->_tokenize_rule($token))
425 $content[$i] = array_merge($content[$i], $rule);
428 else // unknown rule format
437 * Convert text script fragment to rule object
439 * @param string Text rule
441 private function _tokenize_rule($content)
445 if (preg_match('/^(if|elsif|else)\s+((allof|anyof|exists|header|not|size)\s+(.*))\s+\{(.*)\}$/sm', trim($content), $matches))
447 list($tests, $join) = $this->_parse_tests(trim($matches[2]));
448 $actions = $this->_parse_actions(trim($matches[5]));
450 if ($tests && $actions)
453 'actions' => $actions,
462 * Parse body of actions section
464 * @param string Text body
465 * @return array Array of parsed action type/target pairs
467 private function _parse_actions($content)
472 $patterns[] = '^\s*discard;';
473 $patterns[] = '^\s*keep;';
474 $patterns[] = '^\s*stop;';
475 $patterns[] = '^\s*redirect\s+(.*?[^\\\]);';
476 if (in_array('fileinto', $this->supported))
477 $patterns[] = '^\s*fileinto\s+(.*?[^\\\]);';
478 if (in_array('reject', $this->supported)) {
479 $patterns[] = '^\s*reject\s+text:(.*)\n\.\n;';
480 $patterns[] = '^\s*reject\s+(.*?[^\\\]);';
481 $patterns[] = '^\s*ereject\s+text:(.*)\n\.\n;';
482 $patterns[] = '^\s*ereject\s+(.*?[^\\\]);';
484 if (in_array('vacation', $this->supported))
485 $patterns[] = '^\s*vacation\s+(.*?[^\\\]);';
487 $pattern = '/(' . implode('$)|(', $patterns) . '$)/ms';
489 // parse actions body
490 if (preg_match_all($pattern, $content, $mm, PREG_SET_ORDER))
494 $content = trim($m[0]);
496 if(preg_match('/^(discard|keep|stop)/', $content, $matches))
498 $result[] = array('type' => $matches[1]);
500 elseif(preg_match('/^fileinto/', $content))
502 $result[] = array('type' => 'fileinto', 'target' => $this->_parse_string($m[sizeof($m)-1]));
504 elseif(preg_match('/^redirect/', $content))
506 $result[] = array('type' => 'redirect', 'target' => $this->_parse_string($m[sizeof($m)-1]));
508 elseif(preg_match('/^(reject|ereject)\s+(.*);$/sm', $content, $matches))
510 $result[] = array('type' => $matches[1], 'target' => $this->_parse_string($matches[2]));
512 elseif(preg_match('/^vacation\s+(.*);$/sm', $content, $matches))
514 $vacation = array('type' => 'vacation');
516 if (preg_match('/:(days)\s+([0-9]+)/', $content, $vm)) {
517 $vacation['days'] = $vm[2];
518 $content = preg_replace('/:(days)\s+([0-9]+)/', '', $content);
520 if (preg_match('/:(subject)\s+(".*?[^\\\]")/', $content, $vm)) {
521 $vacation['subject'] = $vm[2];
522 $content = preg_replace('/:(subject)\s+(".*?[^\\\]")/', '', $content);
524 if (preg_match('/:(addresses)\s+\[(.*?[^\\\])\]/', $content, $vm)) {
525 $vacation['addresses'] = $this->_parse_list($vm[2]);
526 $content = preg_replace('/:(addresses)\s+\[(.*?[^\\\])\]/', '', $content);
528 if (preg_match('/:(handle)\s+(".*?[^\\\]")/', $content, $vm)) {
529 $vacation['handle'] = $vm[2];
530 $content = preg_replace('/:(handle)\s+(".*?[^\\\]")/', '', $content);
532 if (preg_match('/:(from)\s+(".*?[^\\\]")/', $content, $vm)) {
533 $vacation['from'] = $vm[2];
534 $content = preg_replace('/:(from)\s+(".*?[^\\\]")/', '', $content);
536 $content = preg_replace('/^vacation/', '', $content);
537 $content = preg_replace('/;$/', '', $content);
538 $content = trim($content);
539 if (preg_match('/^:(mime)/', $content, $vm)) {
540 $vacation['mime'] = true;
541 $content = preg_replace('/^:mime/', '', $content);
544 $vacation['reason'] = $this->_parse_string($content);
546 $result[] = $vacation;
555 * Parse test/conditions section
560 private function _parse_tests($content)
565 if (preg_match('/^(allof|anyof)\s+\((.*)\)$/sm', $content, $matches))
567 $content = $matches[2];
568 $join = $matches[1]=='allof' ? true : false;
573 // supported tests regular expressions
574 // TODO: comparators, envelope
575 $patterns[] = '(not\s+)?(exists)\s+\[(.*?[^\\\])\]';
576 $patterns[] = '(not\s+)?(exists)\s+(".*?[^\\\]")';
577 $patterns[] = '(not\s+)?(true)';
578 $patterns[] = '(not\s+)?(size)\s+:(under|over)\s+([0-9]+[KGM]{0,1})';
579 $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+\[(.*?[^\\\]")\]\s+\[(.*?[^\\\]")\]';
580 $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+(".*?[^\\\]")\s+(".*?[^\\\]")';
581 $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+\[(.*?[^\\\]")\]\s+(".*?[^\\\]")';
582 $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+(".*?[^\\\]")\s+\[(.*?[^\\\]")\]';
585 $pattern = '/(' . implode(')|(', $patterns) . ')/';
587 // ...and parse tests list
588 if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER))
590 foreach ($matches as $match)
592 $size = sizeof($match);
594 if (preg_match('/^(not\s+)?size/', $match[0]))
598 'not' => $match[$size-4] ? true : false,
599 'type' => $match[$size-2], // under/over
600 'arg' => $match[$size-1], // value
603 elseif (preg_match('/^(not\s+)?header/', $match[0]))
607 'not' => $match[$size-5] ? true : false,
608 'type' => $match[$size-3], // is/contains/matches
609 'arg1' => $this->_parse_list($match[$size-2]), // header(s)
610 'arg2' => $this->_parse_list($match[$size-1]), // string(s)
613 elseif (preg_match('/^(not\s+)?exists/', $match[0]))
617 'not' => $match[$size-3] ? true : false,
618 'arg' => $this->_parse_list($match[$size-1]), // header(s)
621 elseif (preg_match('/^(not\s+)?true/', $match[0]))
625 'not' => $match[$size-2] ? true : false,
631 return array($result, $join);
639 private function _parse_string($content)
642 $content = trim($content);
644 if (preg_match('/^text:(.*)\.$/sm', $content, $matches))
645 $text = trim($matches[1]);
646 elseif (preg_match('/^"(.*)"$/', $content, $matches))
647 $text = str_replace('\"', '"', $matches[1]);
653 * Escape special chars in string value
657 private function _escape_string($content)
659 $replace['/"/'] = '\\"';
661 if (is_array($content))
663 for ($x=0, $y=sizeof($content); $x<$y; $x++)
664 $content[$x] = preg_replace(array_keys($replace), array_values($replace), $content[$x]);
669 return preg_replace(array_keys($replace), array_values($replace), $content);
673 * Parse string or list of strings to string or array of strings
677 private function _parse_list($content)
681 for ($x=0, $len=strlen($content); $x<$len; $x++)
683 switch ($content[$x])
686 $str .= $content[++$x];
699 $str .= $content[$x];
704 if (sizeof($result)>1)
706 elseif (sizeof($result) == 1)
713 * Convert array of elements to list of strings
717 private function _print_list($list)
719 $list = (array) $list;
720 foreach($list as $idx => $val)
721 $list[$idx] = $this->_escape_string($val);
723 return '["' . implode('","', $list) . '"]';