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