PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 10.2.2
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v10.2.2
11.1.1 11.1.0 11.0.5 11.0.4 11.0.3 11.0.2 11.0.1 11.0.0 10.11.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 trunk 10.0.0 10.0.1 10.1.0 10.1.1 10.1.2 10.1.3 10.1.4 10.2.0 10.2.1 10.2.2 All 62 releases
acymailing / back / Libraries / Imap2 / Roundcube / ResultThread.php

ResultThread.php in AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress 10.2.2, at back/Libraries/Imap2/Roundcube/ResultThread.php

665 lines 19.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * +-----------------------------------------------------------------------+
5 * | This file is part of the Roundcube Webmail client |
6 * | |
7 * | Copyright (C) The Roundcube Dev Team |
8 * | Copyright (C) Kolab Systems AG |
9 * | |
10 * | Licensed under the GNU General Public License version 3 or |
11 * | any later version with exceptions for skins & plugins. |
12 * | See the README file for a full license statement. |
13 * | |
14 * | PURPOSE: |
15 * | THREAD response handler |
16 * +-----------------------------------------------------------------------+
17 * | Author: Thomas Bruederli <[email protected]> |
18 * | Author: Aleksander Machniak <[email protected]> |
19 * +-----------------------------------------------------------------------+
20 */
21
22 namespace AcyMailing\Libraries\Imap2\Roundcube;
23
24 use AcyMailing\Libraries\Imap2\Offset;
25 use AcyMailing\Libraries\Imap2\rcube_imap_generic;
26 use AcyMailing\Libraries\Imap2\rcube_result_index;
27
28 /**
29 * Class for accessing IMAP's THREAD result
30 *
31 * @package Framework
32 * @subpackage Storage
33 */
34 class ResultThread
35 {
36 public $incomplete = false;
37
38 protected $raw_data;
39 protected $mailbox;
40 protected $meta = [];
41 protected $order = 'ASC';
42
43 const SEPARATOR_ELEMENT = ' ';
44 const SEPARATOR_ITEM = '~';
45 const SEPARATOR_LEVEL = ':';
46
47
48 /**
49 * Object constructor.
50 */
51 public function __construct($mailbox = null, $data = null)
52 {
53 $this->mailbox = $mailbox;
54 $this->init($data);
55 }
56
57 /**
58 * Initializes object with IMAP command response
59 *
60 * @param string $data IMAP response string
61 */
62 public function init($data = null)
63 {
64 $this->meta = [];
65
66 $data = explode('*', (string)$data);
67
68 // ...skip unilateral untagged server responses
69 for ($i = 0, $len = count($data) ; $i < $len ; $i++) {
70 if (preg_match('/^ THREAD/i', $data[$i])) {
71 // valid response, initialize raw_data for is_error()
72 $this->raw_data = '';
73 $data[$i] = substr($data[$i], 7);
74 break;
75 }
76
77 unset($data[$i]);
78 }
79
80 if (empty($data)) {
81 return;
82 }
83
84 $data = array_shift($data);
85 $data = trim($data);
86 $data = preg_replace('/[\r\n]/', '', $data);
87 $data = preg_replace('/\s+/', ' ', $data);
88
89 $this->raw_data = $this->parse_thread($data);
90 }
91
92 /**
93 * Checks the result from IMAP command
94 *
95 * @return bool True if the result is an error, False otherwise
96 */
97 public function is_error()
98 {
99 return $this->raw_data === null;
100 }
101
102 /**
103 * Checks if the result is empty
104 *
105 * @return bool True if the result is empty, False otherwise
106 */
107 public function is_empty()
108 {
109 return empty($this->raw_data);
110 }
111
112 /**
113 * Returns number of elements (threads) in the result
114 *
115 * @return int Number of elements
116 */
117 public function count()
118 {
119 if (isset($this->meta['count']) && $this->meta['count'] !== null) {
120 return $this->meta['count'];
121 }
122
123 if (empty($this->raw_data)) {
124 $this->meta['count'] = 0;
125 } else {
126 $this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT);
127 }
128
129 if (!$this->meta['count']) {
130 $this->meta['messages'] = 0;
131 }
132
133 return $this->meta['count'];
134 }
135
136 /**
137 * Returns number of all messages in the result
138 *
139 * @return int Number of elements
140 */
141 public function count_messages()
142 {
143 if ($this->meta['messages'] !== null) {
144 return $this->meta['messages'];
145 }
146
147 if (empty($this->raw_data)) {
148 $this->meta['messages'] = 0;
149 } else {
150 $this->meta['messages'] = 1
151 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT)
152 + substr_count($this->raw_data, self::SEPARATOR_ITEM);
153 }
154
155 if ($this->meta['messages'] == 0 || $this->meta['messages'] == 1) {
156 $this->meta['count'] = $this->meta['messages'];
157 }
158
159 return $this->meta['messages'];
160 }
161
162 /**
163 * Returns maximum message identifier in the result
164 *
165 * @return int Maximum message identifier
166 */
167 public function max()
168 {
169 if (!isset($this->meta['max'])) {
170 $this->meta['max'] = (int)@max($this->get());
171 }
172
173 return $this->meta['max'];
174 }
175
176 /**
177 * Returns minimum message identifier in the result
178 *
179 * @return int Minimum message identifier
180 */
181 public function min()
182 {
183 if (!isset($this->meta['min'])) {
184 $this->meta['min'] = (int)@min($this->get());
185 }
186
187 return $this->meta['min'];
188 }
189
190 /**
191 * Slices data set.
192 *
193 * @param $offset Offset (as for PHP's array_slice())
194 * @param $length Number of elements (as for PHP's array_slice())
195 */
196 public function slice($offset, $length)
197 {
198 $data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
199 $data = array_slice($data, $offset, $length);
200
201 $this->meta = [];
202 $this->meta['count'] = count($data);
203 $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
204 }
205
206 /**
207 * Filters data set. Removes threads not listed in $roots list.
208 *
209 * @param array $roots List of IDs of thread roots.
210 */
211 public function filter($roots)
212 {
213 $datalen = strlen($this->raw_data);
214 $roots = array_flip($roots);
215 $result = '';
216 $start = 0;
217
218 $this->meta = [];
219 $this->meta['count'] = 0;
220
221 while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
222 || ($start < $datalen && ($pos = $datalen))
223 ) {
224 $len = $pos - $start;
225 $elem = substr($this->raw_data, $start, $len);
226 $start = $pos + 1;
227
228 // extract root message ID
229 if ($npos = strpos($elem, self::SEPARATOR_ITEM)) {
230 $root = (int)substr($elem, 0, $npos);
231 } else {
232 $root = $elem;
233 }
234
235 if (isset($roots[$root])) {
236 $this->meta['count']++;
237 $result .= self::SEPARATOR_ELEMENT.$elem;
238 }
239 }
240
241 $this->raw_data = ltrim($result, self::SEPARATOR_ELEMENT);
242 }
243
244 /**
245 * Reverts order of elements in the result
246 */
247 public function revert()
248 {
249 $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
250
251 if (empty($this->raw_data)) {
252 return;
253 }
254
255 $data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
256 $data = array_reverse($data);
257 $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
258
259 $this->meta['pos'] = [];
260 }
261
262 /**
263 * Check if the given message ID exists in the object
264 *
265 * @param int $msgid Message ID
266 * @param bool $get_index When enabled element's index will be returned.
267 * Elements are indexed starting with 0
268 *
269 * @return boolean True on success, False if message ID doesn't exist
270 */
271 public function exists($msgid, $get_index = false)
272 {
273 $msgid = (int)$msgid;
274 $begin = implode('|', [
275 '^',
276 preg_quote(self::SEPARATOR_ELEMENT, '/'),
277 preg_quote(self::SEPARATOR_LEVEL, '/'),
278 ]);
279 $end = implode('|', [
280 '$',
281 preg_quote(self::SEPARATOR_ELEMENT, '/'),
282 preg_quote(self::SEPARATOR_ITEM, '/'),
283 ]);
284
285 if (preg_match(
286 "/($begin)$msgid($end)/",
287 $this->raw_data,
288 $m,
289 $get_index ? PREG_OFFSET_CAPTURE : null
290 )
291 ) {
292 if ($get_index) {
293 $idx = 0;
294 if ($m[0][1]) {
295 $idx = substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1] + 1)
296 + substr_count($this->raw_data, self::SEPARATOR_ITEM, 0, $m[0][1] + 1);
297 }
298 // cache position of this element, so we can use it in get_element()
299 $this->meta['pos'][$idx] = (int)$m[0][1];
300
301 return $idx;
302 }
303
304 return true;
305 }
306
307 return false;
308 }
309
310 /**
311 * Return IDs of all messages in the result. Threaded data will be flattened.
312 *
313 * @return array List of message identifiers
314 */
315 public function get()
316 {
317 if (empty($this->raw_data)) {
318 return [];
319 }
320
321 $regexp = '/('.preg_quote(self::SEPARATOR_ELEMENT, '/')
322 .'|'.preg_quote(self::SEPARATOR_ITEM, '/').'[0-9]+'.preg_quote(self::SEPARATOR_LEVEL, '/')
323 .')/';
324
325 return preg_split($regexp, $this->raw_data);
326 }
327
328 /**
329 * Return all messages in the result.
330 *
331 * @return array List of message identifiers
332 */
333 public function get_compressed()
334 {
335 if (empty($this->raw_data)) {
336 return '';
337 }
338
339 return rcube_imap_generic::compressMessageSet($this->get());
340 }
341
342 /**
343 * Return result element at specified index (all messages, not roots)
344 *
345 * @param int|string $index Element's index or "FIRST" or "LAST"
346 *
347 * @return int Element value
348 */
349 public function get_element($index)
350 {
351 $count = $this->count();
352
353 if (!$count) {
354 return null;
355 }
356
357 // first element
358 if ($index === 0 || $index === '0' || $index === 'FIRST') {
359 preg_match('/^([0-9]+)/', $this->raw_data, $m);
360 $result = (int)$m[1];
361
362 return $result;
363 }
364
365 // last element
366 if ($index === 'LAST' || $index == $count - 1) {
367 preg_match('/([0-9]+)$/', $this->raw_data, $m);
368 $result = (int)$m[1];
369
370 return $result;
371 }
372
373 // do we know the position of the element or the neighbour of it?
374 if (!empty($this->meta['pos'])) {
375 $element = preg_quote(self::SEPARATOR_ELEMENT, '/');
376 $item = preg_quote(self::SEPARATOR_ITEM, '/').'[0-9]+'.preg_quote(self::SEPARATOR_LEVEL, '/').'?';
377 $regexp = '('.$element.'|'.$item.')';
378
379 if (isset($this->meta['pos'][$index])) {
380 if (preg_match('/([0-9]+)/', $this->raw_data, $m, null, $this->meta['pos'][$index])) {
381 $result = $m[1];
382 }
383 } elseif (isset($this->meta['pos'][$index - 1])) {
384 // get chunk of data after previous element
385 $data = substr($this->raw_data, $this->meta['pos'][$index - 1] + 1, 50);
386 $data = preg_replace('/^[0-9]+/', '', $data); // remove UID at $index position
387 $data = preg_replace("/^$regexp/", '', $data); // remove separator
388 if (preg_match('/^([0-9]+)/', $data, $m)) {
389 $result = $m[1];
390 }
391 } elseif (isset($this->meta['pos'][$index + 1])) {
392 // get chunk of data before next element
393 $pos = max(0, $this->meta['pos'][$index + 1] - 50);
394 $len = min(50, $this->meta['pos'][$index + 1]);
395 $data = substr($this->raw_data, $pos, $len);
396 $data = preg_replace("/$regexp\$/", '', $data); // remove separator
397
398 if (preg_match('/([0-9]+)$/', $data, $m)) {
399 $result = $m[1];
400 }
401 }
402
403 if (isset($result)) {
404 return (int)$result;
405 }
406 }
407
408 // Finally use less effective method
409 $data = $this->get();
410
411 return $data[$index];
412 }
413
414 /**
415 * Returns response parameters e.g. MAILBOX, ORDER
416 *
417 * @param string $param Parameter name
418 *
419 * @return array|string Response parameters or parameter value
420 */
421 public function get_parameters($param = null)
422 {
423 $params = [];
424 $params['MAILBOX'] = $this->mailbox;
425 $params['ORDER'] = $this->order;
426
427 if ($param !== null) {
428 return $params[$param];
429 }
430
431 return $params;
432 }
433
434 /**
435 * THREAD=REFS sorting implementation (based on provided index)
436 *
437 * @param rcube_result_index $index Sorted message identifiers
438 */
439 public function sort($index)
440 {
441 $this->sort_order = $index->get_parameters('ORDER');
442
443 if (empty($this->raw_data)) {
444 return;
445 }
446
447 // when sorting search result it's good to make the index smaller
448 if ($index->count() != $this->count_messages()) {
449 $index->filter($this->get());
450 }
451
452 $result = array_fill_keys($index->get(), null);
453 $datalen = strlen($this->raw_data);
454 $start = 0;
455
456 // Here we're parsing raw_data twice, we want only one big array
457 // in memory at a time
458
459 // Assign roots
460 while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
461 || ($start < $datalen && ($pos = $datalen))
462 ) {
463 $len = $pos - $start;
464 $elem = substr($this->raw_data, $start, $len);
465 $start = $pos + 1;
466
467 $items = explode(self::SEPARATOR_ITEM, $elem);
468 $root = (int)array_shift($items);
469
470 if ($root) {
471 $result[$root] = $root;
472 foreach ($items as $item) {
473 [$lv, $id] = explode(self::SEPARATOR_LEVEL, $item);
474 $result[$id] = $root;
475 }
476 }
477 }
478
479 // get only unique roots
480 $result = array_filter($result); // make sure there are no nulls
481 $result = array_unique($result);
482
483 // Re-sort raw data
484 $result = array_fill_keys($result, null);
485 $start = 0;
486
487 while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
488 || ($start < $datalen && ($pos = $datalen))
489 ) {
490 $len = $pos - $start;
491 $elem = substr($this->raw_data, $start, $len);
492 $start = $pos + 1;
493
494 $npos = strpos($elem, self::SEPARATOR_ITEM);
495 $root = (int)($npos ? substr($elem, 0, $npos) : $elem);
496
497 $result[$root] = $elem;
498 }
499
500 $this->raw_data = implode(self::SEPARATOR_ELEMENT, $result);
501 }
502
503 /**
504 * Returns data as tree
505 *
506 * @return array Data tree
507 */
508 public function get_tree()
509 {
510 $datalen = strlen($this->raw_data);
511 $result = [];
512 $start = 0;
513
514 while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
515 || ($start < $datalen && ($pos = $datalen))
516 ) {
517 $len = $pos - $start;
518 $elem = substr($this->raw_data, $start, $len);
519 $items = explode(self::SEPARATOR_ITEM, $elem);
520 $result[array_shift($items)] = $this->build_thread($items);
521 $start = $pos + 1;
522 }
523
524 return $result;
525 }
526
527 /**
528 * Returns thread depth and children data
529 *
530 * @return array Thread data
531 */
532 public function get_thread_data()
533 {
534 $data = $this->get_tree();
535 $depth = [];
536 $children = [];
537
538 $this->build_thread_data($data, $depth, $children);
539
540 return [$depth, $children];
541 }
542
543 /**
544 * Creates 'depth' and 'children' arrays from stored thread 'tree' data.
545 */
546 protected function build_thread_data($data, &$depth, &$children, $level = 0)
547 {
548 foreach ((array)$data as $key => $val) {
549 $empty = empty($val) || !is_array($val);
550 $children[$key] = !$empty;
551 $depth[$key] = $level;
552 if (!$empty) {
553 $this->build_thread_data($val, $depth, $children, $level + 1);
554 }
555 }
556 }
557
558 /**
559 * Converts part of the raw thread into an array
560 */
561 protected function build_thread($items, $level = 1, &$pos = 0)
562 {
563 $result = [];
564
565 for ($len = count($items) ; $pos < $len ; $pos++) {
566 [$lv, $id] = explode(self::SEPARATOR_LEVEL, $items[$pos]);
567 if ($level == $lv) {
568 $pos++;
569 $result[$id] = $this->build_thread($items, $level + 1, $pos);
570 } else {
571 $pos--;
572 break;
573 }
574 }
575
576 return $result;
577 }
578
579 /**
580 * IMAP THREAD response parser
581 */
582 protected function parse_thread($str, $begin = 0, $end = 0, $depth = 0)
583 {
584 // Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about
585 // 7 times instead :-) See comments on http://uk2.php.net/references and this article:
586 // http://derickrethans.nl/files/phparch-php-variables-article.pdf
587 $node = '';
588 if (!$end) {
589 $end = strlen($str);
590 }
591
592 // Let's try to store data in max. compacted stracture as a string,
593 // arrays handling is much more expensive
594 // For the following structure: THREAD (2)(3 6 (4 23)(44 7 96))
595 // -- 2
596 // -- 3
597 // \-- 6
598 // |-- 4
599 // | \-- 23
600 // |
601 // \-- 44
602 // \-- 7
603 // \-- 96
604 //
605 // The output will be: 2,3^1:6^2:4^3:23^2:44^3:7^4:96
606
607 if ($str[$begin] != '(') {
608 // find next bracket
609 $stop = $begin + strcspn($str, '()', $begin, $end - $begin);
610 $messages = explode(' ', trim(substr($str, $begin, $stop - $begin)));
611
612 if (empty($messages)) {
613 return $node;
614 }
615
616 foreach ($messages as $msg) {
617 if ($msg) {
618 $node .= ($depth ? self::SEPARATOR_ITEM.$depth.self::SEPARATOR_LEVEL : '').$msg;
619 $this->meta['messages']++;
620 $depth++;
621 }
622 }
623
624 if ($stop < $end) {
625 $node .= $this->parse_thread($str, $stop, $end, $depth);
626 }
627 } else {
628 $off = $begin;
629 while ($off < $end) {
630 $start = $off;
631 $off++;
632 $n = 1;
633 while ($n > 0) {
634 $p = strpos($str, ')', $off);
635 if ($p === false) {
636 // error, wrong structure, mismatched brackets in IMAP THREAD response
637 // @TODO: write error to the log or maybe set $this->raw_data = null;
638 return $node;
639 }
640 $p1 = strpos($str, '(', $off);
641 if ($p1 !== false && $p1 < $p) {
642 $off = $p1 + 1;
643 $n++;
644 } else {
645 $off = $p + 1;
646 $n--;
647 }
648 }
649
650 $thread = $this->parse_thread($str, $start + 1, $off - 1, $depth);
651 if ($thread) {
652 if (!$depth) {
653 if ($node) {
654 $node .= self::SEPARATOR_ELEMENT;
655 }
656 }
657 $node .= $thread;
658 }
659 }
660 }
661
662 return $node;
663 }
664 }
665