You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
roundcubemail/plugins/managesieve/lib/rcube_sieve.php

857 lines
22 KiB
PHP

<?php
/*
Classes for managesieve operations (using PEAR::Net_Sieve)
Author: Aleksander Machniak <alec@alec.pl>
$Id$
*/
// Sieve Language Basics: http://www.ietf.org/rfc/rfc5228.txt
define('SIEVE_ERROR_CONNECTION', 1);
define('SIEVE_ERROR_LOGIN', 2);
define('SIEVE_ERROR_NOT_EXISTS', 3); // script not exists
define('SIEVE_ERROR_INSTALL', 4); // script installation
define('SIEVE_ERROR_ACTIVATE', 5); // script activation
define('SIEVE_ERROR_DELETE', 6); // script deletion
define('SIEVE_ERROR_INTERNAL', 7); // internal error
define('SIEVE_ERROR_OTHER', 255); // other/unknown error
class rcube_sieve
{
private $sieve; // Net_Sieve object
private $error = false; // error flag
private $list = array(); // scripts list
public $script; // rcube_sieve_script object
public $current; // name of currently loaded script
private $disabled; // array of disabled extensions
/**
* Object constructor
*
* @param string Username (to managesieve login)
* @param string Password (to managesieve login)
* @param string Managesieve server hostname/address
* @param string Managesieve server port number
* @param string Enable/disable TLS use
* @param array Disabled extensions
*/
public function __construct($username, $password='', $host='localhost', $port=2000,
$usetls=true, $disabled=array(), $debug=false)
{
$this->sieve = new Net_Sieve();
if ($debug)
$this->sieve->setDebug(true, array($this, 'debug_handler'));
if (PEAR::isError($this->sieve->connect($host, $port, NULL, $usetls)))
return $this->_set_error(SIEVE_ERROR_CONNECTION);
if (PEAR::isError($this->sieve->login($username, $password)))
return $this->_set_error(SIEVE_ERROR_LOGIN);
$this->disabled = $disabled;
}
/**
* Getter for error code
*/
public function error()
{
return $this->error ? $this->error : false;
}
/**
* Saves current script into server
*/
public function save($name = null)
{
if (!$this->sieve)
return $this->_set_error(SIEVE_ERROR_INTERNAL);
if (!$this->script)
return $this->_set_error(SIEVE_ERROR_INTERNAL);
if (!$name)
$name = $this->current;
$script = $this->script->as_text();
if (!$script)
$script = '/* empty script */';
if (PEAR::isError($this->sieve->installScript($name, $script)))
return $this->_set_error(SIEVE_ERROR_INSTALL);
return true;
}
/**
* Saves text script into server
*/
public function save_script($name, $content = null)
{
if (!$this->sieve)
return $this->_set_error(SIEVE_ERROR_INTERNAL);
if (!$content)
$content = '/* empty script */';
if (PEAR::isError($this->sieve->installScript($name, $content)))
return $this->_set_error(SIEVE_ERROR_INSTALL);
return true;
}
/**
* Activates specified script
*/
public function activate($name = null)
{
if (!$this->sieve)
return $this->_set_error(SIEVE_ERROR_INTERNAL);
if (!$name)
$name = $this->current;
if (PEAR::isError($this->sieve->setActive($name)))
return $this->_set_error(SIEVE_ERROR_ACTIVATE);
return true;
}
/**
* Removes specified script
*/
public function remove($name = null)
{
if (!$this->sieve)
return $this->_set_error(SIEVE_ERROR_INTERNAL);
if (!$name)
$name = $this->current;
// script must be deactivated first
if ($name == $this->sieve->getActive())
if (PEAR::isError($this->sieve->setActive('')))
return $this->_set_error(SIEVE_ERROR_DELETE);
if (PEAR::isError($this->sieve->removeScript($name)))
return $this->_set_error(SIEVE_ERROR_DELETE);
if ($name == $this->current)
$this->current = null;
return true;
}
/**
* Gets list of supported by server Sieve extensions
*/
public function get_extensions()
{
if (!$this->sieve)
return $this->_set_error(SIEVE_ERROR_INTERNAL);
$ext = $this->sieve->getExtensions();
// we're working on lower-cased names
$ext = array_map('strtolower', (array) $ext);
if ($this->script) {
$supported = $this->script->get_extensions();
foreach ($ext as $idx => $ext_name)
if (!in_array($ext_name, $supported))
unset($ext[$idx]);
}
return array_values($ext);
}
/**
* Gets list of scripts from server
*/
public function get_scripts()
{
if (!$this->list) {
if (!$this->sieve)
return $this->_set_error(SIEVE_ERROR_INTERNAL);
$this->list = $this->sieve->listScripts();
if (PEAR::isError($this->list))
return $this->_set_error(SIEVE_ERROR_OTHER);
}
return $this->list;
}
/**
* Returns active script name
*/
public function get_active()
{
if (!$this->sieve)
return $this->_set_error(SIEVE_ERROR_INTERNAL);
return $this->sieve->getActive();
}
/**
* Loads script by name
*/
public function load($name)
{
if (!$this->sieve)
return $this->_set_error(SIEVE_ERROR_INTERNAL);
if ($this->current == $name)
return true;
$script = $this->sieve->getScript($name);
if (PEAR::isError($script))
return $this->_set_error(SIEVE_ERROR_OTHER);
// try to parse from Roundcube format
$this->script = new rcube_sieve_script($script, $this->disabled);
// ... else try Squirrelmail format
if (empty($this->script->content) && $name == 'phpscript') {
$script = $this->sieve->getScript('phpscript');
$script = $this->_convert_from_squirrel_rules($script);
$this->script = new rcube_sieve_script($script, $this->disabled);
}
$this->current = $name;
return true;
}
/**
* Creates empty script or copy of other script
*/
public function copy($name, $copy)
{
if (!$this->sieve)
return $this->_set_error(SIEVE_ERROR_INTERNAL);
if ($copy) {
$content = $this->sieve->getScript($copy);
if (PEAR::isError($content))
return $this->_set_error(SIEVE_ERROR_OTHER);
}
return $this->save_script($name, $content);
}
private function _convert_from_squirrel_rules($script)
{
$i = 0;
$name = array();
// tokenize rules
if ($tokens = preg_split('/(#START_SIEVE_RULE.*END_SIEVE_RULE)\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE))
foreach($tokens as $token) {
if (preg_match('/^#START_SIEVE_RULE.*/', $token, $matches)) {
$name[$i] = "unnamed rule ".($i+1);
$content .= "# rule:[".$name[$i]."]\n";
}
elseif (isset($name[$i])) {
$content .= "if $token\n";
$i++;
}
}
return $content;
}
private function _set_error($error)
{
$this->error = $error;
return false;
}
/**
* This is our own debug handler for connection
* @access public
*/
public function debug_handler(&$sieve, $message)
{
write_log('sieve', preg_replace('/\r\n$/', '', $message));
}
}
class rcube_sieve_script
{
public $content = array(); // script rules array
private $supported = array( // extensions supported by class
'fileinto',
'reject',
'ereject',
'vacation', // RFC5230
// TODO: (most wanted first) body, imapflags, notify, regex
);
/**
* Object constructor
*
* @param string Script's text content
* @param array Disabled extensions
*/
public function __construct($script, $disabled=NULL)
{
if (!empty($disabled))
foreach ($disabled as $ext)
if (($idx = array_search($ext, $this->supported)) !== false)
unset($this->supported[$idx]);
$this->content = $this->_parse_text($script);
}
/**
* Adds script contents as text to the script array (at the end)
*
* @param string Text script contents
*/
public function add_text($script)
{
$content = $this->_parse_text($script);
$result = false;
// check existsing script rules names
foreach ($this->content as $idx => $elem)
$names[$elem['name']] = $idx;
foreach ($content as $elem)
if (!isset($names[$elem['name']]))
{
array_push($this->content, $elem);
$result = true;
}
return $result;
}
/**
* Adds rule to the script (at the end)
*
* @param string Rule name
* @param array Rule content (as array)
*/
public function add_rule($content)
{
// TODO: check this->supported
array_push($this->content, $content);
return sizeof($this->content)-1;
}
public function delete_rule($index)
{
if(isset($this->content[$index]))
{
unset($this->content[$index]);
return true;
}
return false;
}
public function size()
{
return sizeof($this->content);
}
public function update_rule($index, $content)
{
// TODO: check this->supported
if ($this->content[$index])
{
$this->content[$index] = $content;
return $index;
}
return false;
}
/**
* Returns script as text
*/
public function as_text()
{
$script = '';
$exts = array();
$idx = 0;
// rules
foreach ($this->content as $rule)
{
$extension = '';
$tests = array();
$i = 0;
// header
$script .= '# rule:[' . $rule['name'] . "]\n";
// constraints expressions
foreach ($rule['tests'] as $test)
{
$tests[$i] = '';
switch ($test['test'])
{
case 'size':
$tests[$i] .= ($test['not'] ? 'not ' : '');
$tests[$i] .= 'size :' . ($test['type']=='under' ? 'under ' : 'over ') . $test['arg'];
break;
case 'true':
$tests[$i] .= ($test['not'] ? 'not true' : 'true');
break;
case 'exists':
$tests[$i] .= ($test['not'] ? 'not ' : '');
if (is_array($test['arg']))
$tests[$i] .= 'exists ["' . implode('", "', $this->_escape_string($test['arg'])) . '"]';
else
$tests[$i] .= 'exists "' . $this->_escape_string($test['arg']) . '"';
break;
case 'header':
$tests[$i] .= ($test['not'] ? 'not ' : '');
$tests[$i] .= 'header :' . $test['type'];
if (is_array($test['arg1']))
$tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg1'])) . '"]';
else
$tests[$i] .= ' "' . $this->_escape_string($test['arg1']) . '"';
if (is_array($test['arg2']))
$tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg2'])) . '"]';
else
$tests[$i] .= ' "' . $this->_escape_string($test['arg2']) . '"';
break;
}
$i++;
}
$script .= ($idx>0 ? 'els' : '').($rule['join'] ? 'if allof (' : 'if anyof (');
if (sizeof($tests) > 1)
$script .= implode(",\n\t", $tests);
elseif (sizeof($tests))
$script .= $tests[0];
else
$script .= 'true';
$script .= ")\n{\n";
// action(s)
foreach ($rule['actions'] as $action)
{
switch ($action['type'])
{
case 'fileinto':
$extension = 'fileinto';
$script .= "\tfileinto \"" . $this->_escape_string($action['target']) . "\";\n";
break;
case 'redirect':
$script .= "\tredirect \"" . $this->_escape_string($action['target']) . "\";\n";
break;
case 'reject':
case 'ereject':
$extension = $action['type'];
if (strpos($action['target'], "\n")!==false)
$script .= "\t".$action['type']." text:\n" . $action['target'] . "\n.\n;\n";
else
$script .= "\t".$action['type']." \"" . $this->_escape_string($action['target']) . "\";\n";
break;
case 'keep':
case 'discard':
case 'stop':
$script .= "\t" . $action['type'] .";\n";
break;
case 'vacation':
$extension = 'vacation';
$script .= "\tvacation";
if ($action['days'])
$script .= " :days " . $action['days'];
if ($action['addresses'])
$script .= " :addresses " . $this->_print_list($action['addresses']);
if ($action['subject'])
$script .= " :subject \"" . $this->_escape_string($action['subject']) . "\"";
if ($action['handle'])
$script .= " :handle \"" . $this->_escape_string($action['handle']) . "\"";
if ($action['from'])
$script .= " :from \"" . $this->_escape_string($action['from']) . "\"";
if ($action['mime'])
$script .= " :mime";
if (strpos($action['reason'], "\n")!==false)
$script .= " text:\n" . $action['reason'] . "\n.\n;\n";
else
$script .= " \"" . $this->_escape_string($action['reason']) . "\";\n";
break;
}
if ($extension && !isset($exts[$extension]))
$exts[$extension] = $extension;
}
$script .= "}\n";
$idx++;
}
// requires
if (sizeof($exts))
$script = 'require ["' . implode('","', $exts) . "\"];\n" . $script;
return $script;
}
/**
* Returns script object
*
*/
public function as_array()
{
return $this->content;
}
/**
* Returns array of supported extensions
*
*/
public function get_extensions()
{
return array_values($this->supported);
}
/**
* Converts text script to rules array
*
* @param string Text script
*/
private function _parse_text($script)
{
$i = 0;
$content = array();
// remove C comments
$script = preg_replace('|/\*.*?\*/|sm', '', $script);
// tokenize rules
if ($tokens = preg_split('/(# rule:\[.*\])\r?\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE))
foreach($tokens as $token)
{
if (preg_match('/^# rule:\[(.*)\]/', $token, $matches))
{
$content[$i]['name'] = $matches[1];
}
elseif (isset($content[$i]['name']) && sizeof($content[$i]) == 1)
{
if ($rule = $this->_tokenize_rule($token))
{
$content[$i] = array_merge($content[$i], $rule);
$i++;
}
else // unknown rule format
unset($content[$i]);
}
}
return $content;
}
/**
* Convert text script fragment to rule object
*
* @param string Text rule
*/
private function _tokenize_rule($content)
{
$result = NULL;
if (preg_match('/^(if|elsif|else)\s+((true|not\s+true|allof|anyof|exists|header|not|size)(.*))\s+\{(.*)\}$/sm', trim($content), $matches))
{
list($tests, $join) = $this->_parse_tests(trim($matches[2]));
$actions = $this->_parse_actions(trim($matches[5]));
if ($tests && $actions)
$result = array(
'tests' => $tests,
'actions' => $actions,
'join' => $join,
);
}
return $result;
}
/**
* Parse body of actions section
*
* @param string Text body
* @return array Array of parsed action type/target pairs
*/
private function _parse_actions($content)
{
$result = NULL;
// supported actions
$patterns[] = '^\s*discard;';
$patterns[] = '^\s*keep;';
$patterns[] = '^\s*stop;';
$patterns[] = '^\s*redirect\s+(.*?[^\\\]);';
if (in_array('fileinto', $this->supported))
$patterns[] = '^\s*fileinto\s+(.*?[^\\\]);';
if (in_array('reject', $this->supported)) {
$patterns[] = '^\s*reject\s+text:(.*)\n\.\n;';
$patterns[] = '^\s*reject\s+(.*?[^\\\]);';
$patterns[] = '^\s*ereject\s+text:(.*)\n\.\n;';
$patterns[] = '^\s*ereject\s+(.*?[^\\\]);';
}
if (in_array('vacation', $this->supported))
$patterns[] = '^\s*vacation\s+(.*?[^\\\]);';
$pattern = '/(' . implode('$)|(', $patterns) . '$)/ms';
// parse actions body
if (preg_match_all($pattern, $content, $mm, PREG_SET_ORDER))
{
foreach ($mm as $m)
{
$content = trim($m[0]);
if(preg_match('/^(discard|keep|stop)/', $content, $matches))
{
$result[] = array('type' => $matches[1]);
}
elseif(preg_match('/^fileinto/', $content))
{
$result[] = array('type' => 'fileinto', 'target' => $this->_parse_string($m[sizeof($m)-1]));
}
elseif(preg_match('/^redirect/', $content))
{
$result[] = array('type' => 'redirect', 'target' => $this->_parse_string($m[sizeof($m)-1]));
}
elseif(preg_match('/^(reject|ereject)\s+(.*);$/sm', $content, $matches))
{
$result[] = array('type' => $matches[1], 'target' => $this->_parse_string($matches[2]));
}
elseif(preg_match('/^vacation\s+(.*);$/sm', $content, $matches))
{
$vacation = array('type' => 'vacation');
if (preg_match('/:(days)\s+([0-9]+)/', $content, $vm)) {
$vacation['days'] = $vm[2];
$content = preg_replace('/:(days)\s+([0-9]+)/', '', $content);
}
if (preg_match('/:(subject)\s+(".*?[^\\\]")/', $content, $vm)) {
$vacation['subject'] = $vm[2];
$content = preg_replace('/:(subject)\s+(".*?[^\\\]")/', '', $content);
}
if (preg_match('/:(addresses)\s+\[(.*?[^\\\])\]/', $content, $vm)) {
$vacation['addresses'] = $this->_parse_list($vm[2]);
$content = preg_replace('/:(addresses)\s+\[(.*?[^\\\])\]/', '', $content);
}
if (preg_match('/:(handle)\s+(".*?[^\\\]")/', $content, $vm)) {
$vacation['handle'] = $vm[2];
$content = preg_replace('/:(handle)\s+(".*?[^\\\]")/', '', $content);
}
if (preg_match('/:(from)\s+(".*?[^\\\]")/', $content, $vm)) {
$vacation['from'] = $vm[2];
$content = preg_replace('/:(from)\s+(".*?[^\\\]")/', '', $content);
}
$content = preg_replace('/^vacation/', '', $content);
$content = preg_replace('/;$/', '', $content);
$content = trim($content);
if (preg_match('/^:(mime)/', $content, $vm)) {
$vacation['mime'] = true;
$content = preg_replace('/^:mime/', '', $content);
}
$vacation['reason'] = $this->_parse_string($content);
$result[] = $vacation;
}
}
}
return $result;
}
/**
* Parse test/conditions section
*
* @param string Text
*/
private function _parse_tests($content)
{
$result = NULL;
// lists
if (preg_match('/^(allof|anyof)\s+\((.*)\)$/sm', $content, $matches))
{
$content = $matches[2];
$join = $matches[1]=='allof' ? true : false;
}
else
$join = false;
// supported tests regular expressions
// TODO: comparators, envelope
$patterns[] = '(not\s+)?(exists)\s+\[(.*?[^\\\])\]';
$patterns[] = '(not\s+)?(exists)\s+(".*?[^\\\]")';
$patterns[] = '(not\s+)?(true)';
$patterns[] = '(not\s+)?(size)\s+:(under|over)\s+([0-9]+[KGM]{0,1})';
$patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+\[(.*?[^\\\]")\]\s+\[(.*?[^\\\]")\]';
$patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+(".*?[^\\\]")\s+(".*?[^\\\]")';
$patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+\[(.*?[^\\\]")\]\s+(".*?[^\\\]")';
$patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+(".*?[^\\\]")\s+\[(.*?[^\\\]")\]';
// join patterns...
$pattern = '/(' . implode(')|(', $patterns) . ')/';
// ...and parse tests list
if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER))
{
foreach ($matches as $match)
{
$size = sizeof($match);
if (preg_match('/^(not\s+)?size/', $match[0]))
{
$result[] = array(
'test' => 'size',
'not' => $match[$size-4] ? true : false,
'type' => $match[$size-2], // under/over
'arg' => $match[$size-1], // value
);
}
elseif (preg_match('/^(not\s+)?header/', $match[0]))
{
$result[] = array(
'test' => 'header',
'not' => $match[$size-5] ? true : false,
'type' => $match[$size-3], // is/contains/matches
'arg1' => $this->_parse_list($match[$size-2]), // header(s)
'arg2' => $this->_parse_list($match[$size-1]), // string(s)
);
}
elseif (preg_match('/^(not\s+)?exists/', $match[0]))
{
$result[] = array(
'test' => 'exists',
'not' => $match[$size-3] ? true : false,
'arg' => $this->_parse_list($match[$size-1]), // header(s)
);
}
elseif (preg_match('/^(not\s+)?true/', $match[0]))
{
$result[] = array(
'test' => 'true',
'not' => $match[$size-2] ? true : false,
);
}
}
}
return array($result, $join);
}
/**
* Parse string value
*
* @param string Text
*/
private function _parse_string($content)
{
$text = '';
$content = trim($content);
if (preg_match('/^text:(.*)\.$/sm', $content, $matches))
$text = trim($matches[1]);
elseif (preg_match('/^"(.*)"$/', $content, $matches))
$text = str_replace('\"', '"', $matches[1]);
return $text;
}
/**
* Escape special chars in string value
*
* @param string Text
*/
private function _escape_string($content)
{
$replace['/"/'] = '\\"';
if (is_array($content))
{
for ($x=0, $y=sizeof($content); $x<$y; $x++)
$content[$x] = preg_replace(array_keys($replace), array_values($replace), $content[$x]);
return $content;
}
else
return preg_replace(array_keys($replace), array_values($replace), $content);
}
/**
* Parse string or list of strings to string or array of strings
*
* @param string Text
*/
private function _parse_list($content)
{
$result = array();
for ($x=0, $len=strlen($content); $x<$len; $x++)
{
switch ($content[$x])
{
case '\\':
$str .= $content[++$x];
break;
case '"':
if (isset($str))
{
$result[] = $str;
unset($str);
}
else
$str = '';
break;
default:
if(isset($str))
$str .= $content[$x];
break;
}
}
if (sizeof($result)>1)
return $result;
elseif (sizeof($result) == 1)
return $result[0];
else
return NULL;
}
/**
* Convert array of elements to list of strings
*
* @param string Text
*/
private function _print_list($list)
{
$list = (array) $list;
foreach($list as $idx => $val)
$list[$idx] = $this->_escape_string($val);
return '["' . implode('","', $list) . '"]';
}
}
?>