PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.0.5
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.0.5
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 11.0.5, at back/Libraries/Imap2/Roundcube/ResultThread.php

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