PageRenderTime 58ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/include/rcube_sieve_script.php

https://github.com/igloonet/Roundcube-Plugin-SieveRules-Managesieve
PHP | 969 lines | 787 code | 129 blank | 53 comment | 137 complexity | 74b87770ae2e8f7f01d0d96e3649f21b MD5 | raw file
  1. <?php
  2. /*
  3. +-----------------------------------------------------------------------+
  4. | rcube_sieve_script class for sieverules script parsing |
  5. | |
  6. | Author: Aleksander Machniak <alec@alec.pl> |
  7. | Modifications by: Philip Weir |
  8. | * Changed name of keys in script array |
  9. | * Added support for address and envelope |
  10. | * Added support for vacation |
  11. | * Added support for disabled rules (written to file as comment) |
  12. | * Added support for regex tests |
  13. | * Added support for imapflags |
  14. | * Added support for relational operators and comparators |
  15. | * Added support for subaddress tests |
  16. | * Added support for notify action |
  17. | * Added support for stop action |
  18. | * Added support for body and copy |
  19. | * Added support for spamtest and virustest |
  20. | * Added support for date |
  21. | * Added support for editheader |
  22. +-----------------------------------------------------------------------+
  23. */
  24. define('SIEVE_ERROR_BAD_ACTION', 1);
  25. define('SIEVE_ERROR_NOT_FOUND', 2);
  26. // define constants for sieve file
  27. if (!defined('RCUBE_SIEVE_NEWLINE'))
  28. define('RCUBE_SIEVE_NEWLINE', "\r\n");
  29. if (!defined('RCUBE_SIEVE_INDENT'))
  30. define('RCUBE_SIEVE_INDENT', "\t");
  31. if (!defined('RCUBE_SIEVE_HEADER'))
  32. define('RCUBE_SIEVE_HEADER', "## Generated by Roundcube Webmail SieveRules Plugin ##");
  33. class rcube_sieve_script
  34. {
  35. private $elsif = true;
  36. private $content = array();
  37. private $supported = array(
  38. 'fileinto',
  39. 'reject',
  40. 'ereject',
  41. 'vacation',
  42. 'imapflags',
  43. 'imap4flags',
  44. 'notify',
  45. 'enotify',
  46. 'spamtest',
  47. 'virustest',
  48. 'date',
  49. 'editheader'
  50. );
  51. public $raw = '';
  52. public function __construct($script, $ext = array(), $elsif = true)
  53. {
  54. $this->raw = $script;
  55. $this->elsif = $elsif;
  56. // adjust supported extenstion to match sieve server
  57. $this->supported = array_intersect($this->supported, $ext);
  58. if (in_array('copy', $ext))
  59. $this->supported = array_merge($this->supported, array('fileinto_copy','redirect_copy'));
  60. if (in_array('editheader', $ext))
  61. $this->supported = array_merge($this->supported, array('editheaderadd','editheaderrem'));
  62. // include standard actions in supported list
  63. $this->supported = array_merge($this->supported, array('redirect','keep','discard','stop'));
  64. // load script
  65. $this->content = $this->parse_text($script);
  66. }
  67. public function add_text($script)
  68. {
  69. $content = $this->parse_text($script);
  70. $result = false;
  71. // check existsing script rules names
  72. foreach ($this->content as $idx => $elem)
  73. $names[$elem['name']] = $idx;
  74. foreach ($content as $elem) {
  75. if (!isset($names[$elem['name']])) {
  76. array_push($this->content, $elem);
  77. $result = true;
  78. }
  79. }
  80. return $result;
  81. }
  82. public function import_filters($content)
  83. {
  84. if (is_array($content)) {
  85. $result = false;
  86. // check existsing script rules names
  87. foreach ($this->content as $idx => $elem)
  88. $names[$elem['name']] = $idx;
  89. foreach ($content as $elem) {
  90. if (!isset($names[$elem['name']])) {
  91. array_push($this->content, $elem);
  92. $result = true;
  93. }
  94. }
  95. }
  96. else {
  97. $this->add_text($content);
  98. }
  99. }
  100. public function add_rule($content, $pos = null)
  101. {
  102. foreach ($content['actions'] as $action) {
  103. if (!in_array($action['type'], $this->supported))
  104. return SIEVE_ERROR_BAD_ACTION;
  105. }
  106. if ($pos !== null)
  107. array_splice($this->content, $pos, 0, array($content));
  108. else
  109. array_push($this->content, $content);
  110. return true;
  111. }
  112. public function delete_rule($index)
  113. {
  114. if (isset($this->content[$index])) {
  115. unset($this->content[$index]);
  116. $this->content = array_values($this->content);
  117. return true;
  118. }
  119. return SIEVE_ERROR_NOT_FOUND;
  120. }
  121. public function size()
  122. {
  123. return sizeof($this->content);
  124. }
  125. public function update_rule($index, $content)
  126. {
  127. foreach ($content['actions'] as $action) {
  128. if (!in_array($action['type'], $this->supported))
  129. return SIEVE_ERROR_BAD_ACTION;
  130. }
  131. if ($this->content[$index]) {
  132. $this->content[$index] = $content;
  133. return true;
  134. }
  135. return SIEVE_ERROR_NOT_FOUND;
  136. }
  137. public function move_rule($source, $destination)
  138. {
  139. $this->add_rule($this->content[$source], $destination);
  140. if ($source < $destination)
  141. $this->delete_rule($source);
  142. else
  143. $this->delete_rule($source + 1);
  144. }
  145. public function as_text()
  146. {
  147. $script = '';
  148. $variables = '';
  149. $exts = array();
  150. // rules
  151. $activeRules = 0;
  152. foreach ($this->content as $rule) {
  153. $tests = array();
  154. $i = 0;
  155. if ($rule['disabled'] == 1) {
  156. $script .= '# rule:[' . $rule['name'] . "]" . RCUBE_SIEVE_NEWLINE;
  157. $script .= '# disabledRule:[' . $this->_safe_serial(serialize($rule)) . "]" . RCUBE_SIEVE_NEWLINE;
  158. }
  159. else {
  160. // header
  161. $script .= '# rule:[' . $rule['name'] . "]" . RCUBE_SIEVE_NEWLINE;
  162. // constraints expressions
  163. foreach ($rule['tests'] as $test) {
  164. $tests[$i] = '';
  165. switch ($test['type']) {
  166. case 'size':
  167. $tests[$i] .= ($test['not'] ? 'not ' : '');
  168. $tests[$i] .= 'size :' . ($test['operator']=='under' ? 'under ' : 'over ') . $test['target'];
  169. break;
  170. case 'virustest':
  171. case 'spamtest':
  172. array_push($exts, $test['type']);
  173. array_push($exts, 'relational');
  174. array_push($exts, 'comparator-i;ascii-numeric');
  175. $tests[$i] .= ($test['not'] ? 'not ' : '');
  176. $tests[$i] .= $test['type'] . ' :value ' . ($test['operator'] == 'eq' ? '"eq" ' :
  177. ($test['operator'] == 'le' ? '"le" ' : '"ge" ')) .
  178. ':comparator "i;ascii-numeric" "' . $test['target'] .'"';
  179. break;
  180. case 'true':
  181. $tests[$i] .= ($test['not'] ? 'not true' : 'true');
  182. break;
  183. case 'exists':
  184. $tests[$i] .= ($test['not'] ? 'not ' : '');
  185. if (is_array($test['header']))
  186. $tests[$i] .= 'exists ["' . implode('", "', $this->_escape_string($test['header'])) . '"]';
  187. else
  188. $tests[$i] .= 'exists "' . $this->_escape_string($test['header']) . '"';
  189. break;
  190. case 'envelope':
  191. array_push($exts, 'envelope');
  192. case 'header':
  193. case 'address':
  194. if ($test['operator'] == 'regex')
  195. array_push($exts, 'regex');
  196. elseif (substr($test['operator'], 0, 5) == 'count' || substr($test['operator'], 0, 5) == 'value')
  197. array_push($exts, 'relational');
  198. elseif ($test['operator'] == 'user' || $test['operator'] == 'detail' || $test['operator'] == 'domain')
  199. array_push($exts, 'subaddress');
  200. $tests[$i] .= ($test['not'] ? 'not ' : '');
  201. $tests[$i] .= $test['type']. ' :' . $test['operator'];
  202. if ($test['comparator'] != '') {
  203. if ($test['comparator'] != 'i;ascii-casemap' && $test['comparator'] != 'i;octet')
  204. array_push($exts, 'comparator-' . $test['comparator']);
  205. $tests[$i] .= ' :comparator "' . $test['comparator'] . '"';
  206. }
  207. if (is_array($test['header']))
  208. $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['header'])) . '"]';
  209. else
  210. $tests[$i] .= ' "' . $this->_escape_string($test['header']) . '"';
  211. if (is_array($test['target']))
  212. $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['target'])) . '"]';
  213. else
  214. $tests[$i] .= ' "' . $this->_escape_string($test['target']) . '"';
  215. break;
  216. case 'body':
  217. array_push($exts, 'body');
  218. if ($test['operator'] == 'regex')
  219. array_push($exts, 'regex');
  220. elseif (substr($test['operator'], 0, 5) == 'count' || substr($test['operator'], 0, 5) == 'value')
  221. array_push($exts, 'relational');
  222. $tests[$i] .= ($test['not'] ? 'not ' : '');
  223. $tests[$i] .= $test['type'];
  224. if ($test['bodypart'] != '')
  225. $tests[$i] .= ' :' . $test['bodypart'];
  226. if ($test['contentpart'] != '')
  227. $tests[$i] .= ' "'. $test['contentpart'] .'"';
  228. $tests[$i] .= ' :' . $test['operator'];
  229. if ($test['comparator'] != '') {
  230. if ($test['comparator'] != 'i;ascii-casemap' && $test['comparator'] != 'i;octet')
  231. array_push($exts, 'comparator-' . $test['comparator']);
  232. $tests[$i] .= ' :comparator "' . $test['comparator'] . '"';
  233. }
  234. if (is_array($test['target']))
  235. $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['target'])) . '"]';
  236. else
  237. $tests[$i] .= ' "' . $this->_escape_string($test['target']) . '"';
  238. break;
  239. case 'date':
  240. array_push($exts, 'date');
  241. if ($test['operator'] == 'regex')
  242. array_push($exts, 'regex');
  243. elseif (substr($test['operator'], 0, 5) == 'count' || substr($test['operator'], 0, 5) == 'value')
  244. array_push($exts, 'relational');
  245. $tests[$i] .= ($test['not'] ? 'not ' : '');
  246. $tests[$i] .= $test['header'];
  247. $timezone = rcube::get_instance()->config->get('timezone', 'auto');
  248. if ($timezone != 'auto') {
  249. $tz = new DateTimeZone($timezone);
  250. $date = new DateTime('now', $tz);
  251. $tests[$i] .= ' :zone ' . '"' . $date->format('O') . '"';
  252. }
  253. $tests[$i] .= ' :' . $test['operator'];
  254. if ($test['comparator'] != '') {
  255. if ($test['comparator'] != 'i;ascii-casemap' && $test['comparator'] != 'i;octet')
  256. array_push($exts, 'comparator-' . $test['comparator']);
  257. $tests[$i] .= ' :comparator "' . $test['comparator'] . '"';
  258. }
  259. $tests[$i] .= ' "' . $this->_escape_string($test['datepart']) . '"';
  260. $tests[$i] .= ' "' . $this->_escape_string($test['target']) . '"';
  261. break;
  262. }
  263. $i++;
  264. }
  265. $script .= ($activeRules > 0 && $this->elsif ? 'els' : '') . ($rule['join'] ? 'if allof (' : 'if anyof (');
  266. $activeRules++;
  267. if (sizeof($tests) > 1)
  268. $script .= implode("," . RCUBE_SIEVE_NEWLINE . RCUBE_SIEVE_INDENT, $tests);
  269. elseif (sizeof($tests))
  270. $script .= $tests[0];
  271. else
  272. $script .= 'true';
  273. $script .= ")". RCUBE_SIEVE_NEWLINE ."{" . RCUBE_SIEVE_NEWLINE;
  274. // action(s)
  275. $actions = '';
  276. foreach ($rule['actions'] as $action) {
  277. switch ($action['type']) {
  278. case 'fileinto':
  279. case 'fileinto_copy':
  280. array_push($exts, 'fileinto');
  281. $args = '';
  282. if ($action['type'] == 'fileinto_copy') {
  283. array_push($exts, 'copy');
  284. $args .= ' :copy';
  285. }
  286. if ($action['create']) {
  287. array_push($exts, 'mailbox');
  288. $args .= ' :create';
  289. }
  290. // variables support in fileinto by David Warden
  291. if (preg_match('/\$\{\d+\}/', $action['target']))
  292. array_push($exts, 'variables');
  293. $actions .= RCUBE_SIEVE_INDENT . "fileinto" . $args . " \"" . $this->_escape_string($action['target']) . "\";" . RCUBE_SIEVE_NEWLINE;
  294. break;
  295. case 'redirect':
  296. case 'redirect_copy':
  297. $args = '';
  298. if ($action['type'] == 'redirect_copy') {
  299. array_push($exts, 'copy');
  300. $args .= ' :copy';
  301. }
  302. $tokens = preg_split('/[,;\s]/', $action['target']);
  303. foreach ($tokens as $email)
  304. $actions .= RCUBE_SIEVE_INDENT . "redirect" . $args . " \"" . $this->_escape_string($email) . "\";" . RCUBE_SIEVE_NEWLINE;
  305. break;
  306. case 'reject':
  307. case 'ereject':
  308. array_push($exts, $action['type']);
  309. if (strpos($action['target'], "\n")!==false)
  310. $actions .= RCUBE_SIEVE_INDENT . $action['type']." text:" . RCUBE_SIEVE_NEWLINE . $action['target'] . RCUBE_SIEVE_NEWLINE . "." . RCUBE_SIEVE_NEWLINE . ";" . RCUBE_SIEVE_NEWLINE;
  311. else
  312. $actions .= RCUBE_SIEVE_INDENT . $action['type']." \"" . $this->_escape_string($action['target']) . "\";" . RCUBE_SIEVE_NEWLINE;
  313. break;
  314. case 'vacation':
  315. array_push($exts, 'vacation');
  316. $action['subject'] = $this->_escape_string($action['subject']);
  317. // // encoding subject header with mb_encode provides better results with asian characters
  318. // if (function_exists("mb_encode_mimeheader"))
  319. // {
  320. // mb_internal_encoding($action['charset']);
  321. // $action['subject'] = mb_encode_mimeheader($action['subject'], $action['charset'], 'Q');
  322. // mb_internal_encoding(RCMAIL_CHARSET);
  323. // }
  324. // detect original recipient
  325. if ($action['from'] == 'auto' && strpos($variables, 'set "from"') === false) {
  326. array_push($exts, 'variables');
  327. $variables .= 'set "from" "";' . RCUBE_SIEVE_NEWLINE;
  328. $variables .= 'if header :matches "to" "*" {' . RCUBE_SIEVE_NEWLINE;
  329. $variables .= RCUBE_SIEVE_INDENT . 'set "from" "${1}";' . RCUBE_SIEVE_NEWLINE;
  330. $variables .= '}' . RCUBE_SIEVE_NEWLINE;
  331. $action['from'] = "\${from}";
  332. }
  333. elseif ($action['from'] == 'auto') {
  334. $action['from'] = "\${from}";
  335. }
  336. // append original subject
  337. if ($action['origsubject'] == '1' && strpos($variables, 'set "subject"') === false) {
  338. array_push($exts, 'variables');
  339. $variables .= 'set "subject" "";' . RCUBE_SIEVE_NEWLINE;
  340. $variables .= 'if header :matches "subject" "*" {' . RCUBE_SIEVE_NEWLINE;
  341. $variables .= RCUBE_SIEVE_INDENT . 'set "subject" "${1}";' . RCUBE_SIEVE_NEWLINE;
  342. $variables .= '}' . RCUBE_SIEVE_NEWLINE;
  343. $action['subject'] = trim($action['subject']);
  344. if (substr($action['subject'], -1, 1) != ":") $action['subject'] .= ":";
  345. $action['subject'] .= " \${subject}";
  346. }
  347. $actions .= RCUBE_SIEVE_INDENT . "vacation" . RCUBE_SIEVE_NEWLINE;
  348. $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":days ". $action['days'] . RCUBE_SIEVE_NEWLINE;
  349. if (!empty($action['addresses'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":addresses [\"". str_replace(",", "\",\"", $this->_escape_string($action['addresses'])) ."\"]" . RCUBE_SIEVE_NEWLINE;
  350. if (!empty($action['subject'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":subject \"". $action['subject'] ."\"" . RCUBE_SIEVE_NEWLINE;
  351. if (!empty($action['handle'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":handle \"". $this->_escape_string($action['handle']) ."\"" . RCUBE_SIEVE_NEWLINE;
  352. if (!empty($action['from'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":from \"". $this->_escape_string($action['from']) ."\"" . RCUBE_SIEVE_NEWLINE;
  353. if ($action['htmlmsg']) {
  354. $MAIL_MIME = new Mail_mime("\r\n");
  355. $action['msg'] = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN">' .
  356. "\r\n<html><body>\r\n" . $action['msg'] . "\r\n</body></html>\r\n";
  357. $MAIL_MIME->setHTMLBody($action['msg']);
  358. // add a plain text version of the e-mail as an alternative part.
  359. $h2t = new html2text($action['msg'], false, true, 0);
  360. $plainTextPart = $h2t->get_text();
  361. if (!$plainTextPart) {
  362. // empty message body breaks attachment handling in drafts
  363. $plainTextPart = "\r\n";
  364. }
  365. else {
  366. // make sure all line endings are CRLF (#1486712)
  367. $plainTextPart = preg_replace('/\r?\n/', "\r\n", $plainTextPart);
  368. }
  369. $MAIL_MIME->setTXTBody($plainTextPart);
  370. $MAIL_MIME->setParam('html_charset', $action['charset']);
  371. $MAIL_MIME->setParam('text_charset', $action['charset']);
  372. $action['msg'] = $MAIL_MIME->getMessage();
  373. }
  374. // escape lines which start is a .
  375. $action['msg'] = preg_replace('/(^|\r?\n)\./', "$1..", $action['msg']);
  376. if ($action['htmlmsg'])
  377. $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":mime text:". RCUBE_SIEVE_NEWLINE . $action['msg'] . RCUBE_SIEVE_NEWLINE . "." . RCUBE_SIEVE_NEWLINE . ";" . RCUBE_SIEVE_NEWLINE;
  378. elseif ($action['charset'] != "UTF-8")
  379. $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;
  380. elseif (strpos($action['msg'], "\n") !== false)
  381. $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . "text:" . RCUBE_SIEVE_NEWLINE . $action['msg'] . RCUBE_SIEVE_NEWLINE . "." . RCUBE_SIEVE_NEWLINE . ";" . RCUBE_SIEVE_NEWLINE;
  382. else
  383. $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . "\"" . $this->_escape_string($action['msg']) . "\";" . RCUBE_SIEVE_NEWLINE;
  384. break;
  385. case 'imapflags':
  386. case 'imap4flags':
  387. array_push($exts, $action['type']);
  388. if (strpos($actions, "setflag") !== false)
  389. $actions .= RCUBE_SIEVE_INDENT . "addflag \"" . $this->_escape_string($action['target']) . "\";" . RCUBE_SIEVE_NEWLINE;
  390. else
  391. $actions .= RCUBE_SIEVE_INDENT . "setflag \"" . $this->_escape_string($action['target']) . "\";" . RCUBE_SIEVE_NEWLINE;
  392. break;
  393. case 'notify':
  394. array_push($exts, 'notify');
  395. $actions .= RCUBE_SIEVE_INDENT . "notify" . RCUBE_SIEVE_NEWLINE;
  396. $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":method \"" . $this->_escape_string($action['method']) . "\"" . RCUBE_SIEVE_NEWLINE;
  397. if (!empty($action['options'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":options [\"" . str_replace(",", "\",\"", $this->_escape_string($action['options'])) . "\"]" . RCUBE_SIEVE_NEWLINE;
  398. if (!empty($action['from'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":from \"" . $this->_escape_string($action['from']) . "\"" . RCUBE_SIEVE_NEWLINE;
  399. if (!empty($action['importance'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":importance \"" . $this->_escape_string($action['importance']) . "\"" . RCUBE_SIEVE_NEWLINE;
  400. $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":message \"". $this->_escape_string($action['msg']) ."\";" . RCUBE_SIEVE_NEWLINE;
  401. break;
  402. case 'enotify':
  403. array_push($exts, 'enotify');
  404. $actions .= RCUBE_SIEVE_INDENT . "notify" . RCUBE_SIEVE_NEWLINE;
  405. if (!empty($action['options'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":options [\"" . str_replace(",", "\",\"", $this->_escape_string($action['options'])) . "\"]" . RCUBE_SIEVE_NEWLINE;
  406. if (!empty($action['from'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":from \"" . $this->_escape_string($action['from']) . "\"" . RCUBE_SIEVE_NEWLINE;
  407. if (!empty($action['importance'])) $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":importance \"" . $this->_escape_string($action['importance']) . "\"" . RCUBE_SIEVE_NEWLINE;
  408. $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . ":message \"". $this->_escape_string($action['msg']) ."\"" . RCUBE_SIEVE_NEWLINE;
  409. $actions .= RCUBE_SIEVE_INDENT . RCUBE_SIEVE_INDENT . "\"" . $this->_escape_string($action['method']) . "\";" . RCUBE_SIEVE_NEWLINE;
  410. break;
  411. case 'editheaderadd':
  412. array_push($exts, 'editheader');
  413. $actions .= RCUBE_SIEVE_INDENT . "addheader";
  414. if ($action['index'] == 'last')
  415. $actions .= " :last";
  416. $actions .= " \"". $this->_escape_string($action['name']) ."\" \"". $this->_escape_string($action['value']) ."\";" . RCUBE_SIEVE_NEWLINE;
  417. break;
  418. case 'editheaderrem':
  419. array_push($exts, 'editheader');
  420. $actions .= RCUBE_SIEVE_INDENT . "deleteheader";
  421. if (is_numeric($action['index']))
  422. $actions .= " :index " . $action['index'];
  423. elseif ($action['index'] == 'last')
  424. $actions .= " :last";
  425. if (strlen($action['operator']) > 0)
  426. $actions .= " :" . $action['operator'];
  427. $actions .= " \"". $this->_escape_string($action['name']) ."\"";
  428. if (strlen($action['value']) > 0)
  429. $actions .= " \"". $this->_escape_string($action['value']) ."\"";
  430. $actions .= ";" . RCUBE_SIEVE_NEWLINE;
  431. break;
  432. case 'keep':
  433. case 'discard':
  434. case 'stop':
  435. $actions .= RCUBE_SIEVE_INDENT . $action['type'] .";" . RCUBE_SIEVE_NEWLINE;
  436. break;
  437. }
  438. }
  439. $script .= $actions . "}" . RCUBE_SIEVE_NEWLINE;
  440. }
  441. }
  442. if ($variables)
  443. $variables .= RCUBE_SIEVE_NEWLINE;
  444. // requires
  445. $exts = array_unique($exts);
  446. if (sizeof($exts))
  447. $script = 'require ["' . implode('","', $exts) . "\"];" . RCUBE_SIEVE_NEWLINE . RCUBE_SIEVE_NEWLINE . $variables . $script;
  448. // author
  449. if ($script && RCUBE_SIEVE_HEADER)
  450. $script = RCUBE_SIEVE_HEADER . RCUBE_SIEVE_NEWLINE . $script;
  451. return $script;
  452. }
  453. public function as_array()
  454. {
  455. return $this->content;
  456. }
  457. public function parse_text($script)
  458. {
  459. $i = 0;
  460. $content = array();
  461. // remove C comments
  462. $script = preg_replace('|/\*.*?\*/|sm', '', $script);
  463. // tokenize rules - \r is optional for backward compatibility (added 20090413)
  464. if ($tokens = preg_split('/(# rule:\[.*\])\r?\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE)) {
  465. foreach($tokens as $token) {
  466. if (preg_match('/^# rule:\[(.*)\]/', $token, $matches)) {
  467. $content[$i]['name'] = $matches[1];
  468. }
  469. elseif (isset($content[$i]['name']) && sizeof($content[$i]) == 1 && preg_match('/^# disabledRule:\[(.*)\]/', $token, $matches)) {
  470. $content[$i] = unserialize($this->_regular_serial($matches[1]));
  471. $i++;
  472. }
  473. elseif (isset($content[$i]['name']) && sizeof($content[$i]) == 1) {
  474. if ($rule = $this->_tokenize_rule($token)) {
  475. $content[$i] = array_merge($content[$i], $rule);
  476. $i++;
  477. }
  478. else {
  479. unset($content[$i]);
  480. }
  481. }
  482. }
  483. }
  484. return $content;
  485. }
  486. private function _tokenize_rule($content)
  487. {
  488. $result = NULL;
  489. 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)) {
  490. list($tests, $join) = $this->_parse_tests(trim($matches[2]));
  491. $actions = $this->_parse_actions(trim($matches[5]));
  492. if ($tests && $actions) {
  493. $result = array(
  494. 'tests' => $tests,
  495. 'actions' => $actions,
  496. 'join' => $join,
  497. );
  498. }
  499. }
  500. return $result;
  501. }
  502. private function _parse_actions($content)
  503. {
  504. $content = str_replace("\r\n", "\n", $content);
  505. $result = NULL;
  506. // supported actions
  507. $patterns[] = '^\s*discard;';
  508. $patterns[] = '^\s*keep;';
  509. $patterns[] = '^\s*stop;';
  510. $patterns[] = '^\s*fileinto\s+(:copy\s+)?(:create\s+)?(.*?[^\\\]);';
  511. $patterns[] = '^\s*redirect\s+(:copy\s+)?(.*?[^\\\]);';
  512. $patterns[] = '^\s*setflag\s+(.*?[^\\\]);';
  513. $patterns[] = '^\s*addflag\s+(.*?[^\\\]);';
  514. $patterns[] = '^\s*reject\s+text:(.*)\n\.\n;';
  515. $patterns[] = '^\s*ereject\s+text:(.*)\n\.\n;';
  516. $patterns[] = '^\s*reject\s+(.*?[^\\\]);';
  517. $patterns[] = '^\s*ereject\s+(.*?[^\\\]);';
  518. $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;';
  519. $patterns[] = '^\s*vacation\s+:days\s+([0-9]+)\s+(:addresses\s+\[(.*?[^\\\])\]\s+)?(:subject\s+(".*?[^"\\\]")\s+)?(:handle\s+(".*?[^"\\\]")\s+)?(:from\s+(".*?[^"\\\]")\s+)?(.*?[^\\\]);';
  520. $patterns[] = '^\s*notify\s+:method\s+(".*?[^"\\\]")\s+(:options\s+\[(.*?[^\\\])\]\s+)?(:from\s+(".*?[^"\\\]")\s+)?(:importance\s+(".*?[^"\\\]")\s+)?:message\s+(".*?[^"\\\]");';
  521. $patterns[] = '^\s*notify\s+(:options\s+\[(.*?[^\\\])\]\s+)?(:from\s+(".*?[^"\\\]")\s+)?(:importance\s+(".*?[^"\\\]")\s+)?:message\s+(".*?[^"\\\]")\s+(.*);';
  522. $patterns[] = '^\s*addheader\s+(:(last))?\s*(".*?[^"\\\]")\s+(".*?[^"\\\]");';
  523. $patterns[] = '^\s*deleteheader\s+(:(last)|:index\s([0-9])+)?\s*(:(contains))?\s*(".*?[^"\\\]")\s*(".*?[^"\\\]")?;';
  524. $pattern = '/(' . implode('$)|(', $patterns) . '$)/ms';
  525. // parse actions body
  526. if (preg_match_all($pattern, $content, $mm, PREG_SET_ORDER)) {
  527. foreach ($mm as $m) {
  528. $content = trim($m[0]);
  529. if (preg_match('/^(discard|keep|stop)/', $content, $matches)) {
  530. $result[] = array('type' => $matches[1]);
  531. }
  532. elseif (preg_match('/^fileinto\s+:copy/', $content)) {
  533. $result[] = array('type' => 'fileinto_copy', 'target' => $this->_parse_string($m[sizeof($m)-1]));
  534. }
  535. elseif (preg_match('/^fileinto/', $content)) {
  536. $result[] = array('type' => 'fileinto', 'target' => $this->_parse_string($m[sizeof($m)-1]));
  537. }
  538. elseif (preg_match('/^redirect\s+:copy/', $content)) {
  539. $result[] = array('type' => 'redirect_copy', 'target' => $this->_parse_string($m[sizeof($m)-1]));
  540. }
  541. elseif (preg_match('/^redirect/', $content)) {
  542. $result[] = array('type' => 'redirect', 'target' => $this->_parse_string($m[sizeof($m)-1]));
  543. }
  544. elseif (preg_match('/^(reject|ereject)\s+(.*);$/sm', $content, $matches)) {
  545. $result[] = array('type' => $matches[1], 'target' => $this->_parse_string($matches[2]));
  546. }
  547. elseif (preg_match('/^(setflag|addflag)/', $content)) {
  548. if (in_array('imap4flags', $this->supported))
  549. $result[] = array('type' => 'imap4flags', 'target' => $this->_parse_string($m[sizeof($m)-1]));
  550. else
  551. $result[] = array('type' => 'imapflags', 'target' => $this->_parse_string($m[sizeof($m)-1]));
  552. }
  553. 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)) {
  554. $origsubject = "";
  555. if (substr($matches[5], -13, 12) == ": \${subject}") {
  556. $matches[5] = trim(substr($matches[5], 0, -13)) . "\"";
  557. $origsubject = "1";
  558. }
  559. if ($matches[9] == "\"\${from}\"")
  560. $matches[9] = "\"auto\"";
  561. // if (function_exists("mb_decode_mimeheader")) $matches[5] = mb_decode_mimeheader($matches[5]);
  562. if (strpos($matches[10], 'Content-Type: multipart/alternative') !== false) {
  563. $htmlmsg = true;
  564. preg_match('/Content-Type: text\/html; charset=([^\r\n]+).*<body>(.+)<\/body>/sm', $matches[10], $htmlparts);
  565. $msg = quoted_printable_decode($htmlparts[2]);
  566. $charset = $htmlparts[1];
  567. }
  568. else {
  569. $htmlmsg = false;
  570. $msg = $this->_parse_string($matches[10]);
  571. $charset = $this->_parse_charset($matches[10]);
  572. }
  573. // unescape lines which start is a .
  574. $msg = preg_replace('/(^|\r?\n)\.\./', "$1.", $msg);
  575. $result[] = array('type' => 'vacation',
  576. 'days' => $matches[1],
  577. 'subject' => $this->_parse_string($matches[5]),
  578. 'origsubject' => $origsubject,
  579. 'from' => $this->_parse_string($matches[9]),
  580. 'addresses' => $this->_parse_string(str_replace("\",\"", ",", $matches[3])),
  581. 'handle' => $this->_parse_string($matches[7]),
  582. 'msg' => $msg,
  583. 'htmlmsg' => $htmlmsg,
  584. 'charset' => $charset);
  585. }
  586. elseif (preg_match('/^notify\s+:method\s+(".*?[^"\\\]")\s+(:options\s+\[(.*?[^\\\])\]\s+)?(:from\s+(".*?[^"\\\]")\s+)?(:importance\s+(".*?[^"\\\]")\s+)?:message\s+(".*?[^"\\\]");$/sm', $content, $matches)) {
  587. $result[] = array('type' => 'notify',
  588. 'method' => $this->_parse_string($matches[1]),
  589. 'options' => $this->_parse_string($matches[3]),
  590. 'from' => $this->_parse_string($matches[5]),
  591. 'importance' => $this->_parse_string($matches[7]),
  592. 'msg' => $this->_parse_string($matches[8]));
  593. }
  594. elseif (preg_match('/^notify\s+(:options\s+\[(.*?[^\\\])\]\s+)?(:from\s+(".*?[^"\\\]")\s+)?(:importance\s+(".*?[^"\\\]")\s+)?:message\s+(".*?[^"\\\]")\s+(.*);$/sm', $content, $matches)) {
  595. $result[] = array('type' => 'enotify',
  596. 'method' => $this->_parse_string($matches[8]),
  597. 'options' => $this->_parse_string($matches[2]),
  598. 'from' => $this->_parse_string($matches[4]),
  599. 'importance' => $this->_parse_string($matches[6]),
  600. 'msg' => $this->_parse_string($matches[7]));
  601. }
  602. elseif (preg_match('/^addheader/', $content)) {
  603. $result[] = array('type' => 'editheaderadd',
  604. 'index' => $m[sizeof($m)-3],
  605. 'name' => $this->_parse_string($m[sizeof($m)-2]),
  606. 'value' => $this->_parse_string($m[sizeof($m)-1]));
  607. }
  608. elseif (preg_match('/^deleteheader/', $content)) {
  609. $result[] = array('type' => 'editheaderrem',
  610. 'index' => $m[sizeof($m)-6] == 'last' ? $m[sizeof($m)-6] : $m[sizeof($m)-5],
  611. 'operator' => $m[sizeof($m)-3],
  612. 'name' => strlen($m[sizeof($m)-2]) == 0 ? $this->_parse_string($m[sizeof($m)-1]) : $this->_parse_string($m[sizeof($m)-2]),
  613. 'value' => strlen($m[sizeof($m)-2]) == 0 ? '' : $this->_parse_string($m[sizeof($m)-1]));
  614. }
  615. }
  616. }
  617. return $result;
  618. }
  619. private function _parse_tests($content)
  620. {
  621. $result = NULL;
  622. // lists
  623. if (preg_match('/^(allof|anyof)\s+\((.*)\)$/sm', $content, $matches)) {
  624. $content = $matches[2];
  625. $join = $matches[1]=='allof' ? true : false;
  626. }
  627. else {
  628. $join = false;
  629. }
  630. // supported tests regular expressions
  631. $patterns[] = '(not\s+)?(exists)\s+\[(.*?[^\\\])\]';
  632. $patterns[] = '(not\s+)?(exists)\s+(".*?[^\\\]")';
  633. $patterns[] = '(not\s+)?(true)';
  634. $patterns[] = '(not\s+)?(size)\s+:(under|over)\s+([0-9]+[KGM]{0,1})';
  635. $patterns[] = '(not\s+)?(spamtest|virustest)\s+:value\s+"(eq|ge|le)"\s+:comparator\s+"i;ascii-numeric"\s+"(.*?[^\\\])"';
  636. $patterns[] = '(not\s+)?(header|address|envelope)\s+:(contains|is|matches|regex|user|detail|domain)((\s+))\[(.*?[^\\\]")\]\s+\[(.*?[^\\\]")\]';
  637. $patterns[] = '(not\s+)?(header|address|envelope)\s+:(contains|is|matches|regex|user|detail|domain)((\s+))(".*?[^\\\]")\s+(".*?[^\\\]")';
  638. $patterns[] = '(not\s+)?(header|address|envelope)\s+:(contains|is|matches|regex|user|detail|domain)((\s+))\[(.*?[^\\\]")\]\s+(".*?[^\\\]")';
  639. $patterns[] = '(not\s+)?(header|address|envelope)\s+:(contains|is|matches|regex|user|detail|domain)((\s+))(".*?[^\\\]")\s+\[(.*?[^\\\]")\]';
  640. $patterns[] = '(not\s+)?(header|address|envelope)\s+:(count\s+".*?[^\\\]"|value\s+".*?[^\\\]")(\s+:comparator\s+"(.*?[^\\\])")?\s+\[(.*?[^\\\]")\]\s+\[(.*?[^\\\]")\]';
  641. $patterns[] = '(not\s+)?(header|address|envelope)\s+:(count\s+".*?[^\\\]"|value\s+".*?[^\\\]")(\s+:comparator\s+"(.*?[^\\\])")?\s+(".*?[^\\\]")\s+(".*?[^\\\]")';
  642. $patterns[] = '(not\s+)?(header|address|envelope)\s+:(count\s+".*?[^\\\]"|value\s+".*?[^\\\]")(\s+:comparator\s+"(.*?[^\\\])")?\s+\[(.*?[^\\\]")\]\s+(".*?[^\\\]")';
  643. $patterns[] = '(not\s+)?(header|address|envelope)\s+:(count\s+".*?[^\\\]"|value\s+".*?[^\\\]")(\s+:comparator\s+"(.*?[^\\\])")?\s+(".*?[^\\\]")\s+\[(.*?[^\\\]")\]';
  644. $patterns[] = '(not\s+)?(body)(\s+:(raw|text|content\s+".*?[^\\\]"))?\s+:(contains|is|matches|regex)((\s+))\[(.*?[^\\\]")\]';
  645. $patterns[] = '(not\s+)?(body)(\s+:(raw|text|content\s+".*?[^\\\]"))?\s+:(contains|is|matches|regex)((\s+))(".*?[^\\\]")';
  646. $patterns[] = '(not\s+)?(body)(\s+:(raw|text|content\s+".*?[^\\\]"))?\s+:(count\s+".*?[^\\\]"|value\s+".*?[^\\\]")(\s+:comparator\s+"(.*?[^\\\])")?\s+\[(.*?[^\\\]")\]';
  647. $patterns[] = '(not\s+)?(body)(\s+:(raw|text|content\s+".*?[^\\\]"))?\s+:(count\s+".*?[^\\\]"|value\s+".*?[^\\\]")(\s+:comparator\s+"(.*?[^\\\])")?\s+(".*?[^\\\]")';
  648. $patterns[] = '(not\s+)?(date|currentdate)(\s+:zone\s+"([\+\-][0-9]{4})")?\s+:(contains|is|matches|regex)((\s+))(".*?[^\\\]"\s+)?(".*?[^\\\]")\s+(".*?[^\\\]")';
  649. $patterns[] = '(not\s+)?(date|currentdate)(\s+:zone\s+"([\+\-][0-9]{4})")?\s+:(count\s+".*?[^\\\]"|value\s+".*?[^\\\]")(\s+:comparator\s+"(.*?[^\\\])")?(\s+".*?[^\\\]")?\s+(".*?[^\\\]")\s+(".*?[^\\\]")';
  650. // join patterns...
  651. $pattern = '/(' . implode(')|(', $patterns) . ')/';
  652. // ...and parse tests list
  653. if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) {
  654. foreach ($matches as $match) {
  655. $size = sizeof($match);
  656. if (preg_match('/^(not\s+)?size/', $match[0])) {
  657. $result[] = array(
  658. 'type' => 'size',
  659. 'not' => $match[$size-4] ? true : false,
  660. 'operator' => $match[$size-2], // under/over
  661. 'target' => $match[$size-1], // value
  662. );
  663. }
  664. elseif (preg_match('/^(not\s+)?(spamtest|virustest)/', $match[0])) {
  665. $result[] = array(
  666. 'type' => $match[$size-3],
  667. 'not' => $match[$size-4] ? true : false,
  668. 'operator' => $match[$size-2], // ge/le/eq
  669. 'target' => $match[$size-1], // value
  670. );
  671. }
  672. elseif (preg_match('/^(not\s+)?(header|address|envelope)/', $match[0])) {
  673. $result[] = array(
  674. 'type' => $match[$size-6],
  675. 'not' => $match[$size-7] ? true : false,
  676. 'operator' => $match[$size-5], // is/contains/matches
  677. 'header' => $this->_parse_list($match[$size-2]), // header(s)
  678. 'target' => $this->_parse_list($match[$size-1], ($match[$size-5] == 'regex' ? true : false)), // string(s)
  679. 'comparator' => trim($match[$size-3])
  680. );
  681. }
  682. elseif (preg_match('/^(not\s+)?exists/', $match[0])) {
  683. $result[] = array(
  684. 'type' => 'exists',
  685. 'not' => $match[$size-3] ? true : false,
  686. 'operator' => 'exists',
  687. 'header' => $this->_parse_list($match[$size-1]), // header(s)
  688. );
  689. }
  690. elseif (preg_match('/^(not\s+)?true/', $match[0])) {
  691. $result[] = array(
  692. 'type' => 'true',
  693. 'not' => $match[$size-2] ? true : false,
  694. );
  695. }
  696. elseif (preg_match('/^(not\s+)?body/', $match[0])) {
  697. if (preg_match('/.*content\s+"(.*?[^\\\])".*/', $match[$size-5], $parts)) {
  698. $bodypart = 'content';
  699. $contentpart = $parts[1];
  700. }
  701. else {
  702. $bodypart = $match[$size-5];
  703. $contentpart = '';
  704. }
  705. $result[] = array(
  706. 'type' => 'body',
  707. 'not' => $match[$size-8] ? true : false,
  708. 'bodypart' => $bodypart,
  709. 'contentpart' => $contentpart,
  710. 'operator' => $match[$size-4], // is/contains/matches
  711. 'header' => 'body', // header(s)
  712. 'target' => $this->_parse_list($match[$size-1], ($match[$size-4] == 'regex' ? true : false)), // string(s)
  713. 'comparator' => trim($match[$size-2])
  714. );
  715. }
  716. elseif (preg_match('/^(not\s+)?(date|currentdate)/', $match[0])) {
  717. $result[] = array(
  718. 'type' => 'date',
  719. 'not' => $match[$size-10] ? true : false,
  720. 'header' => $match[$size-9], // header
  721. 'operator' => $match[$size-6], // is/contains/matches
  722. 'datepart' => $this->_parse_list($match[$size-2]),
  723. 'target' => $this->_parse_list($match[$size-1], ($match[$size-5] == 'regex' ? true : false)), // string(s)
  724. 'field' => $match[$size-3], // received
  725. 'comparator' => trim($match[$size-4])
  726. );
  727. }
  728. }
  729. }
  730. return array($result, $join);
  731. }
  732. private function _parse_string($content)
  733. {
  734. $text = '';
  735. $content = trim($content);
  736. if (preg_match('/^:mime\s+text:(.*)\.$/sm', $content, $matches)) {
  737. $parts = preg_split("/\r?\n/", $matches[1], 4);
  738. $text = trim($parts[3]);
  739. }
  740. elseif (preg_match('/^text:(.*)\.$/sm', $content, $matches))
  741. $text = trim($matches[1]);
  742. elseif (preg_match('/^"(.*)"$/', $content, $matches))
  743. $text = str_replace('\"', '"', $matches[1]);
  744. return $text;
  745. }
  746. private function _parse_charset($content)
  747. {
  748. $charset = RCMAIL_CHARSET;
  749. $content = trim($content);
  750. if (preg_match('/^:mime\s+text:(.*)\.$/sm', $content, $matches)) {
  751. $parts = preg_split("/\r?\n/", $matches[1], 4);
  752. $charset = trim(substr($parts[1], stripos($parts[1], "charset=") + 8));
  753. }
  754. return $charset;
  755. }
  756. private function _escape_string($content)
  757. {
  758. $replace['/"/'] = '\\"';
  759. if (is_array($content)) {
  760. for ($x=0, $y=sizeof($content); $x<$y; $x++)
  761. $content[$x] = preg_replace(array_keys($replace), array_values($replace), $content[$x]);
  762. return $content;
  763. }
  764. else {
  765. return preg_replace(array_keys($replace), array_values($replace), $content);
  766. }
  767. }
  768. private function _parse_list($content, $regex = false)
  769. {
  770. $result = array();
  771. if ($regex) {
  772. if (preg_match('/^"(.*)"$/', $content, $matches));
  773. $content = $matches[1];
  774. $content = str_replace('\"', '"', $content);
  775. return $content;
  776. }
  777. for ($x=0, $len=strlen($content); $x<$len; $x++) {
  778. switch ($content[$x]) {
  779. case '\\':
  780. $str .= $content[++$x];
  781. break;
  782. case '"':
  783. if (isset($str)) {
  784. $result[] = $str;
  785. unset($str);
  786. }
  787. else {
  788. $str = '';
  789. }
  790. break;
  791. default:
  792. if (isset($str))
  793. $str .= $content[$x];
  794. break;
  795. }
  796. }
  797. if (sizeof($result)>1)
  798. return $result;
  799. elseif (sizeof($result) == 1)
  800. return $result[0];
  801. else
  802. return NULL;
  803. }
  804. private function _safe_serial($data)
  805. {
  806. $data = str_replace("\r", "[!r]", $data);
  807. $data = str_replace("\n", "[!n]", $data);
  808. return $data;
  809. }
  810. private function _regular_serial($data)
  811. {
  812. $data = str_replace("[!r]", "\r", $data);
  813. $data = str_replace("[!n]", "\n", $data);
  814. return $data;
  815. }
  816. }
  817. ?>