| | Modifications by: Philip Weir | | * Changed name of keys in script array | | * Added support for address and envelope | | * Added support for vacation | | * Added support for disabled rules (written to file as comment) | | * Added support for regex tests | | * Added support for imapflags | | * Added support for relational operators and comparators | | * Added support for subaddress tests | | * Added support for notify action | | * Added support for stop action | | * Added support for body and copy | | * Added support for spamtest and virustest | | * Added support for date | | * Added support for editheader | +-----------------------------------------------------------------------+ */ define('SIEVE_ERROR_BAD_ACTION', 1); define('SIEVE_ERROR_NOT_FOUND', 2); // define constants for sieve file if (!defined('RCUBE_SIEVE_NEWLINE')) define('RCUBE_SIEVE_NEWLINE', "\r\n"); if (!defined('RCUBE_SIEVE_INDENT')) define('RCUBE_SIEVE_INDENT', "\t"); if (!defined('RCUBE_SIEVE_HEADER')) define('RCUBE_SIEVE_HEADER', "## Generated by Roundcube Webmail SieveRules Plugin ##"); class rcube_sieve_script { private $elsif = true; private $content = array(); private $supported = array( 'fileinto', 'reject', 'ereject', 'vacation', 'imapflags', 'imap4flags', 'notify', 'enotify', 'spamtest', 'virustest', 'date', 'editheader' ); public $raw = ''; public function __construct($script, $ext = array(), $elsif = true) { $this->raw = $script; $this->elsif = $elsif; // adjust supported extenstion to match sieve server $this->supported = array_intersect($this->supported, $ext); if (in_array('copy', $ext)) $this->supported = array_merge($this->supported, array('fileinto_copy','redirect_copy')); if (in_array('editheader', $ext)) $this->supported = array_merge($this->supported, array('editheaderadd','editheaderrem')); // include standard actions in supported list $this->supported = array_merge($this->supported, array('redirect','keep','discard','stop')); // load script $this->content = $this->parse_text($script); } 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; } public function import_filters($content) { if (is_array($content)) { $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; } } } else { $this->add_text($content); } } public function add_rule($content, $pos = null) { foreach ($content['actions'] as $action) { if (!in_array($action['type'], $this->supported)) return SIEVE_ERROR_BAD_ACTION; } if ($pos !== null) array_splice($this->content, $pos, 0, array($content)); else array_push($this->content, $content); return true; } public function delete_rule($index) { if (isset($this->content[$index])) { unset($this->content[$index]); $this->content = array_values($this->content); return true; } return SIEVE_ERROR_NOT_FOUND; } public function size() { return sizeof($this->content); } public function update_rule($index, $content) { foreach ($content['actions'] as $action) { if (!in_array($action['type'], $this->supported)) return SIEVE_ERROR_BAD_ACTION; } if ($this->content[$index]) { $this->content[$index] = $content; return true; } return SIEVE_ERROR_NOT_FOUND; } public function move_rule($source, $destination) { $this->add_rule($this->content[$source], $destination); if ($source < $destination) $this->delete_rule($source); else $this->delete_rule($source + 1); } public function as_text() { $script = ''; $variables = ''; $exts = array(); // rules $activeRules = 0; foreach ($this->content as $rule) { $tests = array(); $i = 0; if ($rule['disabled'] == 1) { $script .= '# rule:[' . $rule['name'] . "]" . RCUBE_SIEVE_NEWLINE; $script .= '# disabledRule:[' . $this->_safe_serial(serialize($rule)) . "]" . RCUBE_SIEVE_NEWLINE; } else { // header $script .= '# rule:[' . $rule['name'] . "]" . RCUBE_SIEVE_NEWLINE; // constraints expressions foreach ($rule['tests'] as $test) { $tests[$i] = ''; switch ($test['type']) { case 'size': $tests[$i] .= ($test['not'] ? 'not ' : ''); $tests[$i] .= 'size :' . ($test['operator']=='under' ? 'under ' : 'over ') . $test['target']; break; case 'virustest': case 'spamtest': array_push($exts, $test['type']); array_push($exts, 'relational'); array_push($exts, 'comparator-i;ascii-numeric'); $tests[$i] .= ($test['not'] ? 'not ' : ''); $tests[$i] .= $test['type'] . ' :value ' . ($test['operator'] == 'eq' ? '"eq" ' : ($test['operator'] == 'le' ? '"le" ' : '"ge" ')) . ':comparator "i;ascii-numeric" "' . $test['target'] .'"'; break; case 'true': $tests[$i] .= ($test['not'] ? 'not true' : 'true'); break; case 'exists': $tests[$i] .= ($test['not'] ? 'not ' : ''); if (is_array($test['header'])) $tests[$i] .= 'exists ["' . implode('", "', $this->_escape_string($test['header'])) . '"]'; else $tests[$i] .= 'exists "' . $this->_escape_string($test['header']) . '"'; break; case 'envelope': array_push($exts, 'envelope'); case 'header': case 'address': if ($test['operator'] == 'regex') array_push($exts, 'regex'); elseif (substr($test['operator'], 0, 5) == 'count' || substr($test['operator'], 0, 5) == 'value') array_push($exts, 'relational'); elseif ($test['operator'] == 'user' || $test['operator'] == 'detail' || $test['operator'] == 'domain') array_push($exts, 'subaddress'); $tests[$i] .= ($test['not'] ? 'not ' : ''); $tests[$i] .= $test['type']. ' :' . $test['operator']; if ($test['comparator'] != '') { if ($test['comparator'] != 'i;ascii-casemap' && $test['comparator'] != 'i;octet') array_push($exts, 'comparator-' . $test['comparator']); $tests[$i] .= ' :comparator "' . $test['comparator'] . '"'; } if (is_array($test['header'])) $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['header'])) . '"]'; else $tests[$i] .= ' "' . $this->_escape_string($test['header']) . '"'; if (is_array($test['target'])) $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['target'])) . '"]'; else $tests[$i] .= ' "' . $this->_escape_string($test['target']) . '"'; break; case 'body': array_push($exts, 'body'); if ($test['operator'] == 'regex') array_push($exts, 'regex'); elseif (substr($test['operator'], 0, 5) == 'count' || substr($test['operator'], 0, 5) == 'value') array_push($exts, 'relational'); $tests[$i] .= ($test['not'] ? 'not ' : ''); $tests[$i] .= $test['type']; if ($test['bodypart'] != '') $tests[$i] .= ' :' . $test['bodypart']; if ($test['contentpart'] != '') $tests[$i] .= ' "'. $test['contentpart'] .'"'; $tests[$i] .= ' :' . $test['operator']; if ($test['comparator'] != '') { if ($test['comparator'] != 'i;ascii-casemap' && $test['comparator'] != 'i;octet') array_push($exts, 'comparator-' . $test['comparator']); $tests[$i] .= ' :comparator "' . $test['comparator'] . '"'; } if (is_array($test['target'])) $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['target'])) . '"]'; else $tests[$i] .= ' "' . $this->_escape_string($test['target']) . '"'; break; case 'date': array_push($exts, 'date'); if ($test['operator'] == 'regex') array_push($exts, 'regex'); elseif (substr($test['operator'], 0, 5) == 'count' || substr($test['operator'], 0, 5) == 'value') array_push($exts, 'relational'); $tests[$i] .= ($test['not'] ? 'not ' : ''); $tests[$i] .= $test['header']; $timezone = rcube::get_instance()->config->get('timezone', 'auto'); if ($timezone != 'auto') { $tz = new DateTimeZone($timezone); $date = new DateTime('now', $tz); $tests[$i] .= ' :zone ' . '"' . $date->format('O') . '"'; } $tests[$i] .= ' :' . $test['operator']; if ($test['comparator'] != '') { if ($test['comparator'] != 'i;ascii-casemap' && $test['comparator'] != 'i;octet') array_push($exts, 'comparator-' . $test['comparator']); $tests[$i] .= ' :comparator "' . $test['comparator'] . '"'; } $tests[$i] .= ' "' . $this->_escape_string($test['datepart']) . '"'; $tests[$i] .= ' "' . $this->_escape_string($test['target']) . '"'; break; } $i++; } $script .= ($activeRules > 0 && $this->elsif ? 'els' : '') . ($rule['join'] ? 'if allof (' : 'if anyof ('); $activeRules++; if (sizeof($tests) > 1) $script .= implode("," . RCUBE_SIEVE_NEWLINE . RCUBE_SIEVE_INDENT, $tests); elseif (sizeof($tests)) $script .= $tests[0]; else $script .= 'true'; $script .= ")". RCUBE_SIEVE_NEWLINE ."{" . RCUBE_SIEVE_NEWLINE; // action(s) $actions = ''; foreach ($rule['actions'] as $action) { switch ($action['type']) { case 'fileinto': case 'fileinto_copy': array_push($exts, 'fileinto'); $args = ''; if ($action['type'] == 'fileinto_copy') { array_push($exts, 'copy'); $args .= ' :copy'; } if ($action['create']) { array_push($exts, 'mailbox'); $args .= ' :create'; } // variables support in fileinto by David Warden if (preg_match('/\$\{\d+\}/', $action['target'])) array_push($exts, 'variables'); $actions .= RCUBE_SIEVE_INDENT . "fileinto" . $args . " \"" . $this->_escape_string($action['target']) . "\";" . RCUBE_SIEVE_NEWLINE; break; case 'redirect': case 'redirect_copy': $args = ''; if ($action['type'] == 'redirect_copy') { array_push($exts, 'copy'); $args .= ' :copy'; } $tokens = preg_split('/[,;\s]/', $action['target']); foreach ($tokens as $email) $actions .= RCUBE_SIEVE_INDENT . "redirect" . $args . " \"" . $this->_escape_string($email) . "\";" . RCUBE_SIEVE_NEWLINE; break; case 'reject': case 'ereject': array_push($exts, $action['type']); if (strpos($action['target'], "\n")!==false) $actions .= RCUBE_SIEVE_INDENT . $action['type']." text:" . RCUBE_SIEVE_NEWLINE . $action['target'] . RCUBE_SIEVE_NEWLINE . "." . RCUBE_SIEVE_NEWLINE . ";" . RCUBE_SIEVE_NEWLINE; else $actions .= RCUBE_SIEVE_INDENT . $action['type']." \"" . $this->_escape_string($action['target']) . "\";" . RCUBE_SIEVE_NEWLINE; break; case 'vacation': array_push($exts, 'vacation'); $action['subject'] = $this->_escape_string($action['subject']); // // encoding subject header with mb_encode provides better results with asian characters // if (function_exists("mb_encode_mimeheader")) // { // mb_internal_encoding($action['charset']); // $action['subject'] = mb_encode_mimeheader($action['subject'], $action['charset'], 'Q'); // mb_internal_encoding(RCUBE_CHARSET); // } // detect original recipient if ($action['from'] == 'auto' && strpos($variables, 'set "from"') === false) { array_push($exts, 'variables'); $variables .= 'set "from" "";' . RCUBE_SIEVE_NEWLINE; $variables .= 'if header :matches "to" "*" {' . RCUBE_SIEVE_NEWLINE; $variables .= RCUBE_SIEVE_INDENT . 'set "from" "${1}";' . RCUBE_SIEVE_NEWLINE; $variables .= '}' . RCUBE_SIEVE_NEWLINE; $action['from'] = "\${from}"; } elseif ($action['from'] == 'auto') { $action['from'] = "\${from}"; } // append original subject if ($action['origsubject'] == '1' && strpos($variables, 'set "subject"') === false) { array_push($exts, 'variables'); $variables .= 'set "subject" "";' . RCUBE_SIEVE_NEWLINE; $variables .= 'if header :matches "subject" "*" {' . RCUBE_SIEVE_NEWLINE; $variables .= RCUBE_SIEVE_INDENT . 'set "subject" "${1}";' . RCUBE_SIEVE_NEWLINE; $variables .= '}' . RCUBE_SIEVE_NEWLINE; $action['subject'] = trim($action['subject']); if (substr($action['subject'], -1, 1) != ":") $action['subject'] .= ":"; $action['subject'] .= " \${subject}"; } $actions .= RCUBE_SIEVE_INDENT . "vacation" . RCUBE_SIEVE_NEWLINE; if (!empty($action['days'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":days ". $action['days'] . RCUBE_SIEVE_NEWLINE; if (!empty($action['addresses'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":addresses [\"". str_replace(",", "\",\"", $this->_escape_string($action['addresses'])) ."\"]" . RCUBE_SIEVE_NEWLINE; if (!empty($action['subject'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":subject \"". $action['subject'] ."\"" . RCUBE_SIEVE_NEWLINE; if (!empty($action['handle'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":handle \"". $this->_escape_string($action['handle']) ."\"" . RCUBE_SIEVE_NEWLINE; if (!empty($action['from'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":from \"". $this->_escape_string($action['from']) ."\"" . RCUBE_SIEVE_NEWLINE; if ($action['htmlmsg']) { $MAIL_MIME = new Mail_mime("\r\n"); $action['msg'] = '' . "\r\n\r\n" . $action['msg'] . "\r\n\r\n"; $MAIL_MIME->setHTMLBody($action['msg']); // add a plain text version of the e-mail as an alternative part. $h2t = new rcube_html2text($action['msg'], false, true, 0); $plainTextPart = $h2t->get_text(); if (!$plainTextPart) { // empty message body breaks attachment handling in drafts $plainTextPart = "\r\n"; } else { // make sure all line endings are CRLF (#1486712) $plainTextPart = preg_replace('/\r?\n/', "\r\n", $plainTextPart); } $MAIL_MIME->setTXTBody($plainTextPart); $MAIL_MIME->setParam('html_charset', $action['charset']); $MAIL_MIME->setParam('text_charset', $action['charset']); $action['msg'] = $MAIL_MIME->getMessage(); } // escape lines which start is a . $action['msg'] = preg_replace('/(^|\r?\n)\./', "$1..", $action['msg']); if ($action['htmlmsg']) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":mime text:". RCUBE_SIEVE_NEWLINE . $action['msg'] . RCUBE_SIEVE_NEWLINE . "." . RCUBE_SIEVE_NEWLINE . ";" . RCUBE_SIEVE_NEWLINE; elseif ($action['charset'] != "UTF-8") $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":mime text:". RCUBE_SIEVE_NEWLINE ."Content-Type: text/plain; charset=". $action['charset'] . RCUBE_SIEVE_NEWLINE . RCUBE_SIEVE_NEWLINE . $action['msg'] . RCUBE_SIEVE_NEWLINE . "." . RCUBE_SIEVE_NEWLINE . ";" . RCUBE_SIEVE_NEWLINE; elseif (strpos($action['msg'], "\n") !== false) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . "text:" . RCUBE_SIEVE_NEWLINE . $action['msg'] . RCUBE_SIEVE_NEWLINE . "." . RCUBE_SIEVE_NEWLINE . ";" . RCUBE_SIEVE_NEWLINE; else $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . "\"" . $this->_escape_string($action['msg']) . "\";" . RCUBE_SIEVE_NEWLINE; break; case 'imapflags': case 'imap4flags': array_push($exts, $action['type']); if (strpos($actions, "setflag") !== false) $actions .= RCUBE_SIEVE_INDENT . "addflag \"" . $this->_escape_string($action['target']) . "\";" . RCUBE_SIEVE_NEWLINE; else $actions .= RCUBE_SIEVE_INDENT . "setflag \"" . $this->_escape_string($action['target']) . "\";" . RCUBE_SIEVE_NEWLINE; break; case 'notify': array_push($exts, 'notify'); $actions .= RCUBE_SIEVE_INDENT . "notify" . RCUBE_SIEVE_NEWLINE; $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":method \"" . $this->_escape_string($action['method']) . "\"" . RCUBE_SIEVE_NEWLINE; if (!empty($action['options'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":options [\"" . str_replace(",", "\",\"", $this->_escape_string($action['options'])) . "\"]" . RCUBE_SIEVE_NEWLINE; if (!empty($action['from'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":from \"" . $this->_escape_string($action['from']) . "\"" . RCUBE_SIEVE_NEWLINE; if (!empty($action['importance'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":importance \"" . $this->_escape_string($action['importance']) . "\"" . RCUBE_SIEVE_NEWLINE; $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":message \"". $this->_escape_string($action['msg']) ."\";" . RCUBE_SIEVE_NEWLINE; break; case 'enotify': array_push($exts, 'enotify'); $actions .= RCUBE_SIEVE_INDENT . "notify" . RCUBE_SIEVE_NEWLINE; if (!empty($action['options'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":options [\"" . str_replace(",", "\",\"", $this->_escape_string($action['options'])) . "\"]" . RCUBE_SIEVE_NEWLINE; if (!empty($action['from'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":from \"" . $this->_escape_string($action['from']) . "\"" . RCUBE_SIEVE_NEWLINE; if (!empty($action['importance'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":importance \"" . $this->_escape_string($action['importance']) . "\"" . RCUBE_SIEVE_NEWLINE; $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":message \"". $this->_escape_string($action['msg']) ."\"" . RCUBE_SIEVE_NEWLINE; $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . "\"" . $this->_escape_string($action['method']) . "\";" . RCUBE_SIEVE_NEWLINE; break; case 'editheaderadd': array_push($exts, 'editheader'); $actions .= RCUBE_SIEVE_INDENT . "addheader"; if ($action['index'] == 'last') $actions .= " :last"; $actions .= " \"". $this->_escape_string($action['name']) ."\" \"". $this->_escape_string($action['value']) ."\";" . RCUBE_SIEVE_NEWLINE; break; case 'editheaderrem': array_push($exts, 'editheader'); $actions .= RCUBE_SIEVE_INDENT . "deleteheader"; if (is_numeric($action['index'])) $actions .= " :index " . $action['index']; elseif ($action['index'] == 'last') $actions .= " :last"; if (strlen($action['operator']) > 0) $actions .= " :" . $action['operator']; $actions .= " \"". $this->_escape_string($action['name']) ."\""; if (strlen($action['value']) > 0) $actions .= " \"". $this->_escape_string($action['value']) ."\""; $actions .= ";" . RCUBE_SIEVE_NEWLINE; break; case 'keep': case 'discard': case 'stop': $actions .= RCUBE_SIEVE_INDENT . $action['type'] .";" . RCUBE_SIEVE_NEWLINE; break; } } $script .= $actions . "}" . RCUBE_SIEVE_NEWLINE; } } if ($variables) $variables .= RCUBE_SIEVE_NEWLINE; // requires $exts = array_unique($exts); if (sizeof($exts)) $script = 'require ["' . implode('","', $exts) . "\"];" . RCUBE_SIEVE_NEWLINE . RCUBE_SIEVE_NEWLINE . $variables . $script; // author if ($script && RCUBE_SIEVE_HEADER) $script = RCUBE_SIEVE_HEADER . RCUBE_SIEVE_NEWLINE . $script; return $script; } public function as_array() { return $this->content; } public function parse_text($script) { $i = 0; $content = array(); // remove C comments $script = preg_replace('|/\*.*?\*/|sm', '', $script); // tokenize rules - \r is optional for backward compatibility (added 20090413) 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 && preg_match('/^# disabledRule:\[(.*)\]/', $token, $matches)) { $content[$i] = unserialize($this->_regular_serial($matches[1])); $i++; } elseif (isset($content[$i]['name']) && sizeof($content[$i]) == 1) { if ($rule = $this->_tokenize_rule($token)) { $content[$i] = array_merge($content[$i], $rule); $i++; } else { unset($content[$i]); } } } } return $content; } private function _tokenize_rule($content) { $result = NULL; if (preg_match('/^(if|elsif|else)\s+((true|not\s+true|allof|anyof|exists|header|not|size|envelope|address|spamtest|virustest|date|currentdate)\s+(.*))\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; } private function _parse_actions($content) { $content = str_replace("\r\n", "\n", $content); $result = NULL; // supported actions $patterns[] = '^\s*discard;'; $patterns[] = '^\s*keep;'; $patterns[] = '^\s*stop;'; $patterns[] = '^\s*fileinto\s+(:copy\s+)?(:create\s+)?(.*?[^\\\]);'; $patterns[] = '^\s*redirect\s+(:copy\s+)?(.*?[^\\\]);'; $patterns[] = '^\s*setflag\s+(.*?[^\\\]);'; $patterns[] = '^\s*addflag\s+(.*?[^\\\]);'; $patterns[] = '^\s*reject\s+text:(.*)\n\.\n;'; $patterns[] = '^\s*ereject\s+text:(.*)\n\.\n;'; $patterns[] = '^\s*reject\s+(.*?[^\\\]);'; $patterns[] = '^\s*ereject\s+(.*?[^\\\]);'; $patterns[] = '^\s*vacation\s+(:days\s+([0-9]+)\s+)?(:addresses\s+\[(.*?[^\\\])\]\s+)?(:subject\s+(".*?[^"\\\]")\s+)?(:handle\s+(".*?[^"\\\]")\s+)?(:from\s+(".*?[^"\\\]")\s+)?(:mime\s+)?text:(.*)\n\.\n;'; $patterns[] = '^\s*vacation\s+(:days\s+([0-9]+)\s+)?(:addresses\s+\[(.*?[^\\\])\]\s+)?(:subject\s+(".*?[^"\\\]")\s+)?(:handle\s+(".*?[^"\\\]")\s+)?(:from\s+(".*?[^"\\\]")\s+)?(.*?[^\\\]);'; $patterns[] = '^\s*notify\s+:method\s+(".*?[^"\\\]")\s+(:options\s+\[(.*?[^\\\])\]\s+)?(:from\s+(".*?[^"\\\]")\s+)?(:importance\s+(".*?[^"\\\]")\s+)?:message\s+(".*?[^"\\\]");'; $patterns[] = '^\s*notify\s+(:options\s+\[(.*?[^\\\])\]\s+)?(:from\s+(".*?[^"\\\]")\s+)?(:importance\s+(".*?[^"\\\]")\s+)?:message\s+(".*?[^"\\\]")\s+(.*);'; $patterns[] = '^\s*addheader\s+(:(last))?\s*(".*?[^"\\\]")\s+(".*?[^"\\\]");'; $patterns[] = '^\s*deleteheader\s+(:(last)|:index\s([0-9])+)?\s*(:(contains))?\s*(".*?[^"\\\]")\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\s+:copy/', $content)) { $result[] = array('type' => 'fileinto_copy', 'target' => $this->_parse_string($m[sizeof($m)-1])); } elseif (preg_match('/^fileinto/', $content)) { $result[] = array('type' => 'fileinto', 'target' => $this->_parse_string($m[sizeof($m)-1])); } elseif (preg_match('/^redirect\s+:copy/', $content)) { $result[] = array('type' => 'redirect_copy', '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('/^(setflag|addflag)/', $content)) { if (in_array('imap4flags', $this->supported)) $result[] = array('type' => 'imap4flags', 'target' => $this->_parse_string($m[sizeof($m)-1])); else $result[] = array('type' => 'imapflags', 'target' => $this->_parse_string($m[sizeof($m)-1])); } elseif (preg_match('/^vacation\s+(:days\s+([0-9]+)\s+)?(:addresses\s+\[(.*?[^\\\])\]\s+)?(:subject\s+(".*?[^"\\\]")\s+)?(:handle\s+(".*?[^"\\\]")\s+)?(:from\s+(".*?[^"\\\]")\s+)?(.*);$/sm', $content, $matches)) { $origsubject = ""; if (substr($matches[6], -13, 12) == ": \${subject}") { $matches[6] = trim(substr($matches[6], 0, -13)) . "\""; $origsubject = "1"; } if ($matches[10] == "\"\${from}\"") $matches[10] = "\"auto\""; // if (function_exists("mb_decode_mimeheader")) $matches[5] = mb_decode_mimeheader($matches[5]); if (strpos($matches[11], 'Content-Type: multipart/alternative') !== false) { $htmlmsg = true; preg_match('/Content-Type: text\/html; charset=([^\r\n]+).*(.+)<\/body>/sm', $matches[11], $htmlparts); $msg = quoted_printable_decode($htmlparts[2]); $charset = $htmlparts[1]; } else { $htmlmsg = false; $msg = $this->_parse_string($matches[11]); $charset = $this->_parse_charset($matches[11]); } // unescape lines which start is a . $msg = preg_replace('/(^|\r?\n)\.\./', "$1.", $msg); $result[] = array('type' => 'vacation', 'days' => $matches[2], 'subject' => $this->_parse_string($matches[6]), 'origsubject' => $origsubject, 'from' => $this->_parse_string($matches[10]), 'addresses' => $this->_parse_string(str_replace("\",\"", ",", $matches[4])), 'handle' => $this->_parse_string($matches[8]), 'msg' => $msg, 'htmlmsg' => $htmlmsg, 'charset' => $charset); } elseif (preg_match('/^notify\s+:method\s+(".*?[^"\\\]")\s+(:options\s+\[(.*?[^\\\])\]\s+)?(:from\s+(".*?[^"\\\]")\s+)?(:importance\s+(".*?[^"\\\]")\s+)?:message\s+(".*?[^"\\\]");$/sm', $content, $matches)) { $result[] = array('type' => 'notify', 'method' => $this->_parse_string($matches[1]), 'options' => $this->_parse_string($matches[3]), 'from' => $this->_parse_string($matches[5]), 'importance' => $this->_parse_string($matches[7]), 'msg' => $this->_parse_string($matches[8])); } elseif (preg_match('/^notify\s+(:options\s+\[(.*?[^\\\])\]\s+)?(:from\s+(".*?[^"\\\]")\s+)?(:importance\s+(".*?[^"\\\]")\s+)?:message\s+(".*?[^"\\\]")\s+(.*);$/sm', $content, $matches)) { $result[] = array('type' => 'enotify', 'method' => $this->_parse_string($matches[8]), 'options' => $this->_parse_string($matches[2]), 'from' => $this->_parse_string($matches[4]), 'importance' => $this->_parse_string($matches[6]), 'msg' => $this->_parse_string($matches[7])); } elseif (preg_match('/^addheader/', $content)) { $result[] = array('type' => 'editheaderadd', 'index' => $m[sizeof($m)-3], 'name' => $this->_parse_string($m[sizeof($m)-2]), 'value' => $this->_parse_string($m[sizeof($m)-1])); } elseif (preg_match('/^deleteheader/', $content)) { $result[] = array('type' => 'editheaderrem', 'index' => $m[sizeof($m)-6] == 'last' ? $m[sizeof($m)-6] : $m[sizeof($m)-5], 'operator' => $m[sizeof($m)-3], 'name' => strlen($m[sizeof($m)-2]) == 0 ? $this->_parse_string($m[sizeof($m)-1]) : $this->_parse_string($m[sizeof($m)-2]), 'value' => strlen($m[sizeof($m)-2]) == 0 ? '' : $this->_parse_string($m[sizeof($m)-1])); } } } return $result; } 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 $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+)?(spamtest|virustest)\s+:value\s+"(eq|ge|le)"\s+:comparator\s+"i;ascii-numeric"\s+"(.*?[^\\\])"'; $patterns[] = '(not\s+)?(header|address|envelope)\s+:(contains|is|matches|regex|user|detail|domain)((\s+))\[(.*?[^\\\]")\]\s+\[(.*?[^\\\]")\]'; $patterns[] = '(not\s+)?(header|address|envelope)\s+:(contains|is|matches|regex|user|detail|domain)((\s+))(".*?[^\\\]")\s+(".*?[^\\\]")'; $patterns[] = '(not\s+)?(header|address|envelope)\s+:(contains|is|matches|regex|user|detail|domain)((\s+))\[(.*?[^\\\]")\]\s+(".*?[^\\\]")'; $patterns[] = '(not\s+)?(header|address|envelope)\s+:(contains|is|matches|regex|user|detail|domain)((\s+))(".*?[^\\\]")\s+\[(.*?[^\\\]")\]'; $patterns[] = '(not\s+)?(header|address|envelope)\s+:(count\s+".*?[^\\\]"|value\s+".*?[^\\\]")(\s+:comparator\s+"(.*?[^\\\])")?\s+\[(.*?[^\\\]")\]\s+\[(.*?[^\\\]")\]'; $patterns[] = '(not\s+)?(header|address|envelope)\s+:(count\s+".*?[^\\\]"|value\s+".*?[^\\\]")(\s+:comparator\s+"(.*?[^\\\])")?\s+(".*?[^\\\]")\s+(".*?[^\\\]")'; $patterns[] = '(not\s+)?(header|address|envelope)\s+:(count\s+".*?[^\\\]"|value\s+".*?[^\\\]")(\s+:comparator\s+"(.*?[^\\\])")?\s+\[(.*?[^\\\]")\]\s+(".*?[^\\\]")'; $patterns[] = '(not\s+)?(header|address|envelope)\s+:(count\s+".*?[^\\\]"|value\s+".*?[^\\\]")(\s+:comparator\s+"(.*?[^\\\])")?\s+(".*?[^\\\]")\s+\[(.*?[^\\\]")\]'; $patterns[] = '(not\s+)?(body)(\s+:(raw|text|content\s+".*?[^\\\]"))?\s+:(contains|is|matches|regex)((\s+))\[(.*?[^\\\]")\]'; $patterns[] = '(not\s+)?(body)(\s+:(raw|text|content\s+".*?[^\\\]"))?\s+:(contains|is|matches|regex)((\s+))(".*?[^\\\]")'; $patterns[] = '(not\s+)?(body)(\s+:(raw|text|content\s+".*?[^\\\]"))?\s+:(count\s+".*?[^\\\]"|value\s+".*?[^\\\]")(\s+:comparator\s+"(.*?[^\\\])")?\s+\[(.*?[^\\\]")\]'; $patterns[] = '(not\s+)?(body)(\s+:(raw|text|content\s+".*?[^\\\]"))?\s+:(count\s+".*?[^\\\]"|value\s+".*?[^\\\]")(\s+:comparator\s+"(.*?[^\\\])")?\s+(".*?[^\\\]")'; $patterns[] = '(not\s+)?(date|currentdate)(\s+:zone\s+"([\+\-][0-9]{4})")?\s+:(contains|is|matches|regex)((\s+))(".*?[^\\\]"\s+)?(".*?[^\\\]")\s+(".*?[^\\\]")'; $patterns[] = '(not\s+)?(date|currentdate)(\s+:zone\s+"([\+\-][0-9]{4})")?\s+:(count\s+".*?[^\\\]"|value\s+".*?[^\\\]")(\s+:comparator\s+"(.*?[^\\\])")?(\s+".*?[^\\\]")?\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( 'type' => 'size', 'not' => $match[$size-4] ? true : false, 'operator' => $match[$size-2], // under/over 'target' => $match[$size-1], // value ); } elseif (preg_match('/^(not\s+)?(spamtest|virustest)/', $match[0])) { $result[] = array( 'type' => $match[$size-3], 'not' => $match[$size-4] ? true : false, 'operator' => $match[$size-2], // ge/le/eq 'target' => $match[$size-1], // value ); } elseif (preg_match('/^(not\s+)?(header|address|envelope)/', $match[0])) { $result[] = array( 'type' => $match[$size-6], 'not' => $match[$size-7] ? true : false, 'operator' => $match[$size-5], // is/contains/matches 'header' => $this->_parse_list($match[$size-2]), // header(s) 'target' => $this->_parse_list($match[$size-1], ($match[$size-5] == 'regex' ? true : false)), // string(s) 'comparator' => trim($match[$size-3]) ); } elseif (preg_match('/^(not\s+)?exists/', $match[0])) { $result[] = array( 'type' => 'exists', 'not' => $match[$size-3] ? true : false, 'operator' => 'exists', 'header' => $this->_parse_list($match[$size-1]), // header(s) ); } elseif (preg_match('/^(not\s+)?true/', $match[0])) { $result[] = array( 'type' => 'true', 'not' => $match[$size-2] ? true : false, ); } elseif (preg_match('/^(not\s+)?body/', $match[0])) { if (preg_match('/.*content\s+"(.*?[^\\\])".*/', $match[$size-5], $parts)) { $bodypart = 'content'; $contentpart = $parts[1]; } else { $bodypart = $match[$size-5]; $contentpart = ''; } $result[] = array( 'type' => 'body', 'not' => $match[$size-8] ? true : false, 'bodypart' => $bodypart, 'contentpart' => $contentpart, 'operator' => $match[$size-4], // is/contains/matches 'header' => 'body', // header(s) 'target' => $this->_parse_list($match[$size-1], ($match[$size-4] == 'regex' ? true : false)), // string(s) 'comparator' => trim($match[$size-2]) ); } elseif (preg_match('/^(not\s+)?(date|currentdate)/', $match[0])) { $result[] = array( 'type' => 'date', 'not' => $match[$size-10] ? true : false, 'header' => $match[$size-9], // header 'operator' => $match[$size-6], // is/contains/matches 'datepart' => $this->_parse_list($match[$size-2]), 'target' => $this->_parse_list($match[$size-1], ($match[$size-5] == 'regex' ? true : false)), // string(s) 'field' => $match[$size-3], // received 'comparator' => trim($match[$size-4]) ); } } } return array($result, $join); } private function _parse_string($content) { $text = ''; $content = trim($content); if (preg_match('/^:mime\s+text:(.*)\.$/sm', $content, $matches)) { $parts = preg_split("/\r?\n/", $matches[1], 4); $text = trim($parts[3]); } elseif (preg_match('/^text:(.*)\.$/sm', $content, $matches)) $text = trim($matches[1]); elseif (preg_match('/^"(.*)"$/', $content, $matches)) $text = str_replace('\"', '"', $matches[1]); return $text; } private function _parse_charset($content) { $charset = RCUBE_CHARSET; $content = trim($content); if (preg_match('/^:mime\s+text:(.*)\.$/sm', $content, $matches)) { $parts = preg_split("/\r?\n/", $matches[1], 4); $charset = trim(substr($parts[1], stripos($parts[1], "charset=") + 8)); } return $charset; } 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); } } private function _parse_list($content, $regex = false) { $result = array(); if ($regex) { if (preg_match('/^"(.*)"$/', $content, $matches)); $content = $matches[1]; $content = str_replace('\"', '"', $content); return $content; } 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; } private function _safe_serial($data) { $data = str_replace("\r", "[!r]", $data); $data = str_replace("\n", "[!n]", $data); return $data; } private function _regular_serial($data) { $data = str_replace("[!r]", "\r", $data); $data = str_replace("[!n]", "\n", $data); return $data; } } ?>