Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

rcube_sieve_script.php 44KB

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