PageRenderTime 65ms CodeModel.GetById 28ms RepoModel.GetById 1ms app.codeStats 0ms

/plugins/managesieve/lib/rcube_sieve_script.php

https://github.com/netconstructor/roundcubemail
PHP | 1154 lines | 1009 code | 65 blank | 80 comment | 70 complexity | bf0cdcff4fc85f74507bbb7e8744faa6 MD5 | raw file
Possible License(s): GPL-3.0, LGPL-2.1
  1. <?php
  2. /**
  3. * Class for operations on Sieve scripts
  4. *
  5. * Copyright (C) 2008-2011, The Roundcube Dev Team
  6. * Copyright (C) 2011, Kolab Systems AG
  7. *
  8. * This program is free software; you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License version 2
  10. * as published by the Free Software Foundation.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License along
  18. * with this program; if not, write to the Free Software Foundation, Inc.,
  19. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  20. */
  21. class rcube_sieve_script
  22. {
  23. public $content = array(); // script rules array
  24. private $vars = array(); // "global" variables
  25. private $prefix = ''; // script header (comments)
  26. private $supported = array( // Sieve extensions supported by class
  27. 'fileinto', // RFC5228
  28. 'envelope', // RFC5228
  29. 'reject', // RFC5429
  30. 'ereject', // RFC5429
  31. 'copy', // RFC3894
  32. 'vacation', // RFC5230
  33. 'relational', // RFC3431
  34. 'regex', // draft-ietf-sieve-regex-01
  35. 'imapflags', // draft-melnikov-sieve-imapflags-06
  36. 'imap4flags', // RFC5232
  37. 'include', // draft-ietf-sieve-include-12
  38. 'variables', // RFC5229
  39. 'body', // RFC5173
  40. 'subaddress', // RFC5233
  41. 'enotify', // RFC5435
  42. 'notify', // draft-ietf-sieve-notify-00
  43. // @TODO: spamtest+virustest, mailbox, date
  44. );
  45. /**
  46. * Object constructor
  47. *
  48. * @param string Script's text content
  49. * @param array List of capabilities supported by server
  50. */
  51. public function __construct($script, $capabilities=array())
  52. {
  53. $capabilities = array_map('strtolower', (array) $capabilities);
  54. // disable features by server capabilities
  55. if (!empty($capabilities)) {
  56. foreach ($this->supported as $idx => $ext) {
  57. if (!in_array($ext, $capabilities)) {
  58. unset($this->supported[$idx]);
  59. }
  60. }
  61. }
  62. // Parse text content of the script
  63. $this->_parse_text($script);
  64. }
  65. /**
  66. * Adds rule to the script (at the end)
  67. *
  68. * @param string Rule name
  69. * @param array Rule content (as array)
  70. *
  71. * @return int The index of the new rule
  72. */
  73. public function add_rule($content)
  74. {
  75. // TODO: check this->supported
  76. array_push($this->content, $content);
  77. return sizeof($this->content)-1;
  78. }
  79. public function delete_rule($index)
  80. {
  81. if(isset($this->content[$index])) {
  82. unset($this->content[$index]);
  83. return true;
  84. }
  85. return false;
  86. }
  87. public function size()
  88. {
  89. return sizeof($this->content);
  90. }
  91. public function update_rule($index, $content)
  92. {
  93. // TODO: check this->supported
  94. if ($this->content[$index]) {
  95. $this->content[$index] = $content;
  96. return $index;
  97. }
  98. return false;
  99. }
  100. /**
  101. * Sets "global" variable
  102. *
  103. * @param string $name Variable name
  104. * @param string $value Variable value
  105. * @param array $mods Variable modifiers
  106. */
  107. public function set_var($name, $value, $mods = array())
  108. {
  109. // Check if variable exists
  110. for ($i=0, $len=count($this->vars); $i<$len; $i++) {
  111. if ($this->vars[$i]['name'] == $name) {
  112. break;
  113. }
  114. }
  115. $var = array_merge($mods, array('name' => $name, 'value' => $value));
  116. $this->vars[$i] = $var;
  117. }
  118. /**
  119. * Unsets "global" variable
  120. *
  121. * @param string $name Variable name
  122. */
  123. public function unset_var($name)
  124. {
  125. // Check if variable exists
  126. foreach ($this->vars as $idx => $var) {
  127. if ($var['name'] == $name) {
  128. unset($this->vars[$idx]);
  129. break;
  130. }
  131. }
  132. }
  133. /**
  134. * Gets the value of "global" variable
  135. *
  136. * @param string $name Variable name
  137. *
  138. * @return string Variable value
  139. */
  140. public function get_var($name)
  141. {
  142. // Check if variable exists
  143. for ($i=0, $len=count($this->vars); $i<$len; $i++) {
  144. if ($this->vars[$i]['name'] == $name) {
  145. return $this->vars[$i]['name'];
  146. }
  147. }
  148. }
  149. /**
  150. * Sets script header content
  151. *
  152. * @param string $text Header content
  153. */
  154. public function set_prefix($text)
  155. {
  156. $this->prefix = $text;
  157. }
  158. /**
  159. * Returns script as text
  160. */
  161. public function as_text()
  162. {
  163. $output = '';
  164. $exts = array();
  165. $idx = 0;
  166. if (!empty($this->vars)) {
  167. if (in_array('variables', (array)$this->supported)) {
  168. $has_vars = true;
  169. array_push($exts, 'variables');
  170. }
  171. foreach ($this->vars as $var) {
  172. if (empty($has_vars)) {
  173. // 'variables' extension not supported, put vars in comments
  174. $output .= sprintf("# %s %s\n", $var['name'], $var['value']);
  175. }
  176. else {
  177. $output .= 'set ';
  178. foreach (array_diff(array_keys($var), array('name', 'value')) as $opt) {
  179. $output .= ":$opt ";
  180. }
  181. $output .= self::escape_string($var['name']) . ' ' . self::escape_string($var['value']) . ";\n";
  182. }
  183. }
  184. }
  185. $imapflags = in_array('imap4flags', $this->supported) ? 'imap4flags' : 'imapflags';
  186. $notify = in_array('enotify', $this->supported) ? 'enotify' : 'notify';
  187. // rules
  188. foreach ($this->content as $rule) {
  189. $extension = '';
  190. $script = '';
  191. $tests = array();
  192. $i = 0;
  193. // header
  194. if (!empty($rule['name']) && strlen($rule['name'])) {
  195. $script .= '# rule:[' . $rule['name'] . "]\n";
  196. }
  197. // constraints expressions
  198. if (!empty($rule['tests'])) {
  199. foreach ($rule['tests'] as $test) {
  200. $tests[$i] = '';
  201. switch ($test['test']) {
  202. case 'size':
  203. $tests[$i] .= ($test['not'] ? 'not ' : '');
  204. $tests[$i] .= 'size :' . ($test['type']=='under' ? 'under ' : 'over ') . $test['arg'];
  205. break;
  206. case 'true':
  207. $tests[$i] .= ($test['not'] ? 'false' : 'true');
  208. break;
  209. case 'exists':
  210. $tests[$i] .= ($test['not'] ? 'not ' : '');
  211. $tests[$i] .= 'exists ' . self::escape_string($test['arg']);
  212. break;
  213. case 'header':
  214. $tests[$i] .= ($test['not'] ? 'not ' : '');
  215. $tests[$i] .= 'header';
  216. if (!empty($test['type'])) {
  217. // relational operator + comparator
  218. if (preg_match('/^(value|count)-([gteqnl]{2})/', $test['type'], $m)) {
  219. array_push($exts, 'relational');
  220. array_push($exts, 'comparator-i;ascii-numeric');
  221. $tests[$i] .= ' :' . $m[1] . ' "' . $m[2] . '" :comparator "i;ascii-numeric"';
  222. }
  223. else {
  224. $this->add_comparator($test, $tests[$i], $exts);
  225. if ($test['type'] == 'regex') {
  226. array_push($exts, 'regex');
  227. }
  228. $tests[$i] .= ' :' . $test['type'];
  229. }
  230. }
  231. $tests[$i] .= ' ' . self::escape_string($test['arg1']);
  232. $tests[$i] .= ' ' . self::escape_string($test['arg2']);
  233. break;
  234. case 'address':
  235. case 'envelope':
  236. if ($test['test'] == 'envelope') {
  237. array_push($exts, 'envelope');
  238. }
  239. $tests[$i] .= ($test['not'] ? 'not ' : '');
  240. $tests[$i] .= $test['test'];
  241. if (!empty($test['part'])) {
  242. $tests[$i] .= ' :' . $test['part'];
  243. if ($test['part'] == 'user' || $test['part'] == 'detail') {
  244. array_push($exts, 'subaddress');
  245. }
  246. }
  247. $this->add_comparator($test, $tests[$i], $exts);
  248. if (!empty($test['type'])) {
  249. if ($test['type'] == 'regex') {
  250. array_push($exts, 'regex');
  251. }
  252. $tests[$i] .= ' :' . $test['type'];
  253. }
  254. $tests[$i] .= ' ' . self::escape_string($test['arg1']);
  255. $tests[$i] .= ' ' . self::escape_string($test['arg2']);
  256. break;
  257. case 'body':
  258. array_push($exts, 'body');
  259. $tests[$i] .= ($test['not'] ? 'not ' : '') . 'body';
  260. $this->add_comparator($test, $tests[$i], $exts);
  261. if (!empty($test['part'])) {
  262. $tests[$i] .= ' :' . $test['part'];
  263. if (!empty($test['content']) && $test['part'] == 'content') {
  264. $tests[$i] .= ' ' . self::escape_string($test['content']);
  265. }
  266. }
  267. if (!empty($test['type'])) {
  268. if ($test['type'] == 'regex') {
  269. array_push($exts, 'regex');
  270. }
  271. $tests[$i] .= ' :' . $test['type'];
  272. }
  273. $tests[$i] .= ' ' . self::escape_string($test['arg']);
  274. break;
  275. }
  276. $i++;
  277. }
  278. }
  279. // disabled rule: if false #....
  280. if (!empty($tests)) {
  281. $script .= 'if ' . ($rule['disabled'] ? 'false # ' : '');
  282. if (count($tests) > 1) {
  283. $tests_str = implode(', ', $tests);
  284. }
  285. else {
  286. $tests_str = $tests[0];
  287. }
  288. if ($rule['join'] || count($tests) > 1) {
  289. $script .= sprintf('%s (%s)', $rule['join'] ? 'allof' : 'anyof', $tests_str);
  290. }
  291. else {
  292. $script .= $tests_str;
  293. }
  294. $script .= "\n{\n";
  295. }
  296. // action(s)
  297. if (!empty($rule['actions'])) {
  298. foreach ($rule['actions'] as $action) {
  299. $action_script = '';
  300. switch ($action['type']) {
  301. case 'fileinto':
  302. array_push($exts, 'fileinto');
  303. $action_script .= 'fileinto ';
  304. if ($action['copy']) {
  305. $action_script .= ':copy ';
  306. array_push($exts, 'copy');
  307. }
  308. $action_script .= self::escape_string($action['target']);
  309. break;
  310. case 'redirect':
  311. $action_script .= 'redirect ';
  312. if ($action['copy']) {
  313. $action_script .= ':copy ';
  314. array_push($exts, 'copy');
  315. }
  316. $action_script .= self::escape_string($action['target']);
  317. break;
  318. case 'reject':
  319. case 'ereject':
  320. array_push($exts, $action['type']);
  321. $action_script .= $action['type'].' '
  322. . self::escape_string($action['target']);
  323. break;
  324. case 'addflag':
  325. case 'setflag':
  326. case 'removeflag':
  327. array_push($exts, $imapflags);
  328. $action_script .= $action['type'].' '
  329. . self::escape_string($action['target']);
  330. break;
  331. case 'keep':
  332. case 'discard':
  333. case 'stop':
  334. $action_script .= $action['type'];
  335. break;
  336. case 'include':
  337. array_push($exts, 'include');
  338. $action_script .= 'include ';
  339. foreach (array_diff(array_keys($action), array('target', 'type')) as $opt) {
  340. $action_script .= ":$opt ";
  341. }
  342. $action_script .= self::escape_string($action['target']);
  343. break;
  344. case 'set':
  345. array_push($exts, 'variables');
  346. $action_script .= 'set ';
  347. foreach (array_diff(array_keys($action), array('name', 'value', 'type')) as $opt) {
  348. $action_script .= ":$opt ";
  349. }
  350. $action_script .= self::escape_string($action['name']) . ' ' . self::escape_string($action['value']);
  351. break;
  352. case 'notify':
  353. array_push($exts, $notify);
  354. $action_script .= 'notify';
  355. // Here we support only 00 version of notify draft, there
  356. // were a couple regressions in 00 to 04 changelog, we use
  357. // the version used by Cyrus
  358. if ($notify == 'notify') {
  359. switch ($action['importance']) {
  360. case 1: $action_script .= " :high"; break;
  361. case 2: $action_script .= " :normal"; break;
  362. case 3: $action_script .= " :low"; break;
  363. }
  364. unset($action['importance']);
  365. }
  366. foreach (array('from', 'importance', 'options', 'message') as $n_tag) {
  367. if (!empty($action[$n_tag])) {
  368. $action_script .= " :$n_tag " . self::escape_string($action[$n_tag]);
  369. }
  370. }
  371. if (!empty($action['address'])) {
  372. $method = 'mailto:' . $action['address'];
  373. if (!empty($action['body'])) {
  374. $method .= '?body=' . rawurlencode($action['body']);
  375. }
  376. }
  377. else {
  378. $method = $action['method'];
  379. }
  380. // method is optional in notify extension
  381. if (!empty($method)) {
  382. $action_script .= ($notify == 'notify' ? " :method " : " ") . self::escape_string($method);
  383. }
  384. break;
  385. case 'vacation':
  386. array_push($exts, 'vacation');
  387. $action_script .= 'vacation';
  388. if (!empty($action['days']))
  389. $action_script .= " :days " . $action['days'];
  390. if (!empty($action['addresses']))
  391. $action_script .= " :addresses " . self::escape_string($action['addresses']);
  392. if (!empty($action['subject']))
  393. $action_script .= " :subject " . self::escape_string($action['subject']);
  394. if (!empty($action['handle']))
  395. $action_script .= " :handle " . self::escape_string($action['handle']);
  396. if (!empty($action['from']))
  397. $action_script .= " :from " . self::escape_string($action['from']);
  398. if (!empty($action['mime']))
  399. $action_script .= " :mime";
  400. $action_script .= " " . self::escape_string($action['reason']);
  401. break;
  402. }
  403. if ($action_script) {
  404. $script .= !empty($tests) ? "\t" : '';
  405. $script .= $action_script . ";\n";
  406. }
  407. }
  408. }
  409. if ($script) {
  410. $output .= $script . (!empty($tests) ? "}\n" : '');
  411. $idx++;
  412. }
  413. }
  414. // requires
  415. if (!empty($exts))
  416. $output = 'require ["' . implode('","', array_unique($exts)) . "\"];\n" . $output;
  417. if (!empty($this->prefix)) {
  418. $output = $this->prefix . "\n\n" . $output;
  419. }
  420. return $output;
  421. }
  422. /**
  423. * Returns script object
  424. *
  425. */
  426. public function as_array()
  427. {
  428. return $this->content;
  429. }
  430. /**
  431. * Returns array of supported extensions
  432. *
  433. */
  434. public function get_extensions()
  435. {
  436. return array_values($this->supported);
  437. }
  438. /**
  439. * Converts text script to rules array
  440. *
  441. * @param string Text script
  442. */
  443. private function _parse_text($script)
  444. {
  445. $prefix = '';
  446. $options = array();
  447. while ($script) {
  448. $script = trim($script);
  449. $rule = array();
  450. // Comments
  451. while (!empty($script) && $script[0] == '#') {
  452. $endl = strpos($script, "\n");
  453. $line = $endl ? substr($script, 0, $endl) : $script;
  454. // Roundcube format
  455. if (preg_match('/^# rule:\[(.*)\]/', $line, $matches)) {
  456. $rulename = $matches[1];
  457. }
  458. // KEP:14 variables
  459. else if (preg_match('/^# (EDITOR|EDITOR_VERSION) (.+)$/', $line, $matches)) {
  460. $this->set_var($matches[1], $matches[2]);
  461. }
  462. // Horde-Ingo format
  463. else if (!empty($options['format']) && $options['format'] == 'INGO'
  464. && preg_match('/^# (.*)/', $line, $matches)
  465. ) {
  466. $rulename = $matches[1];
  467. }
  468. else if (empty($options['prefix'])) {
  469. $prefix .= $line . "\n";
  470. }
  471. $script = ltrim(substr($script, strlen($line) + 1));
  472. }
  473. // handle script header
  474. if (empty($options['prefix'])) {
  475. $options['prefix'] = true;
  476. if ($prefix && strpos($prefix, 'horde.org/ingo')) {
  477. $options['format'] = 'INGO';
  478. }
  479. }
  480. // Control structures/blocks
  481. if (preg_match('/^(if|else|elsif)/i', $script)) {
  482. $rule = $this->_tokenize_rule($script);
  483. if (strlen($rulename) && !empty($rule)) {
  484. $rule['name'] = $rulename;
  485. }
  486. }
  487. // Simple commands
  488. else {
  489. $rule = $this->_parse_actions($script, ';');
  490. if (!empty($rule[0]) && is_array($rule)) {
  491. // set "global" variables
  492. if ($rule[0]['type'] == 'set') {
  493. unset($rule[0]['type']);
  494. $this->vars[] = $rule[0];
  495. }
  496. else {
  497. $rule = array('actions' => $rule);
  498. }
  499. }
  500. }
  501. $rulename = '';
  502. if (!empty($rule)) {
  503. $this->content[] = $rule;
  504. }
  505. }
  506. if (!empty($prefix)) {
  507. $this->prefix = trim($prefix);
  508. }
  509. }
  510. /**
  511. * Convert text script fragment to rule object
  512. *
  513. * @param string Text rule
  514. *
  515. * @return array Rule data
  516. */
  517. private function _tokenize_rule(&$content)
  518. {
  519. $cond = strtolower(self::tokenize($content, 1));
  520. if ($cond != 'if' && $cond != 'elsif' && $cond != 'else') {
  521. return null;
  522. }
  523. $disabled = false;
  524. $join = false;
  525. // disabled rule (false + comment): if false # .....
  526. if (preg_match('/^\s*false\s+#/i', $content)) {
  527. $content = preg_replace('/^\s*false\s+#\s*/i', '', $content);
  528. $disabled = true;
  529. }
  530. while (strlen($content)) {
  531. $tokens = self::tokenize($content, true);
  532. $separator = array_pop($tokens);
  533. if (!empty($tokens)) {
  534. $token = array_shift($tokens);
  535. }
  536. else {
  537. $token = $separator;
  538. }
  539. $token = strtolower($token);
  540. if ($token == 'not') {
  541. $not = true;
  542. $token = strtolower(array_shift($tokens));
  543. }
  544. else {
  545. $not = false;
  546. }
  547. switch ($token) {
  548. case 'allof':
  549. $join = true;
  550. break;
  551. case 'anyof':
  552. break;
  553. case 'size':
  554. $size = array('test' => 'size', 'not' => $not);
  555. for ($i=0, $len=count($tokens); $i<$len; $i++) {
  556. if (!is_array($tokens[$i])
  557. && preg_match('/^:(under|over)$/i', $tokens[$i])
  558. ) {
  559. $size['type'] = strtolower(substr($tokens[$i], 1));
  560. }
  561. else {
  562. $size['arg'] = $tokens[$i];
  563. }
  564. }
  565. $tests[] = $size;
  566. break;
  567. case 'header':
  568. $header = array('test' => 'header', 'not' => $not, 'arg1' => '', 'arg2' => '');
  569. for ($i=0, $len=count($tokens); $i<$len; $i++) {
  570. if (!is_array($tokens[$i]) && preg_match('/^:comparator$/i', $tokens[$i])) {
  571. $header['comparator'] = $tokens[++$i];
  572. }
  573. else if (!is_array($tokens[$i]) && preg_match('/^:(count|value)$/i', $tokens[$i])) {
  574. $header['type'] = strtolower(substr($tokens[$i], 1)) . '-' . $tokens[++$i];
  575. }
  576. else if (!is_array($tokens[$i]) && preg_match('/^:(is|contains|matches|regex)$/i', $tokens[$i])) {
  577. $header['type'] = strtolower(substr($tokens[$i], 1));
  578. }
  579. else {
  580. $header['arg1'] = $header['arg2'];
  581. $header['arg2'] = $tokens[$i];
  582. }
  583. }
  584. $tests[] = $header;
  585. break;
  586. case 'address':
  587. case 'envelope':
  588. $header = array('test' => $token, 'not' => $not, 'arg1' => '', 'arg2' => '');
  589. for ($i=0, $len=count($tokens); $i<$len; $i++) {
  590. if (!is_array($tokens[$i]) && preg_match('/^:comparator$/i', $tokens[$i])) {
  591. $header['comparator'] = $tokens[++$i];
  592. }
  593. else if (!is_array($tokens[$i]) && preg_match('/^:(is|contains|matches|regex)$/i', $tokens[$i])) {
  594. $header['type'] = strtolower(substr($tokens[$i], 1));
  595. }
  596. else if (!is_array($tokens[$i]) && preg_match('/^:(localpart|domain|all|user|detail)$/i', $tokens[$i])) {
  597. $header['part'] = strtolower(substr($tokens[$i], 1));
  598. }
  599. else {
  600. $header['arg1'] = $header['arg2'];
  601. $header['arg2'] = $tokens[$i];
  602. }
  603. }
  604. $tests[] = $header;
  605. break;
  606. case 'body':
  607. $header = array('test' => 'body', 'not' => $not, 'arg' => '');
  608. for ($i=0, $len=count($tokens); $i<$len; $i++) {
  609. if (!is_array($tokens[$i]) && preg_match('/^:comparator$/i', $tokens[$i])) {
  610. $header['comparator'] = $tokens[++$i];
  611. }
  612. else if (!is_array($tokens[$i]) && preg_match('/^:(is|contains|matches|regex)$/i', $tokens[$i])) {
  613. $header['type'] = strtolower(substr($tokens[$i], 1));
  614. }
  615. else if (!is_array($tokens[$i]) && preg_match('/^:(raw|content|text)$/i', $tokens[$i])) {
  616. $header['part'] = strtolower(substr($tokens[$i], 1));
  617. if ($header['part'] == 'content') {
  618. $header['content'] = $tokens[++$i];
  619. }
  620. }
  621. else {
  622. $header['arg'] = $tokens[$i];
  623. }
  624. }
  625. $tests[] = $header;
  626. break;
  627. case 'exists':
  628. $tests[] = array('test' => 'exists', 'not' => $not,
  629. 'arg' => array_pop($tokens));
  630. break;
  631. case 'true':
  632. $tests[] = array('test' => 'true', 'not' => $not);
  633. break;
  634. case 'false':
  635. $tests[] = array('test' => 'true', 'not' => !$not);
  636. break;
  637. }
  638. // goto actions...
  639. if ($separator == '{') {
  640. break;
  641. }
  642. }
  643. // ...and actions block
  644. $actions = $this->_parse_actions($content);
  645. if ($tests && $actions) {
  646. $result = array(
  647. 'type' => $cond,
  648. 'tests' => $tests,
  649. 'actions' => $actions,
  650. 'join' => $join,
  651. 'disabled' => $disabled,
  652. );
  653. }
  654. return $result;
  655. }
  656. /**
  657. * Parse body of actions section
  658. *
  659. * @param string $content Text body
  660. * @param string $end End of text separator
  661. *
  662. * @return array Array of parsed action type/target pairs
  663. */
  664. private function _parse_actions(&$content, $end = '}')
  665. {
  666. $result = null;
  667. while (strlen($content)) {
  668. $tokens = self::tokenize($content, true);
  669. $separator = array_pop($tokens);
  670. if (!empty($tokens)) {
  671. $token = array_shift($tokens);
  672. }
  673. else {
  674. $token = $separator;
  675. }
  676. switch ($token) {
  677. case 'discard':
  678. case 'keep':
  679. case 'stop':
  680. $result[] = array('type' => $token);
  681. break;
  682. case 'fileinto':
  683. case 'redirect':
  684. $copy = false;
  685. $target = '';
  686. for ($i=0, $len=count($tokens); $i<$len; $i++) {
  687. if (strtolower($tokens[$i]) == ':copy') {
  688. $copy = true;
  689. }
  690. else {
  691. $target = $tokens[$i];
  692. }
  693. }
  694. $result[] = array('type' => $token, 'copy' => $copy,
  695. 'target' => $target);
  696. break;
  697. case 'reject':
  698. case 'ereject':
  699. $result[] = array('type' => $token, 'target' => array_pop($tokens));
  700. break;
  701. case 'vacation':
  702. $vacation = array('type' => 'vacation', 'reason' => array_pop($tokens));
  703. for ($i=0, $len=count($tokens); $i<$len; $i++) {
  704. $tok = strtolower($tokens[$i]);
  705. if ($tok == ':days') {
  706. $vacation['days'] = $tokens[++$i];
  707. }
  708. else if ($tok == ':subject') {
  709. $vacation['subject'] = $tokens[++$i];
  710. }
  711. else if ($tok == ':addresses') {
  712. $vacation['addresses'] = $tokens[++$i];
  713. }
  714. else if ($tok == ':handle') {
  715. $vacation['handle'] = $tokens[++$i];
  716. }
  717. else if ($tok == ':from') {
  718. $vacation['from'] = $tokens[++$i];
  719. }
  720. else if ($tok == ':mime') {
  721. $vacation['mime'] = true;
  722. }
  723. }
  724. $result[] = $vacation;
  725. break;
  726. case 'setflag':
  727. case 'addflag':
  728. case 'removeflag':
  729. $result[] = array('type' => $token,
  730. // Flags list: last token (skip optional variable)
  731. 'target' => $tokens[count($tokens)-1]
  732. );
  733. break;
  734. case 'include':
  735. $include = array('type' => 'include', 'target' => array_pop($tokens));
  736. // Parameters: :once, :optional, :global, :personal
  737. for ($i=0, $len=count($tokens); $i<$len; $i++) {
  738. $tok = strtolower($tokens[$i]);
  739. if ($tok[0] == ':') {
  740. $include[substr($tok, 1)] = true;
  741. }
  742. }
  743. $result[] = $include;
  744. break;
  745. case 'set':
  746. $set = array('type' => 'set', 'value' => array_pop($tokens), 'name' => array_pop($tokens));
  747. // Parameters: :lower :upper :lowerfirst :upperfirst :quotewildcard :length
  748. for ($i=0, $len=count($tokens); $i<$len; $i++) {
  749. $tok = strtolower($tokens[$i]);
  750. if ($tok[0] == ':') {
  751. $set[substr($tok, 1)] = true;
  752. }
  753. }
  754. $result[] = $set;
  755. break;
  756. case 'require':
  757. // skip, will be build according to used commands
  758. // $result[] = array('type' => 'require', 'target' => $tokens);
  759. break;
  760. case 'notify':
  761. $notify = array('type' => 'notify');
  762. $priorities = array(':high' => 1, ':normal' => 2, ':low' => 3);
  763. // Parameters: :from, :importance, :options, :message
  764. // additional (optional) :method parameter for notify extension
  765. for ($i=0, $len=count($tokens); $i<$len; $i++) {
  766. $tok = strtolower($tokens[$i]);
  767. if ($tok[0] == ':') {
  768. // Here we support only 00 version of notify draft, there
  769. // were a couple regressions in 00 to 04 changelog, we use
  770. // the version used by Cyrus
  771. if (isset($priorities[$tok])) {
  772. $notify['importance'] = $priorities[$tok];
  773. }
  774. else {
  775. $notify[substr($tok, 1)] = $tokens[++$i];
  776. }
  777. }
  778. else {
  779. // unnamed parameter is a :method in enotify extension
  780. $notify['method'] = $tokens[$i];
  781. }
  782. }
  783. $method_components = parse_url($notify['method']);
  784. if ($method_components['scheme'] == 'mailto') {
  785. $notify['address'] = $method_components['path'];
  786. $method_params = array();
  787. if (array_key_exists('query', $method_components)) {
  788. parse_str($method_components['query'], $method_params);
  789. }
  790. $method_params = array_change_key_case($method_params, CASE_LOWER);
  791. // magic_quotes_gpc and magic_quotes_sybase affect the output of parse_str
  792. if (ini_get('magic_quotes_gpc') || ini_get('magic_quotes_sybase')) {
  793. array_map('stripslashes', $method_params);
  794. }
  795. $notify['body'] = (array_key_exists('body', $method_params)) ? $method_params['body'] : '';
  796. }
  797. $result[] = $notify;
  798. break;
  799. }
  800. if ($separator == $end)
  801. break;
  802. }
  803. return $result;
  804. }
  805. /**
  806. *
  807. */
  808. private function add_comparator($test, &$out, &$exts)
  809. {
  810. if (empty($test['comparator'])) {
  811. return;
  812. }
  813. if ($test['comparator'] == 'i;ascii-numeric') {
  814. array_push($exts, 'relational');
  815. array_push($exts, 'comparator-i;ascii-numeric');
  816. }
  817. else if (!in_array($test['comparator'], array('i;octet', 'i;ascii-casemap'))) {
  818. array_push($exts, 'comparator-' . $test['comparator']);
  819. }
  820. // skip default comparator
  821. if ($test['comparator'] != 'i;ascii-casemap') {
  822. $out .= ' :comparator ' . self::escape_string($test['comparator']);
  823. }
  824. }
  825. /**
  826. * Escape special chars into quoted string value or multi-line string
  827. * or list of strings
  828. *
  829. * @param string $str Text or array (list) of strings
  830. *
  831. * @return string Result text
  832. */
  833. static function escape_string($str)
  834. {
  835. if (is_array($str) && count($str) > 1) {
  836. foreach($str as $idx => $val)
  837. $str[$idx] = self::escape_string($val);
  838. return '[' . implode(',', $str) . ']';
  839. }
  840. else if (is_array($str)) {
  841. $str = array_pop($str);
  842. }
  843. // multi-line string
  844. if (preg_match('/[\r\n\0]/', $str) || strlen($str) > 1024) {
  845. return sprintf("text:\n%s\n.\n", self::escape_multiline_string($str));
  846. }
  847. // quoted-string
  848. else {
  849. return '"' . addcslashes($str, '\\"') . '"';
  850. }
  851. }
  852. /**
  853. * Escape special chars in multi-line string value
  854. *
  855. * @param string $str Text
  856. *
  857. * @return string Text
  858. */
  859. static function escape_multiline_string($str)
  860. {
  861. $str = preg_split('/(\r?\n)/', $str, -1, PREG_SPLIT_DELIM_CAPTURE);
  862. foreach ($str as $idx => $line) {
  863. // dot-stuffing
  864. if (isset($line[0]) && $line[0] == '.') {
  865. $str[$idx] = '.' . $line;
  866. }
  867. }
  868. return implode($str);
  869. }
  870. /**
  871. * Splits script into string tokens
  872. *
  873. * @param string &$str The script
  874. * @param mixed $num Number of tokens to return, 0 for all
  875. * or True for all tokens until separator is found.
  876. * Separator will be returned as last token.
  877. * @param int $in_list Enable to call recursively inside a list
  878. *
  879. * @return mixed Tokens array or string if $num=1
  880. */
  881. static function tokenize(&$str, $num=0, $in_list=false)
  882. {
  883. $result = array();
  884. // remove spaces from the beginning of the string
  885. while (($str = ltrim($str)) !== ''
  886. && (!$num || $num === true || count($result) < $num)
  887. ) {
  888. switch ($str[0]) {
  889. // Quoted string
  890. case '"':
  891. $len = strlen($str);
  892. for ($pos=1; $pos<$len; $pos++) {
  893. if ($str[$pos] == '"') {
  894. break;
  895. }
  896. if ($str[$pos] == "\\") {
  897. if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") {
  898. $pos++;
  899. }
  900. }
  901. }
  902. if ($str[$pos] != '"') {
  903. // error
  904. }
  905. // we need to strip slashes for a quoted string
  906. $result[] = stripslashes(substr($str, 1, $pos - 1));
  907. $str = substr($str, $pos + 1);
  908. break;
  909. // Parenthesized list
  910. case '[':
  911. $str = substr($str, 1);
  912. $result[] = self::tokenize($str, 0, true);
  913. break;
  914. case ']':
  915. $str = substr($str, 1);
  916. return $result;
  917. break;
  918. // list/test separator
  919. case ',':
  920. // command separator
  921. case ';':
  922. // block/tests-list
  923. case '(':
  924. case ')':
  925. case '{':
  926. case '}':
  927. $sep = $str[0];
  928. $str = substr($str, 1);
  929. if ($num === true) {
  930. $result[] = $sep;
  931. break 2;
  932. }
  933. break;
  934. // bracket-comment
  935. case '/':
  936. if ($str[1] == '*') {
  937. if ($end_pos = strpos($str, '*/')) {
  938. $str = substr($str, $end_pos + 2);
  939. }
  940. else {
  941. // error
  942. $str = '';
  943. }
  944. }
  945. break;
  946. // hash-comment
  947. case '#':
  948. if ($lf_pos = strpos($str, "\n")) {
  949. $str = substr($str, $lf_pos);
  950. break;
  951. }
  952. else {
  953. $str = '';
  954. }
  955. // String atom
  956. default:
  957. // empty or one character
  958. if ($str === '' || $str === null) {
  959. break 2;
  960. }
  961. if (strlen($str) < 2) {
  962. $result[] = $str;
  963. $str = '';
  964. break;
  965. }
  966. // tag/identifier/number
  967. if (preg_match('/^([a-z0-9:_]+)/i', $str, $m)) {
  968. $str = substr($str, strlen($m[1]));
  969. if ($m[1] != 'text:') {
  970. $result[] = $m[1];
  971. }
  972. // multiline string
  973. else {
  974. // possible hash-comment after "text:"
  975. if (preg_match('/^( |\t)*(#[^\n]+)?\n/', $str, $m)) {
  976. $str = substr($str, strlen($m[0]));
  977. }
  978. // get text until alone dot in a line
  979. if (preg_match('/^(.*)\r?\n\.\r?\n/sU', $str, $m)) {
  980. $text = $m[1];
  981. // remove dot-stuffing
  982. $text = str_replace("\n..", "\n.", $text);
  983. $str = substr($str, strlen($m[0]));
  984. }
  985. else {
  986. $text = '';
  987. }
  988. $result[] = $text;
  989. }
  990. }
  991. // fallback, skip one character as infinite loop prevention
  992. else {
  993. $str = substr($str, 1);
  994. }
  995. break;
  996. }
  997. }
  998. return $num === 1 ? (isset($result[0]) ? $result[0] : null) : $result;
  999. }
  1000. }