PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.1.1
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.1.1
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
← All changes | back/Libraries/Imap2/Roundcube/ImapClient.php +0 -3898 10.10.2 → 11.1.1 View file →
@@ -1,3898 +1,0 @@
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 - * | Provide alternative IMAP library that doesn't rely on the standard |
16 - * | C-Client based version. This allows to function regardless |
17 - * | of whether or not the PHP build it's running on has IMAP |
18 - * | functionality built-in. |
19 - * | |
20 - * | Based on Iloha IMAP Library. See http://ilohamail.org/ for details |
21 - * +-----------------------------------------------------------------------+
22 - * | Author: Aleksander Machniak <[email protected]> |
23 - * | Author: Ryo Chijiiwa <[email protected]> |
24 - * +-----------------------------------------------------------------------+
25 - */
26 -
27 -namespace AcyMailing\Libraries\Imap2\Roundcube;
28 -
29 -/**
30 - * PHP based wrapper class to connect to an IMAP server
31 - *
32 - * @package Framework
33 - * @subpackage Storage
34 - */
35 -class ImapClient
36 -{
37 - public $error;
38 - public $errornum;
39 - public $result;
40 - public $resultcode;
41 - public $selected;
42 - public $data = [];
43 - public $flags = [
44 - 'SEEN' => '\\Seen',
45 - 'DELETED' => '\\Deleted',
46 - 'ANSWERED' => '\\Answered',
47 - 'DRAFT' => '\\Draft',
48 - 'FLAGGED' => '\\Flagged',
49 - 'FORWARDED' => '$Forwarded',
50 - 'MDNSENT' => '$MDNSent',
51 - '*' => '\\*',
52 - ];
53 -
54 - protected $fp;
55 - protected $host;
56 - protected $user;
57 - protected $cmd_tag;
58 - protected $cmd_num = 0;
59 - protected $resourceid;
60 - protected $prefs = [];
61 - protected $logged = false;
62 - protected $capability = [];
63 - protected $capability_readed = false;
64 - protected $debug = false;
65 - protected $debug_handler = false;
66 - protected $rawLastLine;
67 -
68 - const ERROR_OK = 0;
69 - const ERROR_NO = -1;
70 - const ERROR_BAD = -2;
71 - const ERROR_BYE = -3;
72 - const ERROR_UNKNOWN = -4;
73 - const ERROR_COMMAND = -5;
74 - const ERROR_READONLY = -6;
75 -
76 - const COMMAND_NORESPONSE = 1;
77 - const COMMAND_CAPABILITY = 2;
78 - const COMMAND_LASTLINE = 4;
79 - const COMMAND_ANONYMIZED = 8;
80 - const COMMAND_RAW_LASTLINE = 16;
81 -
82 - const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n
83 -
84 -
85 - /**
86 - * Send simple (one line) command to the connection stream
87 - *
88 - * @param string $string Command string
89 - * @param bool $endln True if CRLF need to be added at the end of command
90 - * @param bool $anonymized Don't write the given data to log but a placeholder
91 - *
92 - * @param int Number of bytes sent, False on error
93 - */
94 - protected function putLine($string, $endln = true, $anonymized = false)
95 - {
96 - if (!$this->fp) {
97 - return false;
98 - }
99 -
100 - if ($this->debug) {
101 - // anonymize the sent command for logging
102 - $cut = $endln ? 2 : 0;
103 - if ($anonymized && preg_match('/^(A\d+ (?:[A-Z]+ )+)(.+)/', $string, $m)) {
104 - $log = $m[1].sprintf('****** [%d]', strlen($m[2]) - $cut);
105 - } elseif ($anonymized) {
106 - $log = sprintf('****** [%d]', strlen($string) - $cut);
107 - } else {
108 - $log = rtrim($string);
109 - }
110 -
111 - $this->debug('C: '.$log);
112 - }
113 -
114 - if ($endln) {
115 - $string .= "\r\n";
116 - }
117 -
118 - $res = fwrite($this->fp, $string);
119 -
120 - if ($res === false) {
121 - $this->closeSocket();
122 - }
123 -
124 - return $res;
125 - }
126 -
127 - /**
128 - * Send command to the connection stream with Command Continuation
129 - * Requests (RFC3501 7.5) and LITERAL+ (RFC2088) support
130 - *
131 - * @param string $string Command string
132 - * @param bool $endln True if CRLF need to be added at the end of command
133 - * @param bool $anonymized Don't write the given data to log but a placeholder
134 - *
135 - * @return int|bool Number of bytes sent, False on error
136 - */
137 - protected function putLineC($string, $endln = true, $anonymized = false)
138 - {
139 - if (!$this->fp) {
140 - return false;
141 - }
142 -
143 - if ($endln) {
144 - $string .= "\r\n";
145 - }
146 -
147 - $res = 0;
148 - if ($parts = preg_split('/(\{[0-9]+\}\r\n)/m', $string, -1, PREG_SPLIT_DELIM_CAPTURE)) {
149 - for ($i = 0, $cnt = count($parts) ; $i < $cnt ; $i++) {
150 - if (isset($parts[$i + 1]) && preg_match('/^\{([0-9]+)\}\r\n$/', $parts[$i + 1], $matches)) {
151 - // LITERAL+ support
152 - if ($this->prefs['literal+']) {
153 - $parts[$i + 1] = sprintf("{%d+}\r\n", $matches[1]);
154 - }
155 -
156 - $bytes = $this->putLine($parts[$i].$parts[$i + 1], false, $anonymized);
157 - if ($bytes === false) {
158 - return false;
159 - }
160 -
161 - $res += $bytes;
162 -
163 - // don't wait if server supports LITERAL+ capability
164 - if (!$this->prefs['literal+']) {
165 - $line = $this->readLine(1000);
166 - // handle error in command
167 - if ($line[0] != '+') {
168 - return false;
169 - }
170 - }
171 -
172 - $i++;
173 - } else {
174 - $bytes = $this->putLine($parts[$i], false, $anonymized);
175 - if ($bytes === false) {
176 - return false;
177 - }
178 -
179 - $res += $bytes;
180 - }
181 - }
182 - }
183 -
184 - return $res;
185 - }
186 -
187 - /**
188 - * Reads line from the connection stream
189 - *
190 - * @param int $size Buffer size
191 - *
192 - * @return string Line of text response
193 - */
194 - protected function readLine($size = 1024)
195 - {
196 - $line = '';
197 -
198 - if (!$size) {
199 - $size = 1024;
200 - }
201 -
202 - do {
203 - if ($this->eof()) {
204 - return $line ? : null;
205 - }
206 -
207 - $buffer = fgets($this->fp, $size);
208 -
209 - if ($buffer === false) {
210 - $this->closeSocket();
211 - break;
212 - }
213 -
214 - if ($this->debug) {
215 - $this->debug('S: '.rtrim($buffer));
216 - }
217 -
218 - $line .= $buffer;
219 - } while (substr($buffer, -1) != "\n");
220 -
221 - return $line;
222 - }
223 -
224 - /**
225 - * Reads a line of data from the connection stream inluding all
226 - * string continuation literals.
227 - *
228 - * @param int $size Buffer size
229 - *
230 - * @return string Line of text response
231 - */
232 - protected function readFullLine($size = 1024)
233 - {
234 - $line = $this->readLine($size);
235 -
236 - // include all string literels untile the real end of "line"
237 - while (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) {
238 - $bytes = $m[1];
239 - $out = '';
240 -
241 - while (strlen($out) < $bytes) {
242 - $out = $this->readBytes($bytes);
243 - if ($out === null) {
244 - break;
245 - }
246 -
247 - $line .= $out;
248 - }
249 -
250 - $line .= $this->readLine($size);
251 - }
252 -
253 - return $line;
254 - }
255 -
256 - /**
257 - * Reads more data from the connection stream when provided
258 - * data contain string literal
259 - *
260 - * @param string $line Response text
261 - * @param bool $escape Enables escaping
262 - *
263 - * @return string Line of text response
264 - */
265 - protected function multLine($line, $escape = false)
266 - {
267 - $line = rtrim($line);
268 - if (preg_match('/\{([0-9]+)\}$/', $line, $m)) {
269 - $out = '';
270 - $str = substr($line, 0, -strlen($m[0]));
271 - $bytes = $m[1];
272 -
273 - while (strlen($out) < $bytes) {
274 - $line = $this->readBytes($bytes);
275 - if ($line === null) {
276 - break;
277 - }
278 -
279 - $out .= $line;
280 - }
281 -
282 - $line = $str.($escape ? $this->escape($out) : $out);
283 - }
284 -
285 - return $line;
286 - }
287 -
288 - /**
289 - * Reads specified number of bytes from the connection stream
290 - *
291 - * @param int $bytes Number of bytes to get
292 - *
293 - * @return string Response text
294 - */
295 - protected function readBytes($bytes)
296 - {
297 - $data = '';
298 - $len = 0;
299 -
300 - while ($len < $bytes && !$this->eof()) {
301 - $d = fread($this->fp, $bytes - $len);
302 - if ($this->debug) {
303 - $this->debug('S: '.$d);
304 - }
305 - $data .= $d;
306 - $data_len = strlen($data);
307 - if ($len == $data_len) {
308 - break; // nothing was read -> exit to avoid apache lockups
309 - }
310 - $len = $data_len;
311 - }
312 -
313 - return $data;
314 - }
315 -
316 - /**
317 - * Reads complete response to the IMAP command
318 - *
319 - * @param array $untagged Will be filled with untagged response lines
320 - *
321 - * @return string Response text
322 - */
323 - protected function readReply(&$untagged = null)
324 - {
325 - do {
326 - $line = trim($this->readLine(1024));
327 - // store untagged response lines
328 - if (!empty($line) && $line[0] == '*') {
329 - $untagged[] = $line;
330 - }
331 - } while (!empty($line) && $line[0] == '*');
332 -
333 - if ($untagged) {
334 - $untagged = implode("\n", $untagged);
335 - }
336 -
337 - return $line;
338 - }
339 -
340 - /**
341 - * Response parser.
342 - *
343 - * @param string $string Response text
344 - * @param string $err_prefix Error message prefix
345 - *
346 - * @return int Response status
347 - */
348 - protected function parseResult($string, $err_prefix = '')
349 - {
350 - if (preg_match('/^[a-z0-9*]+ (OK|NO|BAD|BYE)(.*)$/i', trim($string), $matches)) {
351 - $res = strtoupper($matches[1]);
352 - $str = trim($matches[2]);
353 -
354 - if ($res == 'OK') {
355 - $this->errornum = self::ERROR_OK;
356 - } elseif ($res == 'NO') {
357 - $this->errornum = self::ERROR_NO;
358 - } elseif ($res == 'BAD') {
359 - $this->errornum = self::ERROR_BAD;
360 - } elseif ($res == 'BYE') {
361 - $this->closeSocket();
362 - $this->errornum = self::ERROR_BYE;
363 - }
364 -
365 - if ($str) {
366 - $str = trim($str);
367 - // get response string and code (RFC5530)
368 - if (preg_match("/^\[([a-z-]+)\]/i", $str, $m)) {
369 - $this->resultcode = strtoupper($m[1]);
370 - $str = trim(substr($str, strlen($m[1]) + 2));
371 - } else {
372 - $this->resultcode = null;
373 - // parse response for [APPENDUID 1204196876 3456]
374 - if (preg_match("/^\[APPENDUID [0-9]+ ([0-9]+)\]/i", $str, $m)) {
375 - $this->data['APPENDUID'] = $m[1];
376 - } // parse response for [COPYUID 1204196876 3456:3457 123:124]
377 - elseif (preg_match("/^\[COPYUID [0-9]+ ([0-9,:]+) ([0-9,:]+)\]/i", $str, $m)) {
378 - $this->data['COPYUID'] = [$m[1], $m[2]];
379 - }
380 - }
381 -
382 - $this->result = $str;
383 -
384 - if ($this->errornum != self::ERROR_OK) {
385 - $this->error = $err_prefix ? $err_prefix.$str : $str;
386 - }
387 - }
388 -
389 - return $this->errornum;
390 - }
391 -
392 - return self::ERROR_UNKNOWN;
393 - }
394 -
395 - /**
396 - * Checks connection stream state.
397 - *
398 - * @return bool True if connection is closed
399 - */
400 - protected function eof()
401 - {
402 - if (!is_resource($this->fp)) {
403 - return true;
404 - }
405 -
406 - // If a connection opened by fsockopen() wasn't closed
407 - // by the server, feof() will hang.
408 - $start = microtime(true);
409 -
410 - if (feof($this->fp) ||
411 - ($this->prefs['timeout'] && (microtime(true) - $start > $this->prefs['timeout']))
412 - ) {
413 - $this->closeSocket();
414 -
415 - return true;
416 - }
417 -
418 - return false;
419 - }
420 -
421 - /**
422 - * Closes connection stream.
423 - */
424 - protected function closeSocket()
425 - {
426 - if (!empty($this->fp)) {
427 - @fclose($this->fp);
428 - }
429 - $this->fp = null;
430 - }
431 -
432 - /**
433 - * Error code/message setter.
434 - */
435 - public function setError($code, $msg = '')
436 - {
437 - $this->errornum = $code;
438 - $this->error = $msg;
439 -
440 - return $code;
441 - }
442 -
443 - /**
444 - * Checks response status.
445 - * Checks if command response line starts with specified prefix (or * BYE/BAD)
446 - *
447 - * @param string $string Response text
448 - * @param string $match Prefix to match with (case-sensitive)
449 - * @param bool $error Enables BYE/BAD checking
450 - * @param bool $nonempty Enables empty response checking
451 - *
452 - * @return bool True any check is true or connection is closed.
453 - */
454 - protected function startsWith($string, $match, $error = false, $nonempty = false)
455 - {
456 - if (!$this->fp) {
457 - return true;
458 - }
459 -
460 - if (strncmp($string, $match, strlen($match)) == 0) {
461 - return true;
462 - }
463 -
464 - if ($error && preg_match('/^\* (BYE|BAD) /i', $string, $m)) {
465 - if (strtoupper($m[1]) == 'BYE') {
466 - $this->closeSocket();
467 - }
468 -
469 - return true;
470 - }
471 -
472 - if ($nonempty && !strlen($string)) {
473 - return true;
474 - }
475 -
476 - return false;
477 - }
478 -
479 - /**
480 - * Capabilities checker
481 - */
482 - protected function hasCapability($name)
483 - {
484 - if (empty($this->capability) || $name == '') {
485 - return false;
486 - }
487 -
488 - if (in_array($name, $this->capability)) {
489 - return true;
490 - } elseif (strpos($name, '=')) {
491 - return false;
492 - }
493 -
494 - $result = [];
495 - foreach ($this->capability as $cap) {
496 - $entry = explode('=', $cap);
497 - if ($entry[0] == $name) {
498 - $result[] = $entry[1];
499 - }
500 - }
501 -
502 - return $result ? : false;
503 - }
504 -
505 - /**
506 - * Capabilities checker
507 - *
508 - * @param string $name Capability name
509 - *
510 - * @return mixed Capability values array for key=value pairs, true/false for others
511 - */
512 - public function getCapability($name)
513 - {
514 - $result = $this->hasCapability($name);
515 -
516 - if (!empty($result)) {
517 - return $result;
518 - } elseif ($this->capability_readed) {
519 - return false;
520 - }
521 -
522 - // get capabilities (only once) because initial
523 - // optional CAPABILITY response may differ
524 - $result = $this->execute('CAPABILITY');
525 - if ($result[0] == self::ERROR_OK) {
526 - $this->parseCapability($result[1]);
527 - }
528 -
529 - $this->capability_readed = true;
530 -
531 - return $this->hasCapability($name);
532 - }
533 -
534 - /**
535 - * Clears detected server capabilities
536 - */
537 - public function clearCapability()
538 - {
539 - $this->capability = [];
540 - $this->capability_readed = false;
541 - }
542 -
543 - /**
544 - * Authentication
545 - *
546 - * @return resource|string Connection resource on success, error code on error
547 - */
548 - protected function authenticate(string $user, string $pass)
549 - {
550 - $auth = base64_encode("user=$user\1auth=Bearer $pass\1\1");
551 - $this->putLine($this->nextTag()." AUTHENTICATE XOAUTH2 $auth", true, true);
552 -
553 - $line = trim($this->readReply());
554 - if (!empty($line) && $line[0] == '+') {
555 - // send empty line
556 - $this->putLine('', true, true);
557 - $line = $this->readReply();
558 - }
559 -
560 - $result = $this->parseResult($line);
561 -
562 - if ($result === self::ERROR_OK) {
563 - // optional CAPABILITY response
564 - if ($line && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
565 - $this->parseCapability($matches[1], true);
566 - }
567 -
568 - return $this->fp;
569 - }
570 -
571 - return $this->setError($result, "AUTHENTICATE XOAUTH2: $line");
572 - }
573 -
574 - /**
575 - * LOGIN Authentication
576 - *
577 - * @param string $user Username
578 - * @param string $pass Password
579 - *
580 - * @return resource Connection resourse on success, error code on error
581 - */
582 - protected function login($user, $password)
583 - {
584 - // Prevent from sending credentials in plain text when connection is not secure
585 - if ($this->getCapability('LOGINDISABLED')) {
586 - return $this->setError(self::ERROR_BAD, "Login disabled by IMAP server");
587 - }
588 -
589 - [$code, $response] = $this->execute('LOGIN', [
590 - $this->escape($user),
591 - $this->escape($password),
592 - ], self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED);
593 -
594 - // re-set capabilities list if untagged CAPABILITY response provided
595 - if (preg_match('/\* CAPABILITY (.+)/i', $response, $matches)) {
596 - $this->parseCapability($matches[1], true);
597 - }
598 -
599 - if ($code == self::ERROR_OK) {
600 - return $this->fp;
601 - }
602 -
603 - return $code;
604 - }
605 -
606 - /**
607 - * Detects hierarchy delimiter
608 - *
609 - * @return string The delimiter
610 - */
611 - public function getHierarchyDelimiter()
612 - {
613 - if (isset($this->prefs['delimiter']) && $this->prefs['delimiter']) {
614 - return $this->prefs['delimiter'];
615 - }
616 -
617 - // try (LIST "" ""), should return delimiter (RFC2060 Sec 6.3.8)
618 - [$code, $response] = $this->execute(
619 - 'LIST',
620 - [$this->escape(''), $this->escape('')]
621 - );
622 -
623 - if ($code == self::ERROR_OK) {
624 - $args = $this->tokenizeResponse($response, 4);
625 - $delimiter = $args[3];
626 -
627 - if (strlen($delimiter) > 0) {
628 - return ($this->prefs['delimiter'] = $delimiter);
629 - }
630 - }
631 - }
632 -
633 - /**
634 - * NAMESPACE handler (RFC 2342)
635 - *
636 - * @return array Namespace data hash (personal, other, shared)
637 - */
638 - public function getNamespace()
639 - {
640 - if (array_key_exists('namespace', $this->prefs)) {
641 - return $this->prefs['namespace'];
642 - }
643 -
644 - if (!$this->getCapability('NAMESPACE')) {
645 - return self::ERROR_BAD;
646 - }
647 -
648 - [$code, $response] = $this->execute('NAMESPACE');
649 -
650 - if ($code == self::ERROR_OK && preg_match('/^\* NAMESPACE /', $response)) {
651 - $response = substr($response, 11);
652 - $data = $this->tokenizeResponse($response);
653 - }
654 -
655 - if (!is_array($data)) {
656 - return $code;
657 - }
658 -
659 - $this->prefs['namespace'] = [
660 - 'personal' => $data[0],
661 - 'other' => $data[1],
662 - 'shared' => $data[2],
663 - ];
664 -
665 - return $this->prefs['namespace'];
666 - }
667 -
668 - /**
669 - * Connects to IMAP server and authenticates.
670 - *
671 - * @param string $host Server hostname or IP
672 - * @param string $user User name
673 - * @param string $password Password
674 - * @param array $options Connection and class options
675 - *
676 - * @return bool True on success, False on failure
677 - */
678 - public function connect($host, $user, $password, $options = [])
679 - {
680 - // configure
681 - $this->set_prefs($options);
682 -
683 - $this->host = $host;
684 - $this->user = $user;
685 - $this->logged = false;
686 - $this->selected = null;
687 -
688 - // check input
689 - if (empty($host)) {
690 - $this->setError(self::ERROR_BAD, "Empty host");
691 -
692 - return false;
693 - }
694 -
695 - if (empty($user)) {
696 - $this->setError(self::ERROR_NO, "Empty user");
697 -
698 - return false;
699 - }
700 -
701 - if (empty($password) && empty($options['gssapi_cn'])) {
702 - $this->setError(self::ERROR_NO, "Empty password");
703 -
704 - return false;
705 - }
706 -
707 - // Connect
708 - if (!$this->_connect($host)) {
709 - return false;
710 - }
711 -
712 - // Send ID info
713 - if (!empty($this->prefs['ident']) && $this->getCapability('ID')) {
714 - $this->data['ID'] = $this->id($this->prefs['ident']);
715 - }
716 -
717 - // pre-login capabilities can be not complete
718 - $this->capability_readed = false;
719 - $result = $this->authenticate($user, $password);
720 -
721 - // Connected and authenticated
722 - if (is_resource($result)) {
723 - if ($this->prefs['force_caps']) {
724 - $this->clearCapability();
725 - }
726 - $this->logged = true;
727 -
728 - return true;
729 - }
730 -
731 - $this->closeConnection();
732 -
733 - return false;
734 - }
735 -
736 - /**
737 - * Connects to IMAP server.
738 - *
739 - * @param string $host Server hostname or IP
740 - *
741 - * @return bool True on success, False on failure
742 - */
743 - protected function _connect($host)
744 - {
745 - // initialize connection
746 - $this->error = '';
747 - $this->errornum = self::ERROR_OK;
748 -
749 - if (!$this->prefs['port']) {
750 - $this->prefs['port'] = 143;
751 - }
752 -
753 - // check for SSL
754 - if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') {
755 - $host = $this->prefs['ssl_mode'].'://'.$host;
756 - }
757 -
758 - if ($this->prefs['timeout'] <= 0) {
759 - $this->prefs['timeout'] = max(0, intval(ini_get('default_socket_timeout')));
760 - }
761 -
762 - if ($this->debug) {
763 - // set connection identifier for debug output
764 - $this->resourceid = strtoupper(substr(md5(microtime().$host.$this->user), 0, 4));
765 -
766 - $_host = ($this->prefs['ssl_mode'] == 'tls' ? 'tls://' : '').$host.':'.$this->prefs['port'];
767 - $this->debug("Connecting to $_host...");
768 - }
769 -
770 - if (!empty($this->prefs['socket_options'])) {
771 - $context = stream_context_create($this->prefs['socket_options']);
772 - $this->fp = stream_socket_client(
773 - $host.':'.$this->prefs['port'],
774 - $errno,
775 - $errstr,
776 - $this->prefs['timeout'],
777 - STREAM_CLIENT_CONNECT,
778 - $context
779 - );
780 - } else {
781 - $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']);
782 - }
783 -
784 - if (!$this->fp) {
785 - $this->setError(
786 - self::ERROR_BAD,
787 - sprintf(
788 - "Could not connect to %s:%d: %s",
789 - $host,
790 - $this->prefs['port'],
791 - $errstr ? : "Unknown reason"
792 - )
793 - );
794 -
795 - return false;
796 - }
797 -
798 - if ($this->prefs['timeout'] > 0) {
799 - stream_set_timeout($this->fp, $this->prefs['timeout']);
800 - }
801 -
802 - $line = trim(fgets($this->fp, 8192));
803 -
804 - if ($this->debug && $line) {
805 - $this->debug('S: '.$line);
806 - }
807 -
808 - // Connected to wrong port or connection error?
809 - if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) {
810 - if ($line) {
811 - $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line);
812 - } else {
813 - $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']);
814 - }
815 -
816 - $this->setError(self::ERROR_BAD, $error);
817 - $this->closeConnection();
818 -
819 - return false;
820 - }
821 -
822 - $this->data['GREETING'] = trim(preg_replace('/\[[^\]]+\]\s*/', '', $line));
823 -
824 - // RFC3501 [7.1] optional CAPABILITY response
825 - if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
826 - $this->parseCapability($matches[1], true);
827 - }
828 -
829 - // TLS connection
830 - if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
831 - $res = $this->execute('STARTTLS');
832 -
833 - if ($res[0] != self::ERROR_OK) {
834 - $this->closeConnection();
835 -
836 - return false;
837 - }
838 -
839 - if (isset($this->prefs['socket_options']['ssl']['crypto_method'])) {
840 - $crypto_method = $this->prefs['socket_options']['ssl']['crypto_method'];
841 - } else {
842 - // There is no flag to enable all TLS methods. Net_SMTP
843 - // handles enabling TLS similarly.
844 - $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT
845 - | @STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
846 - | @STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
847 - }
848 -
849 - if (!stream_socket_enable_crypto($this->fp, true, $crypto_method)) {
850 - $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
851 - $this->closeConnection();
852 -
853 - return false;
854 - }
855 -
856 - // Now we're secure, capabilities need to be reread
857 - $this->clearCapability();
858 - }
859 -
860 - return true;
861 - }
862 -
863 - /**
864 - * Initializes environment
865 - */
866 - protected function set_prefs($prefs)
867 - {
868 - // set preferences
869 - if (is_array($prefs)) {
870 - $this->prefs = $prefs;
871 - }
872 -
873 - // set auth method
874 - if (!empty($this->prefs['auth_type'])) {
875 - $this->prefs['auth_type'] = strtoupper($this->prefs['auth_type']);
876 - } else {
877 - $this->prefs['auth_type'] = 'CHECK';
878 - }
879 -
880 - // disabled capabilities
881 - if (!empty($this->prefs['disabled_caps'])) {
882 - $this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']);
883 - }
884 -
885 - // additional message flags
886 - if (!empty($this->prefs['message_flags'])) {
887 - $this->flags = array_merge($this->flags, $this->prefs['message_flags']);
888 - unset($this->prefs['message_flags']);
889 - }
890 - }
891 -
892 - /**
893 - * Checks connection status
894 - *
895 - * @return bool True if connection is active and user is logged in, False otherwise.
896 - */
897 - public function connected()
898 - {
899 - return $this->fp && $this->logged;
900 - }
901 -
902 - /**
903 - * Closes connection with logout.
904 - */
905 - public function closeConnection()
906 - {
907 - if ($this->logged && $this->putLine($this->nextTag().' LOGOUT')) {
908 - $this->readReply();
909 - }
910 -
911 - $this->closeSocket();
912 - }
913 -
914 - /**
915 - * Executes SELECT command (if mailbox is already not in selected state)
916 - *
917 - * @param string $mailbox Mailbox name
918 - * @param array $qresync_data QRESYNC data (RFC5162)
919 - *
920 - * @return boolean True on success, false on error
921 - */
922 - public function select($mailbox, $qresync_data = null)
923 - {
924 - if (!strlen($mailbox)) {
925 - return false;
926 - }
927 -
928 - if ($this->selected === $mailbox) {
929 - return true;
930 - }
931 -
932 - $params = [$this->escape($mailbox)];
933 -
934 - // QRESYNC data items
935 - // 0. the last known UIDVALIDITY,
936 - // 1. the last known modification sequence,
937 - // 2. the optional set of known UIDs, and
938 - // 3. an optional parenthesized list of known sequence ranges and their
939 - // corresponding UIDs.
940 - if (!empty($qresync_data)) {
941 - if (!empty($qresync_data[2])) {
942 - $qresync_data[2] = self::compressMessageSet($qresync_data[2]);
943 - }
944 -
945 - $params[] = ['QRESYNC', $qresync_data];
946 - }
947 -
948 - [$code, $response] = $this->execute('SELECT', $params);
949 -
950 - if ($code == self::ERROR_OK) {
951 - $this->clear_mailbox_cache();
952 -
953 - $response = explode("\r\n", $response);
954 - foreach ($response as $line) {
955 - if (preg_match('/^\* OK \[/i', $line)) {
956 - $pos = strcspn($line, ' ]', 6);
957 - $token = strtoupper(substr($line, 6, $pos));
958 - $pos += 7;
959 -
960 - switch ($token) {
961 - case 'UIDNEXT':
962 - case 'UIDVALIDITY':
963 - case 'UNSEEN':
964 - if ($len = strspn($line, '0123456789', $pos)) {
965 - $this->data[$token] = (int)substr($line, $pos, $len);
966 - }
967 - break;
968 -
969 - case 'HIGHESTMODSEQ':
970 - if ($len = strspn($line, '0123456789', $pos)) {
971 - $this->data[$token] = (string)substr($line, $pos, $len);
972 - }
973 - break;
974 -
975 - case 'NOMODSEQ':
976 - $this->data[$token] = true;
977 - break;
978 -
979 - case 'PERMANENTFLAGS':
980 - $start = strpos($line, '(', $pos);
981 - $end = strrpos($line, ')');
982 - if ($start && $end) {
983 - $flags = substr($line, $start + 1, $end - $start - 1);
984 - $this->data[$token] = explode(' ', $flags);
985 - }
986 - break;
987 - }
988 - } elseif (preg_match('/^\* ([0-9]+) (EXISTS|RECENT|FETCH)/i', $line, $match)) {
989 - $token = strtoupper($match[2]);
990 - switch ($token) {
991 - case 'EXISTS':
992 - case 'RECENT':
993 - $this->data[$token] = (int)$match[1];
994 - break;
995 -
996 - case 'FETCH':
997 - // QRESYNC FETCH response (RFC5162)
998 - $line = substr($line, strlen($match[0]));
999 - $fetch_data = $this->tokenizeResponse($line, 1);
1000 - $data = ['id' => $match[1]];
1001 -
1002 - for ($i = 0, $size = count($fetch_data) ; $i < $size ; $i += 2) {
1003 - $data[strtolower($fetch_data[$i])] = $fetch_data[$i + 1];
1004 - }
1005 -
1006 - $this->data['QRESYNC'][$data['uid']] = $data;
1007 - break;
1008 - }
1009 - } // QRESYNC VANISHED response (RFC5162)
1010 - elseif (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
1011 - $line = substr($line, strlen($match[0]));
1012 - $v_data = $this->tokenizeResponse($line, 1);
1013 -
1014 - $this->data['VANISHED'] = $v_data;
1015 - }
1016 - }
1017 -
1018 - $this->data['READ-WRITE'] = $this->resultcode != 'READ-ONLY';
1019 - $this->selected = $mailbox;
1020 -
1021 - return true;
1022 - }
1023 -
1024 - return false;
1025 - }
1026 -
1027 - /**
1028 - * Executes STATUS command
1029 - *
1030 - * @param string $mailbox Mailbox name
1031 - * @param array $items Additional requested item names. By default
1032 - * MESSAGES and UNSEEN are requested. Other defined
1033 - * in RFC3501: UIDNEXT, UIDVALIDITY, RECENT
1034 - *
1035 - * @return array Status item-value hash
1036 - * @since 0.5-beta
1037 - */
1038 - public function status($mailbox, $items = [])
1039 - {
1040 - if (!strlen($mailbox)) {
1041 - return false;
1042 - }
1043 -
1044 - if (!in_array('MESSAGES', $items)) {
1045 - $items[] = 'MESSAGES';
1046 - }
1047 - if (!in_array('UNSEEN', $items)) {
1048 - $items[] = 'UNSEEN';
1049 - }
1050 -
1051 - [$code, $response] = $this->execute(
1052 - 'STATUS',
1053 - [$this->escape($mailbox), '('.implode(' ', $items).')'],
1054 - 0,
1055 - '/^\* STATUS /i'
1056 - );
1057 -
1058 - if ($code == self::ERROR_OK && $response) {
1059 - $result = [];
1060 - $response = substr($response, 9); // remove prefix "* STATUS "
1061 -
1062 - [$mbox, $items] = $this->tokenizeResponse($response, 2);
1063 -
1064 - // Fix for #1487859. Some buggy server returns not quoted
1065 - // folder name with spaces. Let's try to handle this situation
1066 - if (!is_array($items) && ($pos = strpos($response, '(')) !== false) {
1067 - $response = substr($response, $pos);
1068 - $items = $this->tokenizeResponse($response, 1);
1069 - }
1070 -
1071 - if (!is_array($items)) {
1072 - return $result;
1073 - }
1074 -
1075 - for ($i = 0, $len = count($items) ; $i < $len ; $i += 2) {
1076 - $result[$items[$i]] = $items[$i + 1];
1077 - }
1078 -
1079 - $this->data['STATUS:'.$mailbox] = $result;
1080 -
1081 - return $result;
1082 - }
1083 -
1084 - return false;
1085 - }
1086 -
1087 - /**
1088 - * Executes EXPUNGE command
1089 - *
1090 - * @param string $mailbox Mailbox name
1091 - * @param string|array $messages Message UIDs to expunge
1092 - *
1093 - * @return boolean True on success, False on error
1094 - */
1095 - public function expunge($mailbox, $messages = null)
1096 - {
1097 - if (!$this->select($mailbox)) {
1098 - return false;
1099 - }
1100 -
1101 - if (!$this->data['READ-WRITE']) {
1102 - $this->setError(self::ERROR_READONLY, "Mailbox is read-only");
1103 -
1104 - return false;
1105 - }
1106 -
1107 - // Clear internal status cache
1108 - $this->clear_status_cache($mailbox);
1109 -
1110 - if (!empty($messages) && $messages != '*' && $this->hasCapability('UIDPLUS')) {
1111 - $messages = self::compressMessageSet($messages);
1112 - $result = $this->execute('UID EXPUNGE', [$messages], self::COMMAND_NORESPONSE);
1113 - } else {
1114 - $result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE);
1115 - }
1116 -
1117 - if ($result == self::ERROR_OK) {
1118 - $this->selected = null; // state has changed, need to reselect
1119 -
1120 - return true;
1121 - }
1122 -
1123 - return false;
1124 - }
1125 -
1126 - /**
1127 - * Executes CLOSE command
1128 - *
1129 - * @return boolean True on success, False on error
1130 - * @since 0.5
1131 - */
1132 - public function close()
1133 - {
1134 - $result = $this->execute('CLOSE', null, self::COMMAND_NORESPONSE);
1135 -
1136 - if ($result == self::ERROR_OK) {
1137 - $this->selected = null;
1138 -
1139 - return true;
1140 - }
1141 -
1142 - return false;
1143 - }
1144 -
1145 - /**
1146 - * Folder subscription (SUBSCRIBE)
1147 - *
1148 - * @param string $mailbox Mailbox name
1149 - *
1150 - * @return boolean True on success, False on error
1151 - */
1152 - public function subscribe($mailbox)
1153 - {
1154 - $result = $this->execute(
1155 - 'SUBSCRIBE',
1156 - [$this->escape($mailbox)],
1157 - self::COMMAND_NORESPONSE
1158 - );
1159 -
1160 - return $result == self::ERROR_OK;
1161 - }
1162 -
1163 - /**
1164 - * Folder unsubscription (UNSUBSCRIBE)
1165 - *
1166 - * @param string $mailbox Mailbox name
1167 - *
1168 - * @return boolean True on success, False on error
1169 - */
1170 - public function unsubscribe($mailbox)
1171 - {
1172 - $result = $this->execute(
1173 - 'UNSUBSCRIBE',
1174 - [$this->escape($mailbox)],
1175 - self::COMMAND_NORESPONSE
1176 - );
1177 -
1178 - return $result == self::ERROR_OK;
1179 - }
1180 -
1181 - /**
1182 - * Folder creation (CREATE)
1183 - *
1184 - * @param string $mailbox Mailbox name
1185 - * @param array $types Optional folder types (RFC 6154)
1186 - *
1187 - * @return bool True on success, False on error
1188 - */
1189 - public function createFolder($mailbox, $types = null)
1190 - {
1191 - $args = [$this->escape($mailbox)];
1192 -
1193 - // RFC 6154: CREATE-SPECIAL-USE
1194 - if (!empty($types) && $this->getCapability('CREATE-SPECIAL-USE')) {
1195 - $args[] = '(USE ('.implode(' ', $types).'))';
1196 - }
1197 -
1198 - $result = $this->execute('CREATE', $args, self::COMMAND_RAW_LASTLINE);
1199 -
1200 - $this->rawLastLine = $result[1];
1201 -
1202 - return $result[0] == self::ERROR_OK;
1203 - }
1204 -
1205 - /**
1206 - * Folder renaming (RENAME)
1207 - *
1208 - * @param string $mailbox Mailbox name
1209 - *
1210 - * @return bool True on success, False on error
1211 - */
1212 - public function renameFolder($from, $to)
1213 - {
1214 - $result = $this->execute(
1215 - 'RENAME',
1216 - [$this->escape($from), $this->escape($to)],
1217 - self::COMMAND_NORESPONSE
1218 - );
1219 -
1220 - return $result == self::ERROR_OK;
1221 - }
1222 -
1223 - /**
1224 - * Executes DELETE command
1225 - *
1226 - * @param string $mailbox Mailbox name
1227 - *
1228 - * @return boolean True on success, False on error
1229 - */
1230 - public function deleteFolder($mailbox)
1231 - {
1232 - $result = $this->execute(
1233 - 'DELETE',
1234 - [$this->escape($mailbox)],
1235 - self::COMMAND_NORESPONSE
1236 - );
1237 -
1238 - return $result == self::ERROR_OK;
1239 - }
1240 -
1241 - /**
1242 - * Removes all messages in a folder
1243 - *
1244 - * @param string $mailbox Mailbox name
1245 - *
1246 - * @return boolean True on success, False on error
1247 - */
1248 - public function clearFolder($mailbox)
1249 - {
1250 - if ($this->countMessages($mailbox) > 0) {
1251 - $res = $this->flag($mailbox, '1:*', 'DELETED');
1252 - }
1253 -
1254 - if ($res) {
1255 - if ($this->selected === $mailbox) {
1256 - $res = $this->close();
1257 - } else {
1258 - $res = $this->expunge($mailbox);
1259 - }
1260 - }
1261 -
1262 - return $res;
1263 - }
1264 -
1265 - /**
1266 - * Returns list of mailboxes
1267 - *
1268 - * @param string $ref Reference name
1269 - * @param string $mailbox Mailbox name
1270 - * @param array $return_opts (see self::_listMailboxes)
1271 - * @param array $select_opts (see self::_listMailboxes)
1272 - *
1273 - * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
1274 - * is requested, False on error.
1275 - */
1276 - public function listMailboxes($ref, $mailbox, $return_opts = [], $select_opts = [])
1277 - {
1278 - return $this->_listMailboxes($ref, $mailbox, false, $return_opts, $select_opts);
1279 - }
1280 -
1281 - /**
1282 - * Returns list of subscribed mailboxes
1283 - *
1284 - * @param string $ref Reference name
1285 - * @param string $mailbox Mailbox name
1286 - * @param array $return_opts (see self::_listMailboxes)
1287 - *
1288 - * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
1289 - * is requested, False on error.
1290 - */
1291 - public function listSubscribed($ref, $mailbox, $return_opts = [])
1292 - {
1293 - return $this->_listMailboxes($ref, $mailbox, true, $return_opts, null);
1294 - }
1295 -
1296 - /**
1297 - * IMAP LIST/LSUB command
1298 - *
1299 - * @param string $ref Reference name
1300 - * @param string $mailbox Mailbox name
1301 - * @param bool $subscribed Enables returning subscribed mailboxes only
1302 - * @param array $return_opts List of RETURN options (RFC5819: LIST-STATUS, RFC5258: LIST-EXTENDED)
1303 - * Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN,
1304 - * MYRIGHTS, SUBSCRIBED, CHILDREN
1305 - * @param array $select_opts List of selection options (RFC5258: LIST-EXTENDED)
1306 - * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE,
1307 - * SPECIAL-USE (RFC6154)
1308 - *
1309 - * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
1310 - * is requested, False on error.
1311 - */
1312 - protected function _listMailboxes(
1313 - $ref,
1314 - $mailbox,
1315 - $subscribed = false,
1316 - $return_opts = [],
1317 - $select_opts = []
1318 - ) {
1319 - if (!strlen($mailbox)) {
1320 - $mailbox = '*';
1321 - }
1322 -
1323 - $args = [];
1324 - $rets = [];
1325 -
1326 - if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) {
1327 - $select_opts = (array)$select_opts;
1328 -
1329 - $args[] = '('.implode(' ', $select_opts).')';
1330 - }
1331 -
1332 - $args[] = $this->escape($ref);
1333 - $args[] = $this->escape($mailbox);
1334 -
1335 - if (!empty($return_opts) && $this->getCapability('LIST-EXTENDED')) {
1336 - $ext_opts = ['SUBSCRIBED', 'CHILDREN'];
1337 - $rets = array_intersect($return_opts, $ext_opts);
1338 - $return_opts = array_diff($return_opts, $rets);
1339 - }
1340 -
1341 - if (!empty($return_opts) && $this->getCapability('LIST-STATUS')) {
1342 - $lstatus = true;
1343 - $status_opts = ['MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN'];
1344 - $opts = array_diff($return_opts, $status_opts);
1345 - $status_opts = array_diff($return_opts, $opts);
1346 -
1347 - if (!empty($status_opts)) {
1348 - $rets[] = 'STATUS ('.implode(' ', $status_opts).')';
1349 - }
1350 -
1351 - if (!empty($opts)) {
1352 - $rets = array_merge($rets, $opts);
1353 - }
1354 - } else {
1355 - $lstatus = false;
1356 - }
1357 -
1358 - if (!empty($rets)) {
1359 - $args[] = 'RETURN ('.implode(' ', $rets).')';
1360 - }
1361 -
1362 - [$code, $response] = $this->execute($subscribed ? 'LSUB' : 'LIST', $args);
1363 -
1364 - if ($code == self::ERROR_OK) {
1365 - $folders = [];
1366 - $last = 0;
1367 - $pos = 0;
1368 - $response .= "\r\n";
1369 -
1370 - while ($pos = strpos($response, "\r\n", $pos + 1)) {
1371 - // literal string, not real end-of-command-line
1372 - if ($response[$pos - 1] == '}') {
1373 - continue;
1374 - }
1375 -
1376 - $line = substr($response, $last, $pos - $last);
1377 - $last = $pos + 2;
1378 -
1379 - if (!preg_match('/^\* (LIST|LSUB|STATUS|MYRIGHTS) /i', $line, $m)) {
1380 - continue;
1381 - }
1382 -
1383 - $cmd = strtoupper($m[1]);
1384 - $line = substr($line, strlen($m[0]));
1385 -
1386 - // * LIST (<options>) <delimiter> <mailbox>
1387 - if ($cmd == 'LIST' || $cmd == 'LSUB') {
1388 - [$opts, $delim, $mailbox] = $this->tokenizeResponse($line, 3);
1389 -
1390 - // Remove redundant separator at the end of folder name, UW-IMAP bug? (#1488879)
1391 - if ($delim) {
1392 - $mailbox = rtrim($mailbox, $delim);
1393 - }
1394 -
1395 - // Add to result array
1396 - if (!$lstatus) {
1397 - $folders[] = $mailbox;
1398 - } else {
1399 - $folders[$mailbox] = [];
1400 - }
1401 -
1402 - // store folder options
1403 - if ($cmd == 'LIST') {
1404 - // Add to options array
1405 - if (empty($this->data['LIST'][$mailbox])) {
1406 - $this->data['LIST'][$mailbox] = $opts;
1407 - } elseif (!empty($opts)) {
1408 - $this->data['LIST'][$mailbox] = array_unique(
1409 - array_merge(
1410 - $this->data['LIST'][$mailbox],
1411 - $opts
1412 - )
1413 - );
1414 - }
1415 - }
1416 - } elseif ($lstatus) {
1417 - // * STATUS <mailbox> (<result>)
1418 - if ($cmd == 'STATUS') {
1419 - [$mailbox, $status] = $this->tokenizeResponse($line, 2);
1420 -
1421 - for ($i = 0, $len = count($status) ; $i < $len ; $i += 2) {
1422 - [$name, $value] = $this->tokenizeResponse($status, 2);
1423 - $folders[$mailbox][$name] = $value;
1424 - }
1425 - } // * MYRIGHTS <mailbox> <acl>
1426 - elseif ($cmd == 'MYRIGHTS') {
1427 - [$mailbox, $acl] = $this->tokenizeResponse($line, 2);
1428 - $folders[$mailbox]['MYRIGHTS'] = $acl;
1429 - }
1430 - }
1431 - }
1432 -
1433 - return $folders;
1434 - }
1435 -
1436 - return false;
1437 - }
1438 -
1439 - /**
1440 - * Returns count of all messages in a folder
1441 - *
1442 - * @param string $mailbox Mailbox name
1443 - *
1444 - * @return int Number of messages, False on error
1445 - */
1446 - public function countMessages($mailbox)
1447 - {
1448 - if ($this->selected === $mailbox && isset($this->data['EXISTS'])) {
1449 - return $this->data['EXISTS'];
1450 - }
1451 -
1452 - // Check internal cache
1453 - $cache = $this->data['STATUS:'.$mailbox];
1454 - if (!empty($cache) && isset($cache['MESSAGES'])) {
1455 - return (int)$cache['MESSAGES'];
1456 - }
1457 -
1458 - // Try STATUS (should be faster than SELECT)
1459 - $counts = $this->status($mailbox);
1460 - if (is_array($counts)) {
1461 - return (int)$counts['MESSAGES'];
1462 - }
1463 -
1464 - return false;
1465 - }
1466 -
1467 - /**
1468 - * Returns count of messages with \Recent flag in a folder
1469 - *
1470 - * @param string $mailbox Mailbox name
1471 - *
1472 - * @return int Number of messages, False on error
1473 - */
1474 - public function countRecent($mailbox)
1475 - {
1476 - if ($this->selected === $mailbox && isset($this->data['RECENT'])) {
1477 - return $this->data['RECENT'];
1478 - }
1479 -
1480 - // Check internal cache
1481 - $cache = $this->data['STATUS:'.$mailbox];
1482 - if (!empty($cache) && isset($cache['RECENT'])) {
1483 - return (int)$cache['RECENT'];
1484 - }
1485 -
1486 - // Try STATUS (should be faster than SELECT)
1487 - $counts = $this->status($mailbox, ['RECENT']);
1488 - if (is_array($counts)) {
1489 - return (int)$counts['RECENT'];
1490 - }
1491 -
1492 - return false;
1493 - }
1494 -
1495 - /**
1496 - * Returns count of messages without \Seen flag in a specified folder
1497 - *
1498 - * @param string $mailbox Mailbox name
1499 - *
1500 - * @return int Number of messages, False on error
1501 - */
1502 - public function countUnseen($mailbox)
1503 - {
1504 - // Check internal cache
1505 - $cache = $this->data['STATUS:'.$mailbox];
1506 - if (!empty($cache) && isset($cache['UNSEEN'])) {
1507 - return (int)$cache['UNSEEN'];
1508 - }
1509 -
1510 - // Try STATUS (should be faster than SELECT+SEARCH)
1511 - $counts = $this->status($mailbox);
1512 - if (is_array($counts)) {
1513 - return (int)$counts['UNSEEN'];
1514 - }
1515 -
1516 - // Invoke SEARCH as a fallback
1517 - $index = $this->search($mailbox, 'ALL UNSEEN', false, ['COUNT']);
1518 - if (!$index->is_error()) {
1519 - return $index->count();
1520 - }
1521 -
1522 - return false;
1523 - }
1524 -
1525 - /**
1526 - * Executes ID command (RFC2971)
1527 - *
1528 - * @param array $items Client identification information key/value hash
1529 - *
1530 - * @return array|false Server identification information key/value hash, False on error
1531 - * @since 0.6
1532 - */
1533 - public function id($items = [])
1534 - {
1535 - if (is_array($items) && !empty($items)) {
1536 - foreach ($items as $key => $value) {
1537 - $args[] = $this->escape($key, true);
1538 - $args[] = $this->escape($value, true);
1539 - }
1540 - }
1541 -
1542 - [$code, $response] = $this->execute(
1543 - 'ID',
1544 - [!empty($args) ? '('.implode(' ', (array)$args).')' : $this->escape(null)],
1545 - 0,
1546 - '/^\* ID /i'
1547 - );
1548 -
1549 - if ($code == self::ERROR_OK && $response) {
1550 - $response = substr($response, 5); // remove prefix "* ID "
1551 - $items = $this->tokenizeResponse($response, 1);
1552 - $result = [];
1553 -
1554 - if (is_array($items)) {
1555 - for ($i = 0, $len = count($items) ; $i < $len ; $i += 2) {
1556 - $result[$items[$i]] = $items[$i + 1];
1557 - }
1558 - }
1559 -
1560 - return $result;
1561 - }
1562 -
1563 - return false;
1564 - }
1565 -
1566 - /**
1567 - * Executes ENABLE command (RFC5161)
1568 - *
1569 - * @param mixed $extension Extension name to enable (or array of names)
1570 - *
1571 - * @return array|bool List of enabled extensions, False on error
1572 - * @since 0.6
1573 - */
1574 - public function enable($extension)
1575 - {
1576 - if (empty($extension)) {
1577 - return false;
1578 - }
1579 -
1580 - if (!$this->hasCapability('ENABLE')) {
1581 - return false;
1582 - }
1583 -
1584 - if (!is_array($extension)) {
1585 - $extension = [$extension];
1586 - }
1587 -
1588 - if (!empty($this->extensions_enabled)) {
1589 - // check if all extensions are already enabled
1590 - $diff = array_diff($extension, $this->extensions_enabled);
1591 -
1592 - if (empty($diff)) {
1593 - return $extension;
1594 - }
1595 -
1596 - // Make sure the mailbox isn't selected, before enabling extension(s)
1597 - if ($this->selected !== null) {
1598 - $this->close();
1599 - }
1600 - }
1601 -
1602 - [$code, $response] = $this->execute('ENABLE', $extension, 0, '/^\* ENABLED /i');
1603 -
1604 - if ($code == self::ERROR_OK && $response) {
1605 - $response = substr($response, 10); // remove prefix "* ENABLED "
1606 - $result = (array)$this->tokenizeResponse($response);
1607 -
1608 - $this->extensions_enabled = array_unique(array_merge((array)$this->extensions_enabled, $result));
1609 -
1610 - return $this->extensions_enabled;
1611 - }
1612 -
1613 - return false;
1614 - }
1615 -
1616 - /**
1617 - * Executes SORT command
1618 - *
1619 - * @param string $mailbox Mailbox name
1620 - * @param string $field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
1621 - * @param string $criteria Searching criteria
1622 - * @param bool $return_uid Enables UID SORT usage
1623 - * @param string $encoding Character set
1624 - *
1625 - * @return ResultIndex Response data
1626 - */
1627 - public function sort($mailbox, $field = 'ARRIVAL', $criteria = '', $return_uid = false, $encoding = 'US-ASCII')
1628 - {
1629 - $old_sel = $this->selected;
1630 - $supported = ['ARRIVAL', 'CC', 'DATE', 'FROM', 'SIZE', 'SUBJECT', 'TO'];
1631 - $field = strtoupper($field);
1632 -
1633 - if ($field == 'INTERNALDATE') {
1634 - $field = 'ARRIVAL';
1635 - }
1636 -
1637 - if (!in_array($field, $supported)) {
1638 - return new ResultIndex($mailbox);
1639 - }
1640 -
1641 - if (!$this->select($mailbox)) {
1642 - return new ResultIndex($mailbox);
1643 - }
1644 -
1645 - // return empty result when folder is empty and we're just after SELECT
1646 - if ($old_sel != $mailbox && !$this->data['EXISTS']) {
1647 - return new ResultIndex($mailbox, '* SORT');
1648 - }
1649 -
1650 - // RFC 5957: SORT=DISPLAY
1651 - if (($field == 'FROM' || $field == 'TO') && $this->getCapability('SORT=DISPLAY')) {
1652 - $field = 'DISPLAY'.$field;
1653 - }
1654 -
1655 - $encoding = $encoding ? trim($encoding) : 'US-ASCII';
1656 - $criteria = $criteria ? 'ALL '.trim($criteria) : 'ALL';
1657 -
1658 - [$code, $response] = $this->execute(
1659 - $return_uid ? 'UID SORT' : 'SORT',
1660 - ["($field)", $encoding, $criteria]
1661 - );
1662 -
1663 - if ($code != self::ERROR_OK) {
1664 - $response = null;
1665 - }
1666 -
1667 - return new ResultIndex($mailbox, $response);
1668 - }
1669 -
1670 - /**
1671 - * Executes THREAD command
1672 - *
1673 - * @param string $mailbox Mailbox name
1674 - * @param string $algorithm Threading algorithm (ORDEREDSUBJECT, REFERENCES, REFS)
1675 - * @param string $criteria Searching criteria
1676 - * @param bool $return_uid Enables UIDs in result instead of sequence numbers
1677 - * @param string $encoding Character set
1678 - *
1679 - * @return ResultThread Thread data
1680 - */
1681 - public function thread($mailbox, $algorithm = 'REFERENCES', $criteria = '', $return_uid = false, $encoding = 'US-ASCII')
1682 - {
1683 - $old_sel = $this->selected;
1684 -
1685 - if (!$this->select($mailbox)) {
1686 - return new ResultThread($mailbox);
1687 - }
1688 -
1689 - // return empty result when folder is empty and we're just after SELECT
1690 - if ($old_sel != $mailbox && !$this->data['EXISTS']) {
1691 - return new ResultThread($mailbox, '* THREAD');
1692 - }
1693 -
1694 - $encoding = $encoding ? trim($encoding) : 'US-ASCII';
1695 - $algorithm = $algorithm ? trim($algorithm) : 'REFERENCES';
1696 - $criteria = $criteria ? 'ALL '.trim($criteria) : 'ALL';
1697 -
1698 - [$code, $response] = $this->execute(
1699 - $return_uid ? 'UID THREAD' : 'THREAD',
1700 - [$algorithm, $encoding, $criteria]
1701 - );
1702 -
1703 - if ($code != self::ERROR_OK) {
1704 - $response = null;
1705 - }
1706 -
1707 - return new ResultThread($mailbox, $response);
1708 - }
1709 -
1710 - /**
1711 - * Executes SEARCH command
1712 - *
1713 - * @param string $mailbox Mailbox name
1714 - * @param string $criteria Searching criteria
1715 - * @param bool $return_uid Enable UID in result instead of sequence ID
1716 - * @param array $items Return items (MIN, MAX, COUNT, ALL)
1717 - *
1718 - * @return ResultIndex Result data
1719 - */
1720 - public function search($mailbox, $criteria, $return_uid = false, $items = [])
1721 - {
1722 - $old_sel = $this->selected;
1723 -
1724 - if (!$this->select($mailbox)) {
1725 - return new ResultIndex($mailbox);
1726 - }
1727 -
1728 - // return empty result when folder is empty and we're just after SELECT
1729 - if ($old_sel != $mailbox && !$this->data['EXISTS']) {
1730 - return new ResultIndex($mailbox, '* SEARCH');
1731 - }
1732 -
1733 - // If ESEARCH is supported always use ALL
1734 - // but not when items are specified or using simple id2uid search
1735 - if (empty($items) && preg_match('/[^0-9]/', $criteria)) {
1736 - $items = ['ALL'];
1737 - }
1738 -
1739 - $esearch = empty($items) ? false : $this->getCapability('ESEARCH');
1740 - $criteria = trim($criteria);
1741 - $params = '';
1742 -
1743 - // RFC4731: ESEARCH
1744 - if (!empty($items) && $esearch) {
1745 - $params .= 'RETURN ('.implode(' ', $items).')';
1746 - }
1747 -
1748 - if (!empty($criteria)) {
1749 - $params .= ($params ? ' ' : '').$criteria;
1750 - } else {
1751 - $params .= 'ALL';
1752 - }
1753 -
1754 - [$code, $response] = $this->execute(
1755 - $return_uid ? 'UID SEARCH' : 'SEARCH',
1756 - [$params]
1757 - );
1758 -
1759 - if ($code != self::ERROR_OK) {
1760 - $response = null;
1761 - }
1762 -
1763 - return new ResultIndex($mailbox, $response);
1764 - }
1765 -
1766 - /**
1767 - * Simulates SORT command by using FETCH and sorting.
1768 - *
1769 - * @param string $mailbox Mailbox name
1770 - * @param string|array $message_set Searching criteria (list of messages to return)
1771 - * @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
1772 - * @param bool $skip_deleted Makes that DELETED messages will be skipped
1773 - * @param bool $uidfetch Enables UID FETCH usage
1774 - * @param bool $return_uid Enables returning UIDs instead of IDs
1775 - *
1776 - * @return ResultIndex Response data
1777 - */
1778 - public function index(
1779 - $mailbox,
1780 - $message_set,
1781 - $index_field = '',
1782 - $skip_deleted = true,
1783 - $uidfetch = false,
1784 - $return_uid = false
1785 - ) {
1786 - $msg_index = $this->fetchHeaderIndex(
1787 - $mailbox,
1788 - $message_set,
1789 - $index_field,
1790 - $skip_deleted,
1791 - $uidfetch,
1792 - $return_uid
1793 - );
1794 -
1795 - if (!empty($msg_index)) {
1796 - asort($msg_index); // ASC
1797 - $msg_index = array_keys($msg_index);
1798 - $msg_index = '* SEARCH '.implode(' ', $msg_index);
1799 - } else {
1800 - $msg_index = is_array($msg_index) ? '* SEARCH' : null;
1801 - }
1802 -
1803 - return new ResultIndex($mailbox, $msg_index);
1804 - }
1805 -
1806 - /**
1807 - * Fetches specified header/data value for a set of messages.
1808 - *
1809 - * @param string $mailbox Mailbox name
1810 - * @param string|array $message_set Searching criteria (list of messages to return)
1811 - * @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
1812 - * @param bool $skip_deleted Makes that DELETED messages will be skipped
1813 - * @param bool $uidfetch Enables UID FETCH usage
1814 - * @param bool $return_uid Enables returning UIDs instead of IDs
1815 - *
1816 - * @return array|bool List of header values or False on failure
1817 - */
1818 - public function fetchHeaderIndex(
1819 - $mailbox,
1820 - $message_set,
1821 - $index_field = '',
1822 - $skip_deleted = true,
1823 - $uidfetch = false,
1824 - $return_uid = false
1825 - ) {
1826 - if (is_array($message_set)) {
1827 - if (!($message_set = $this->compressMessageSet($message_set))) {
1828 - return false;
1829 - }
1830 - } else {
1831 - [$from_idx, $to_idx] = explode(':', $message_set);
1832 - if (empty($message_set) ||
1833 - (isset($to_idx) && $to_idx != '*' && (int)$from_idx > (int)$to_idx)
1834 - ) {
1835 - return false;
1836 - }
1837 - }
1838 -
1839 - $index_field = empty($index_field) ? 'DATE' : strtoupper($index_field);
1840 -
1841 - $fields_a['DATE'] = 1;
1842 - $fields_a['INTERNALDATE'] = 4;
1843 - $fields_a['ARRIVAL'] = 4;
1844 - $fields_a['FROM'] = 1;
1845 - $fields_a['REPLY-TO'] = 1;
1846 - $fields_a['SENDER'] = 1;
1847 - $fields_a['TO'] = 1;
1848 - $fields_a['CC'] = 1;
1849 - $fields_a['SUBJECT'] = 1;
1850 - $fields_a['UID'] = 2;
1851 - $fields_a['SIZE'] = 2;
1852 - $fields_a['SEEN'] = 3;
1853 - $fields_a['RECENT'] = 3;
1854 - $fields_a['DELETED'] = 3;
1855 -
1856 - if (!($mode = $fields_a[$index_field])) {
1857 - return false;
1858 - }
1859 -
1860 - // Select the mailbox
1861 - if (!$this->select($mailbox)) {
1862 - return false;
1863 - }
1864 -
1865 - // build FETCH command string
1866 - $key = $this->nextTag();
1867 - $cmd = $uidfetch ? 'UID FETCH' : 'FETCH';
1868 - $fields = [];
1869 -
1870 - if ($return_uid) {
1871 - $fields[] = 'UID';
1872 - }
1873 - if ($skip_deleted) {
1874 - $fields[] = 'FLAGS';
1875 - }
1876 -
1877 - if ($mode == 1) {
1878 - if ($index_field == 'DATE') {
1879 - $fields[] = 'INTERNALDATE';
1880 - }
1881 - $fields[] = "BODY.PEEK[HEADER.FIELDS ($index_field)]";
1882 - } elseif ($mode == 2) {
1883 - if ($index_field == 'SIZE') {
1884 - $fields[] = 'RFC822.SIZE';
1885 - } elseif (!$return_uid || $index_field != 'UID') {
1886 - $fields[] = $index_field;
1887 - }
1888 - } elseif ($mode == 3 && !$skip_deleted) {
1889 - $fields[] = 'FLAGS';
1890 - } elseif ($mode == 4) {
1891 - $fields[] = 'INTERNALDATE';
1892 - }
1893 -
1894 - $request = "$key $cmd $message_set (".implode(' ', $fields).")";
1895 -
1896 - if (!$this->putLine($request)) {
1897 - $this->setError(self::ERROR_COMMAND, "Failed to send $cmd command");
1898 -
1899 - return false;
1900 - }
1901 -
1902 - $result = [];
1903 -
1904 - do {
1905 - $line = rtrim($this->readLine(200));
1906 - $line = $this->multLine($line);
1907 -
1908 - if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
1909 - $id = $m[1];
1910 - $flags = null;
1911 -
1912 - if ($return_uid) {
1913 - if (preg_match('/UID ([0-9]+)/', $line, $matches)) {
1914 - $id = (int)$matches[1];
1915 - } else {
1916 - continue;
1917 - }
1918 - }
1919 - if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
1920 - $flags = explode(' ', strtoupper($matches[1]));
1921 - if (in_array('\\DELETED', $flags)) {
1922 - continue;
1923 - }
1924 - }
1925 -
1926 - if ($mode == 1 && $index_field == 'DATE') {
1927 - if (preg_match('/BODY\[HEADER\.FIELDS \("*DATE"*\)\] (.*)/', $line, $matches)) {
1928 - $value = preg_replace(['/^"*[a-z]+:/i'], '', $matches[1]);
1929 - $value = trim($value);
1930 - $result[$id] = Utils::strtotime($value);
1931 - }
1932 - // non-existent/empty Date: header, use INTERNALDATE
1933 - if (empty($result[$id])) {
1934 - if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) {
1935 - $result[$id] = Utils::strtotime($matches[1]);
1936 - } else {
1937 - $result[$id] = 0;
1938 - }
1939 - }
1940 - } elseif ($mode == 1) {
1941 - if (preg_match('/BODY\[HEADER\.FIELDS \("?(FROM|REPLY-TO|SENDER|TO|SUBJECT)"?\)\] (.*)/', $line, $matches)) {
1942 - $value = preg_replace(['/^"*[a-z]+:/i', '/\s+$/sm'], ['', ''], $matches[2]);
1943 - $result[$id] = trim($value);
1944 - } else {
1945 - $result[$id] = '';
1946 - }
1947 - } elseif ($mode == 2) {
1948 - if (preg_match('/'.$index_field.' ([0-9]+)/', $line, $matches)) {
1949 - $result[$id] = trim($matches[1]);
1950 - } else {
1951 - $result[$id] = 0;
1952 - }
1953 - } elseif ($mode == 3) {
1954 - if (!$flags && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
1955 - $flags = explode(' ', $matches[1]);
1956 - }
1957 - $result[$id] = in_array("\\".$index_field, (array)$flags) ? 1 : 0;
1958 - } elseif ($mode == 4) {
1959 - if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) {
1960 - $result[$id] = Utils::strtotime($matches[1]);
1961 - } else {
1962 - $result[$id] = 0;
1963 - }
1964 - }
1965 - }
1966 - } while (!$this->startsWith($line, $key, true, true));
1967 -
1968 - return $result;
1969 - }
1970 -
1971 - /**
1972 - * Returns message sequence identifier
1973 - *
1974 - * @param string $mailbox Mailbox name
1975 - * @param int $uid Message unique identifier (UID)
1976 - *
1977 - * @return int Message sequence identifier
1978 - */
1979 - public function UID2ID($mailbox, $uid)
1980 - {
1981 - if ($uid > 0) {
1982 - $index = $this->search($mailbox, "UID $uid");
1983 -
1984 - if ($index->count() == 1) {
1985 - $arr = $index->get();
1986 -
1987 - return (int)$arr[0];
1988 - }
1989 - }
1990 - }
1991 -
1992 - /**
1993 - * Returns message unique identifier (UID)
1994 - *
1995 - * @param string $mailbox Mailbox name
1996 - * @param int $uid Message sequence identifier
1997 - *
1998 - * @return int Message unique identifier
1999 - */
2000 - public function ID2UID($mailbox, $id)
2001 - {
2002 - if (empty($id) || $id < 0) {
2003 - return null;
2004 - }
2005 -
2006 - if (!$this->select($mailbox)) {
2007 - return null;
2008 - }
2009 -
2010 - if ($uid = $this->data['UID-MAP'][$id]) {
2011 - return $uid;
2012 - }
2013 -
2014 - if (isset($this->data['EXISTS']) && $id > $this->data['EXISTS']) {
2015 - return null;
2016 - }
2017 -
2018 - $index = $this->search($mailbox, $id, true);
2019 -
2020 - if ($index->count() == 1) {
2021 - $arr = $index->get();
2022 -
2023 - return $this->data['UID-MAP'][$id] = (int)$arr[0];
2024 - }
2025 - }
2026 -
2027 - /**
2028 - * Sets flag of the message(s)
2029 - *
2030 - * @param string $mailbox Mailbox name
2031 - * @param string|array $messages Message UID(s)
2032 - * @param string $flag Flag name
2033 - *
2034 - * @return bool True on success, False on failure
2035 - */
2036 - public function flag($mailbox, $messages, $flag)
2037 - {
2038 - return $this->modFlag($mailbox, $messages, $flag, '+');
2039 - }
2040 -
2041 - /**
2042 - * Unsets flag of the message(s)
2043 - *
2044 - * @param string $mailbox Mailbox name
2045 - * @param string|array $messages Message UID(s)
2046 - * @param string $flag Flag name
2047 - *
2048 - * @return bool True on success, False on failure
2049 - */
2050 - public function unflag($mailbox, $messages, $flag)
2051 - {
2052 - return $this->modFlag($mailbox, $messages, $flag, '-');
2053 - }
2054 -
2055 - /**
2056 - * Changes flag of the message(s)
2057 - *
2058 - * @param string $mailbox Mailbox name
2059 - * @param string|array $messages Message UID(s)
2060 - * @param string $flag Flag name
2061 - * @param string $mod Modifier [+|-]. Default: "+".
2062 - *
2063 - * @return bool True on success, False on failure
2064 - */
2065 - protected function modFlag($mailbox, $messages, $flag, $mod = '+')
2066 - {
2067 - if (!$flag) {
2068 - return false;
2069 - }
2070 -
2071 - if (!$this->select($mailbox)) {
2072 - return false;
2073 - }
2074 -
2075 - if (!$this->data['READ-WRITE']) {
2076 - $this->setError(self::ERROR_READONLY, "Mailbox is read-only");
2077 -
2078 - return false;
2079 - }
2080 -
2081 - if (isset($this->flags[strtoupper($flag)]) && $this->flags[strtoupper($flag)]) {
2082 - $flag = $this->flags[strtoupper($flag)];
2083 - }
2084 -
2085 - // if PERMANENTFLAGS is not specified all flags are allowed
2086 - if (!empty($this->data['PERMANENTFLAGS'])
2087 - && !in_array($flag, (array)$this->data['PERMANENTFLAGS'])
2088 - && !in_array('\\*', (array)$this->data['PERMANENTFLAGS'])
2089 - ) {
2090 - return false;
2091 - }
2092 -
2093 - // Clear internal status cache
2094 - if ($flag == 'SEEN') {
2095 - unset($this->data['STATUS:'.$mailbox]['UNSEEN']);
2096 - }
2097 -
2098 - if ($mod != '+' && $mod != '-') {
2099 - $mod = '+';
2100 - }
2101 -
2102 - $result = $this->execute(
2103 - 'UID STORE',
2104 - [
2105 - $this->compressMessageSet($messages),
2106 - $mod.'FLAGS.SILENT',
2107 - "($flag)",
2108 - ],
2109 - self::COMMAND_NORESPONSE
2110 - );
2111 -
2112 - return $result == self::ERROR_OK;
2113 - }
2114 -
2115 - /**
2116 - * Copies message(s) from one folder to another
2117 - *
2118 - * @param string|array $messages Message UID(s)
2119 - * @param string $from Mailbox name
2120 - * @param string $to Destination mailbox name
2121 - *
2122 - * @return bool True on success, False on failure
2123 - */
2124 - public function copy($messages, $from, $to)
2125 - {
2126 - // Clear last COPYUID data
2127 - unset($this->data['COPYUID']);
2128 -
2129 - if (!$this->select($from)) {
2130 - return false;
2131 - }
2132 -
2133 - // Clear internal status cache
2134 - unset($this->data['STATUS:'.$to]);
2135 -
2136 - $result = $this->execute(
2137 - 'UID COPY',
2138 - [
2139 - $this->compressMessageSet($messages),
2140 - $this->escape($to),
2141 - ],
2142 - self::COMMAND_NORESPONSE
2143 - );
2144 -
2145 - return $result == self::ERROR_OK;
2146 - }
2147 -
2148 - /**
2149 - * Moves message(s) from one folder to another.
2150 - *
2151 - * @param string|array $messages Message UID(s)
2152 - * @param string $from Mailbox name
2153 - * @param string $to Destination mailbox name
2154 - *
2155 - * @return bool True on success, False on failure
2156 - */
2157 - public function move($messages, $from, $to)
2158 - {
2159 - if (!$this->select($from)) {
2160 - return false;
2161 - }
2162 -
2163 - if (!$this->data['READ-WRITE']) {
2164 - $this->setError(self::ERROR_READONLY, "Mailbox is read-only");
2165 -
2166 - return false;
2167 - }
2168 -
2169 - // use MOVE command (RFC 6851)
2170 - if ($this->hasCapability('MOVE')) {
2171 - // Clear last COPYUID data
2172 - unset($this->data['COPYUID']);
2173 -
2174 - // Clear internal status cache
2175 - unset($this->data['STATUS:'.$to]);
2176 - $this->clear_status_cache($from);
2177 -
2178 - $result = $this->execute(
2179 - 'UID MOVE',
2180 - [
2181 - $this->compressMessageSet($messages),
2182 - $this->escape($to),
2183 - ],
2184 - self::COMMAND_NORESPONSE
2185 - );
2186 -
2187 - return $result == self::ERROR_OK;
2188 - }
2189 -
2190 - // use COPY + STORE +FLAGS.SILENT \Deleted + EXPUNGE
2191 - $result = $this->copy($messages, $from, $to);
2192 -
2193 - if ($result) {
2194 - // Clear internal status cache
2195 - unset($this->data['STATUS:'.$from]);
2196 -
2197 - $result = $this->flag($from, $messages, 'DELETED');
2198 -
2199 - if ($messages == '*') {
2200 - // CLOSE+SELECT should be faster than EXPUNGE
2201 - $this->close();
2202 - } else {
2203 - $this->expunge($from, $messages);
2204 - }
2205 - }
2206 -
2207 - return $result;
2208 - }
2209 -
2210 - /**
2211 - * FETCH command (RFC3501)
2212 - *
2213 - * @param string $mailbox Mailbox name
2214 - * @param mixed $message_set Message(s) sequence identifier(s) or UID(s)
2215 - * @param bool $is_uid True if $message_set contains UIDs
2216 - * @param array $query_items FETCH command data items
2217 - * @param string $mod_seq Modification sequence for CHANGEDSINCE (RFC4551) query
2218 - * @param bool $vanished Enables VANISHED parameter (RFC5162) for CHANGEDSINCE query
2219 - *
2220 - * @return array List of MessageHeader elements, False on error
2221 - * @since 0.6
2222 - */
2223 - public function fetch(
2224 - $mailbox,
2225 - $message_set,
2226 - $is_uid = false,
2227 - $query_items = [],
2228 - $mod_seq = null,
2229 - $vanished = false
2230 - ) {
2231 - if (!$this->select($mailbox)) {
2232 - return false;
2233 - }
2234 -
2235 - $message_set = $this->compressMessageSet($message_set);
2236 - $result = [];
2237 -
2238 - $key = $this->nextTag();
2239 - $cmd = ($is_uid ? 'UID ' : '').'FETCH';
2240 - $request = "$key $cmd $message_set (".implode(' ', $query_items).")";
2241 -
2242 - if ($mod_seq !== null && $this->hasCapability('CONDSTORE')) {
2243 - $request .= " (CHANGEDSINCE $mod_seq".($vanished ? " VANISHED" : '').")";
2244 - }
2245 -
2246 - if (!$this->putLine($request)) {
2247 - $this->setError(self::ERROR_COMMAND, "Failed to send $cmd command");
2248 -
2249 - return false;
2250 - }
2251 -
2252 - do {
2253 - $line = $this->readFullLine(4096);
2254 -
2255 - if (!$line) {
2256 - break;
2257 - }
2258 -
2259 - // Sample reply line:
2260 - // * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen)
2261 - // INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...)
2262 - // BODY[HEADER.FIELDS ...
2263 -
2264 - if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
2265 - $id = intval($m[1]);
2266 -
2267 - $result[$id] = new MessageHeader;
2268 - $result[$id]->id = $id;
2269 - $result[$id]->subject = '';
2270 - $result[$id]->messageID = 'mid:'.$id;
2271 -
2272 - $headers = null;
2273 - $lines = [];
2274 - $line = substr($line, strlen($m[0]) + 2);
2275 - $ln = 0;
2276 -
2277 - // Tokenize response and assign to object properties
2278 - while (@list($name, $value) = $this->tokenizeResponse($line, 2)) {
2279 - if ($name == 'UID') {
2280 - $result[$id]->uid = intval($value);
2281 - } elseif ($name == 'RFC822.SIZE') {
2282 - $result[$id]->size = intval($value);
2283 - } elseif ($name == 'RFC822.TEXT') {
2284 - $result[$id]->body = $value;
2285 - } elseif ($name == 'INTERNALDATE') {
2286 - $result[$id]->internaldate = $value;
2287 - $result[$id]->date = $value;
2288 - $result[$id]->timestamp = Utils::strtotime($value);
2289 - } elseif ($name == 'FLAGS') {
2290 - if (!empty($value)) {
2291 - foreach ((array)$value as $flag) {
2292 - $flag = str_replace(['$', "\\"], '', $flag);
2293 - $flag = strtoupper($flag);
2294 -
2295 - $result[$id]->flags[$flag] = true;
2296 - }
2297 - }
2298 - } elseif ($name == 'MODSEQ') {
2299 - $result[$id]->modseq = $value[0];
2300 - } elseif ($name == 'ENVELOPE') {
2301 - $result[$id]->envelope = $value;
2302 - } elseif ($name == 'BODYSTRUCTURE' || ($name == 'BODY' && count($value) > 2)) {
2303 - if (!is_array($value[0]) && (strtolower($value[0]) == 'message' && strtolower($value[1]) == 'rfc822')) {
2304 - $value = [$value];
2305 - }
2306 - $result[$id]->bodystructure = $value;
2307 - } elseif ($name == 'RFC822') {
2308 - $result[$id]->body = $value;
2309 - } elseif (stripos($name, 'BODY[') === 0) {
2310 - $name = str_replace(']', '', substr($name, 5));
2311 -
2312 - if ($name == 'HEADER.FIELDS') {
2313 - // skip ']' after headers list
2314 - $this->tokenizeResponse($line, 1);
2315 - $headers = $this->tokenizeResponse($line, 1);
2316 - } elseif (strlen($name)) {
2317 - $result[$id]->bodypart[$name] = $value;
2318 - } else {
2319 - $result[$id]->body = $value;
2320 - }
2321 - }
2322 - }
2323 -
2324 - // create array with header field:data
2325 - if (!empty($headers)) {
2326 - $headers = explode("\n", trim($headers));
2327 - foreach ($headers as $resln) {
2328 - if (ord($resln[0]) <= 32) {
2329 - $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n").trim($resln);
2330 - } else {
2331 - $lines[++$ln] = trim($resln);
2332 - }
2333 - }
2334 -
2335 - foreach ($lines as $str) {
2336 - [$field, $string] = explode(':', $str, 2);
2337 -
2338 - $field = strtolower($field);
2339 - $string = preg_replace('/\n[\t\s]*/', ' ', trim($string));
2340 -
2341 - switch ($field) {
2342 - case 'date';
2343 - $string = substr($string, 0, 128);
2344 - $result[$id]->date = $string;
2345 - $result[$id]->timestamp = Utils::strtotime($string);
2346 - break;
2347 - case 'to':
2348 - $result[$id]->to = preg_replace('/undisclosed-recipients:[;,]*/', '', $string);
2349 - break;
2350 - case 'from':
2351 - case 'subject':
2352 - $string = substr($string, 0, 2048);
2353 - case 'cc':
2354 - case 'bcc':
2355 - case 'references':
2356 - $result[$id]->{$field} = $string;
2357 - break;
2358 - case 'reply-to':
2359 - $result[$id]->replyto = $string;
2360 - break;
2361 - case 'content-transfer-encoding':
2362 - $result[$id]->encoding = substr($string, 0, 32);
2363 - break;
2364 - case 'content-type':
2365 - $ctype_parts = preg_split('/[; ]+/', $string);
2366 - $result[$id]->ctype = strtolower(array_shift($ctype_parts));
2367 - if (preg_match('/charset\s*=\s*"?([a-z0-9\-\.\_]+)"?/i', $string, $regs)) {
2368 - $result[$id]->charset = $regs[1];
2369 - }
2370 - break;
2371 - case 'in-reply-to':
2372 - $result[$id]->in_reply_to = str_replace(["\n", '<', '>'], '', $string);
2373 - break;
2374 - case 'return-receipt-to':
2375 - case 'disposition-notification-to':
2376 - case 'x-confirm-reading-to':
2377 - $result[$id]->mdn_to = substr($string, 0, 2048);
2378 - break;
2379 - case 'message-id':
2380 - $result[$id]->messageID = substr($string, 0, 2048);
2381 - break;
2382 - case 'x-priority':
2383 - if (preg_match('/^(\d+)/', $string, $matches)) {
2384 - $result[$id]->priority = intval($matches[1]);
2385 - }
2386 - break;
2387 - default:
2388 - if (strlen($field) < 3) {
2389 - break;
2390 - }
2391 - if ($result[$id]->others[$field]) {
2392 - $string = array_merge((array)$result[$id]->others[$field], (array)$string);
2393 - }
2394 - $result[$id]->others[$field] = $string;
2395 - }
2396 - }
2397 - }
2398 - }
2399 - // VANISHED response (QRESYNC RFC5162)
2400 - // Sample: * VANISHED (EARLIER) 300:310,405,411
2401 - elseif (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
2402 - $line = substr($line, strlen($match[0]));
2403 - $v_data = $this->tokenizeResponse($line, 1);
2404 -
2405 - $this->data['VANISHED'] = $v_data;
2406 - }
2407 - } while (!$this->startsWith($line, $key, true));
2408 -
2409 - return $result;
2410 - }
2411 -
2412 - /**
2413 - * Returns message(s) data (flags, headers, etc.)
2414 - *
2415 - * @param string $mailbox Mailbox name
2416 - * @param mixed $message_set Message(s) sequence identifier(s) or UID(s)
2417 - * @param bool $is_uid True if $message_set contains UIDs
2418 - * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result
2419 - * @param array $add_headers List of additional headers
2420 - *
2421 - * @return bool|array List of MessageHeader elements, False on error
2422 - */
2423 - public function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add_headers = [])
2424 - {
2425 - $query_items = ['UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE'];
2426 - $headers = [
2427 - 'DATE',
2428 - 'FROM',
2429 - 'TO',
2430 - 'SUBJECT',
2431 - 'CONTENT-TYPE',
2432 - 'CC',
2433 - 'REPLY-TO',
2434 - 'LIST-POST',
2435 - 'DISPOSITION-NOTIFICATION-TO',
2436 - 'X-PRIORITY',
2437 - ];
2438 -
2439 - if (!empty($add_headers)) {
2440 - $add_headers = array_map('strtoupper', $add_headers);
2441 - $headers = array_unique(array_merge($headers, $add_headers));
2442 - }
2443 -
2444 - if ($bodystr) {
2445 - $query_items[] = 'BODYSTRUCTURE';
2446 - }
2447 -
2448 - $query_items[] = 'BODY.PEEK[HEADER.FIELDS ('.implode(' ', $headers).')]';
2449 -
2450 - return $this->fetch($mailbox, $message_set, $is_uid, $query_items);
2451 - }
2452 -
2453 - /**
2454 - * Returns message data (flags, headers, etc.)
2455 - *
2456 - * @param string $mailbox Mailbox name
2457 - * @param int $id Message sequence identifier or UID
2458 - * @param bool $is_uid True if $id is an UID
2459 - * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result
2460 - * @param array $add_headers List of additional headers
2461 - *
2462 - * @return bool|MessageHeader Message data, False on error
2463 - */
2464 - public function fetchHeader($mailbox, $id, $is_uid = false, $bodystr = false, $add_headers = [])
2465 - {
2466 - $a = $this->fetchHeaders($mailbox, $id, $is_uid, $bodystr, $add_headers);
2467 - if (is_array($a)) {
2468 - return array_shift($a);
2469 - }
2470 -
2471 - return false;
2472 - }
2473 -
2474 - /**
2475 - * Sort messages by specified header field
2476 - *
2477 - * @param array $messages Array of MessageHeader objects
2478 - * @param string $field Name of the property to sort by
2479 - * @param string $flag Sorting order (ASC|DESC)
2480 - *
2481 - * @return array Sorted input array
2482 - */
2483 - public static function sortHeaders($messages, $field, $flag)
2484 - {
2485 - $field = empty($field) ? 'uid' : strtolower($field);
2486 - $order = empty($flag) ? 'ASC' : strtoupper($flag);
2487 - $index = [];
2488 -
2489 - reset($messages);
2490 -
2491 - // Create an index
2492 - foreach ($messages as $key => $headers) {
2493 - switch ($field) {
2494 - case 'arrival':
2495 - $field = 'internaldate';
2496 - // no-break
2497 - case 'date':
2498 - case 'internaldate':
2499 - case 'timestamp':
2500 - $value = Utils::strtotime($headers->$field);
2501 - if (!$value && $field != 'timestamp') {
2502 - $value = $headers->timestamp;
2503 - }
2504 -
2505 - break;
2506 -
2507 - default:
2508 - // @TODO: decode header value, convert to UTF-8
2509 - $value = $headers->$field;
2510 - if (is_string($value)) {
2511 - $value = str_replace('"', '', $value);
2512 -
2513 - if ($field == 'subject') {
2514 - $value = preg_replace('/^(Re:\s*|Fwd:\s*|Fw:\s*)+/i', '', $value);
2515 - }
2516 - }
2517 - }
2518 -
2519 - $index[$key] = $value;
2520 - }
2521 -
2522 - $sort_order = $flag == 'ASC' ? SORT_ASC : SORT_DESC;
2523 - $sort_flags = SORT_STRING | SORT_FLAG_CASE;
2524 -
2525 - if (in_array($field, ['arrival', 'date', 'internaldate', 'timestamp'])) {
2526 - $sort_flags = SORT_NUMERIC;
2527 - }
2528 -
2529 - array_multisort($index, $sort_order, $sort_flags, $messages);
2530 -
2531 - return $messages;
2532 - }
2533 -
2534 - /**
2535 - * Fetch MIME headers of specified message parts
2536 - *
2537 - * @param string $mailbox Mailbox name
2538 - * @param int $uid Message UID
2539 - * @param array $parts Message part identifiers
2540 - * @param bool $mime Use MIME instad of HEADER
2541 - *
2542 - * @return array|bool Array containing headers string for each specified body
2543 - * False on failure.
2544 - */
2545 - public function fetchMIMEHeaders($mailbox, $uid, $parts, $mime = true)
2546 - {
2547 - if (!$this->select($mailbox)) {
2548 - return false;
2549 - }
2550 -
2551 - $result = false;
2552 - $parts = (array)$parts;
2553 - $key = $this->nextTag();
2554 - $peeks = [];
2555 - $type = $mime ? 'MIME' : 'HEADER';
2556 -
2557 - // format request
2558 - foreach ($parts as $part) {
2559 - $peeks[] = "BODY.PEEK[$part.$type]";
2560 - }
2561 -
2562 - $request = "$key UID FETCH $uid (".implode(' ', $peeks).')';
2563 -
2564 - // send request
2565 - if (!$this->putLine($request)) {
2566 - $this->setError(self::ERROR_COMMAND, "Failed to send UID FETCH command");
2567 -
2568 - return false;
2569 - }
2570 -
2571 - do {
2572 - $line = $this->readLine(1024);
2573 -
2574 - if (preg_match('/^\* [0-9]+ FETCH [0-9UID( ]+/', $line, $m)) {
2575 - $line = ltrim(substr($line, strlen($m[0])));
2576 - while (preg_match('/^BODY\[([0-9\.]+)\.'.$type.'\]/', $line, $matches)) {
2577 - $line = substr($line, strlen($matches[0]));
2578 - $result[$matches[1]] = trim($this->multLine($line));
2579 - $line = $this->readLine(1024);
2580 - }
2581 - }
2582 - } while (!$this->startsWith($line, $key, true));
2583 -
2584 - return $result;
2585 - }
2586 -
2587 - /**
2588 - * Fetches message part header
2589 - */
2590 - public function fetchPartHeader($mailbox, $id, $is_uid = false, $part = null)
2591 - {
2592 - $part = empty($part) ? 'HEADER' : $part.'.MIME';
2593 -
2594 - return $this->handlePartBody($mailbox, $id, $is_uid, $part);
2595 - }
2596 -
2597 - /**
2598 - * Fetches body of the specified message part
2599 - */
2600 - public function handlePartBody($mailbox, $id, $is_uid = false, $part = '', $encoding = null, $print = null, $file = null, $formatted = false, $max_bytes = 0)
2601 - {
2602 - if (!$this->select($mailbox)) {
2603 - return false;
2604 - }
2605 -
2606 - $binary = true;
2607 -
2608 - do {
2609 - if (!$initiated) {
2610 - switch ($encoding) {
2611 - case 'base64':
2612 - $mode = 1;
2613 - break;
2614 - case 'quoted-printable':
2615 - $mode = 2;
2616 - break;
2617 - case 'x-uuencode':
2618 - case 'x-uue':
2619 - case 'uue':
2620 - case 'uuencode':
2621 - $mode = 3;
2622 - break;
2623 - default:
2624 - $mode = 0;
2625 - }
2626 -
2627 - // Use BINARY extension when possible (and safe)
2628 - $binary = $binary && $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY');
2629 - $fetch_mode = $binary ? 'BINARY' : 'BODY';
2630 - $partial = $max_bytes ? sprintf('<0.%d>', $max_bytes) : '';
2631 -
2632 - // format request
2633 - $key = $this->nextTag();
2634 - $cmd = ($is_uid ? 'UID ' : '').'FETCH';
2635 - $request = "$key $cmd $id ($fetch_mode.PEEK[$part]$partial)";
2636 - $result = false;
2637 - $found = false;
2638 - $initiated = true;
2639 -
2640 - // send request
2641 - if (!$this->putLine($request)) {
2642 - $this->setError(self::ERROR_COMMAND, "Failed to send $cmd command");
2643 -
2644 - return false;
2645 - }
2646 -
2647 - if ($binary) {
2648 - // WARNING: Use $formatted argument with care, this may break binary data stream
2649 - $mode = -1;
2650 - }
2651 - }
2652 -
2653 - $line = trim($this->readLine(1024));
2654 -
2655 - if (!$line) {
2656 - break;
2657 - }
2658 -
2659 - // handle UNKNOWN-CTE response - RFC 3516, try again with standard BODY request
2660 - if ($binary && !$found && preg_match('/^'.$key.' NO \[(UNKNOWN-CTE|PARSE)\]/i', $line)) {
2661 - $binary = $initiated = false;
2662 - continue;
2663 - }
2664 -
2665 - // skip irrelevant untagged responses (we have a result already)
2666 - if ($found || !preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) {
2667 - continue;
2668 - }
2669 -
2670 - $line = $m[2];
2671 -
2672 - // handle one line response
2673 - if ($line[0] == '(' && substr($line, -1) == ')') {
2674 - // tokenize content inside brackets
2675 - // the content can be e.g.: (UID 9844 BODY[2.4] NIL)
2676 - $preg_replace = preg_replace('/(^\(|\)$)/', '', $line);
2677 - $tokens = $this->tokenizeResponse($preg_replace);
2678 -
2679 - for ($i = 0 ; $i < count($tokens) ; $i += 2) {
2680 - if (preg_match('/^(BODY|BINARY)/i', $tokens[$i])) {
2681 - $result = $tokens[$i + 1];
2682 - $found = true;
2683 - break;
2684 - }
2685 - }
2686 -
2687 - if ($result !== false) {
2688 - if ($mode == 1) {
2689 - $result = base64_decode($result);
2690 - } elseif ($mode == 2) {
2691 - $result = quoted_printable_decode($result);
2692 - } elseif ($mode == 3) {
2693 - $result = convert_uudecode($result);
2694 - }
2695 - }
2696 - } // response with string literal
2697 - elseif (preg_match('/\{([0-9]+)\}$/', $line, $m)) {
2698 - $bytes = (int)$m[1];
2699 - $prev = '';
2700 - $found = true;
2701 -
2702 - // empty body
2703 - if (!$bytes) {
2704 - $result = '';
2705 - } else {
2706 - while ($bytes > 0) {
2707 - $line = $this->readLine(8192);
2708 -
2709 - if ($line === null) {
2710 - break;
2711 - }
2712 -
2713 - $len = strlen($line);
2714 -
2715 - if ($len > $bytes) {
2716 - $line = substr($line, 0, $bytes);
2717 - $len = strlen($line);
2718 - }
2719 - $bytes -= $len;
2720 -
2721 - // BASE64
2722 - if ($mode == 1) {
2723 - $line = preg_replace('|[^a-zA-Z0-9+=/]|', '', $line);
2724 - // create chunks with proper length for base64 decoding
2725 - $line = $prev.$line;
2726 - $length = strlen($line);
2727 - if ($length % 4) {
2728 - $length = floor($length / 4) * 4;
2729 - $prev = substr($line, $length);
2730 - $line = substr($line, 0, $length);
2731 - } else {
2732 - $prev = '';
2733 - }
2734 - $line = base64_decode($line);
2735 - } // QUOTED-PRINTABLE
2736 - elseif ($mode == 2) {
2737 - $line = rtrim($line, "\t\r\0\x0B");
2738 - $line = quoted_printable_decode($line);
2739 - } // UUENCODE
2740 - elseif ($mode == 3) {
2741 - $line = rtrim($line, "\t\r\n\0\x0B");
2742 - if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line)) {
2743 - continue;
2744 - }
2745 - $line = convert_uudecode($line);
2746 - } // default
2747 - elseif ($formatted) {
2748 - $line = rtrim($line, "\t\r\n\0\x0B")."\n";
2749 - }
2750 -
2751 - if ($file) {
2752 - if (fwrite($file, $line) === false) {
2753 - break;
2754 - }
2755 - } elseif ($print) {
2756 - echo $line;
2757 - } else {
2758 - $result .= $line;
2759 - }
2760 - }
2761 - }
2762 - }
2763 - } while (!$this->startsWith($line, $key, true) || !$initiated);
2764 -
2765 - if ($result !== false) {
2766 - if ($file) {
2767 - return fwrite($file, $result);
2768 - } elseif ($print) {
2769 - echo $result;
2770 -
2771 - return true;
2772 - }
2773 -
2774 - return $result;
2775 - }
2776 -
2777 - return false;
2778 - }
2779 -
2780 - /**
2781 - * Handler for IMAP APPEND command
2782 - *
2783 - * @param string $mailbox Mailbox name
2784 - * @param string|array $message The message source string or array (of strings and file pointers)
2785 - * @param array $flags Message flags
2786 - * @param string $date Message internal date
2787 - * @param bool $binary Enable BINARY append (RFC3516)
2788 - *
2789 - * @return string|bool On success APPENDUID response (if available) or True, False on failure
2790 - */
2791 - public function append($mailbox, &$message, $flags = [], $date = null, $binary = false)
2792 - {
2793 - unset($this->data['APPENDUID']);
2794 -
2795 - if ($mailbox === null || $mailbox === '') {
2796 - return false;
2797 - }
2798 -
2799 - $binary = $binary && $this->getCapability('BINARY');
2800 - $literal_plus = !$binary && isset($this->prefs['literal+']) && $this->prefs['literal+'];
2801 - $len = 0;
2802 - $msg = is_array($message) ? $message : [&$message];
2803 - $chunk_size = 512000;
2804 -
2805 - for ($i = 0, $cnt = count($msg) ; $i < $cnt ; $i++) {
2806 - if (is_resource($msg[$i])) {
2807 - $stat = fstat($msg[$i]);
2808 - if ($stat === false) {
2809 - return false;
2810 - }
2811 - $len += $stat['size'];
2812 - } else {
2813 - if (!$binary) {
2814 - $msg[$i] = str_replace("\r", '', $msg[$i]);
2815 - $msg[$i] = str_replace("\n", "\r\n", $msg[$i]);
2816 - }
2817 -
2818 - $len += strlen($msg[$i]);
2819 - }
2820 - }
2821 -
2822 - if (!$len) {
2823 - return false;
2824 - }
2825 -
2826 - // build APPEND command
2827 - $key = $this->nextTag();
2828 - $request = "$key APPEND ".$this->escape($mailbox).' ('.$this->flagsToStr($flags).')';
2829 - if (!empty($date)) {
2830 - $request .= ' '.$this->escape($date);
2831 - }
2832 - $request .= ' '.($binary ? '~' : '').'{'.$len.($literal_plus ? '+' : '').'}';
2833 -
2834 - // send APPEND command
2835 - if (!$this->putLine($request)) {
2836 - $this->setError(self::ERROR_COMMAND, "Failed to send APPEND command");
2837 -
2838 - return false;
2839 - }
2840 -
2841 - // Do not wait when LITERAL+ is supported
2842 - if (!$literal_plus) {
2843 - $line = $this->readReply();
2844 -
2845 - if ($line[0] != '+') {
2846 - $this->parseResult($line, 'APPEND: ');
2847 -
2848 - return false;
2849 - }
2850 - }
2851 -
2852 - foreach ($msg as $msg_part) {
2853 - // file pointer
2854 - if (is_resource($msg_part)) {
2855 - rewind($msg_part);
2856 - while (!feof($msg_part) && $this->fp) {
2857 - $buffer = fread($msg_part, $chunk_size);
2858 - $this->putLine($buffer, false);
2859 - }
2860 - fclose($msg_part);
2861 - } // string
2862 - else {
2863 - $size = strlen($msg_part);
2864 -
2865 - // Break up the data by sending one chunk (up to 512k) at a time.
2866 - // This approach reduces our peak memory usage
2867 - for ($offset = 0 ; $offset < $size ; $offset += $chunk_size) {
2868 - $chunk = substr($msg_part, $offset, $chunk_size);
2869 - if (!$this->putLine($chunk, false)) {
2870 - return false;
2871 - }
2872 - }
2873 - }
2874 - }
2875 -
2876 - if (!$this->putLine('')) { // \r\n
2877 - return false;
2878 - }
2879 -
2880 - do {
2881 - $line = $this->readLine();
2882 - } while (!$this->startsWith($line, $key, true, true));
2883 -
2884 - // Clear internal status cache
2885 - unset($this->data['STATUS:'.$mailbox]);
2886 -
2887 - if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK) {
2888 - return false;
2889 - }
2890 -
2891 - if (!empty($this->data['APPENDUID'])) {
2892 - return $this->data['APPENDUID'];
2893 - }
2894 -
2895 - return true;
2896 - }
2897 -
2898 - /**
2899 - * Handler for IMAP APPEND command.
2900 - *
2901 - * @param string $mailbox Mailbox name
2902 - * @param string $path Path to the file with message body
2903 - * @param string $headers Message headers
2904 - * @param array $flags Message flags
2905 - * @param string $date Message internal date
2906 - * @param bool $binary Enable BINARY append (RFC3516)
2907 - *
2908 - * @return string|bool On success APPENDUID response (if available) or True, False on failure
2909 - */
2910 - public function appendFromFile($mailbox, $path, $headers = null, $flags = [], $date = null, $binary = false)
2911 - {
2912 - // open message file
2913 - if (file_exists(realpath($path))) {
2914 - $fp = fopen($path, 'r');
2915 - }
2916 -
2917 - if (!$fp) {
2918 - $this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading");
2919 -
2920 - return false;
2921 - }
2922 -
2923 - $message = [];
2924 - if ($headers) {
2925 - $message[] = trim($headers, "\r\n")."\r\n\r\n";
2926 - }
2927 - $message[] = $fp;
2928 -
2929 - return $this->append($mailbox, $message, $flags, $date, $binary);
2930 - }
2931 -
2932 - /**
2933 - * Send the SETACL command (RFC4314)
2934 - *
2935 - * @param string $mailbox Mailbox name
2936 - * @param string $user User name
2937 - * @param mixed $acl ACL string or array
2938 - *
2939 - * @return boolean True on success, False on failure
2940 - *
2941 - * @since 0.5-beta
2942 - */
2943 - public function setACL($mailbox, $user, $acl)
2944 - {
2945 - if (is_array($acl)) {
2946 - $acl = implode('', $acl);
2947 - }
2948 -
2949 - $result = $this->execute(
2950 - 'SETACL',
2951 - [
2952 - $this->escape($mailbox),
2953 - $this->escape($user),
2954 - strtolower($acl),
2955 - ],
2956 - self::COMMAND_NORESPONSE
2957 - );
2958 -
2959 - return ($result == self::ERROR_OK);
2960 - }
2961 -
2962 - /**
2963 - * Send the DELETEACL command (RFC4314)
2964 - *
2965 - * @param string $mailbox Mailbox name
2966 - * @param string $user User name
2967 - *
2968 - * @return boolean True on success, False on failure
2969 - *
2970 - * @since 0.5-beta
2971 - */
2972 - public function deleteACL($mailbox, $user)
2973 - {
2974 - $result = $this->execute(
2975 - 'DELETEACL',
2976 - [
2977 - $this->escape($mailbox),
2978 - $this->escape($user),
2979 - ],
2980 - self::COMMAND_NORESPONSE
2981 - );
2982 -
2983 - return ($result == self::ERROR_OK);
2984 - }
2985 -
2986 - /**
2987 - * Send the GETACL command (RFC4314)
2988 - *
2989 - * @param string $mailbox Mailbox name
2990 - *
2991 - * @return array User-rights array on success, NULL on error
2992 - * @since 0.5-beta
2993 - */
2994 - public function getACL($mailbox)
2995 - {
2996 - [$code, $response] = $this->execute('GETACL', [$this->escape($mailbox)], 0, '/^\* ACL /i');
2997 -
2998 - if ($code == self::ERROR_OK && $response) {
2999 - // Parse server response (remove "* ACL ")
3000 - $response = substr($response, 6);
3001 - $ret = $this->tokenizeResponse($response);
3002 - $mbox = array_shift($ret);
3003 - $size = count($ret);
3004 -
3005 - // Create user-rights hash array
3006 - // @TODO: consider implementing fixACL() method according to RFC4314.2.1.1
3007 - // so we could return only standard rights defined in RFC4314,
3008 - // excluding 'c' and 'd' defined in RFC2086.
3009 - if ($size % 2 == 0) {
3010 - for ($i = 0 ; $i < $size ; $i++) {
3011 - $ret[$ret[$i]] = str_split($ret[++$i]);
3012 - unset($ret[$i - 1]);
3013 - unset($ret[$i]);
3014 - }
3015 -
3016 - return $ret;
3017 - }
3018 -
3019 - $this->setError(self::ERROR_COMMAND, "Incomplete ACL response");
3020 - }
3021 - }
3022 -
3023 - /**
3024 - * Send the LISTRIGHTS command (RFC4314)
3025 - *
3026 - * @param string $mailbox Mailbox name
3027 - * @param string $user User name
3028 - *
3029 - * @return array List of user rights
3030 - * @since 0.5-beta
3031 - */
3032 - public function listRights($mailbox, $user)
3033 - {
3034 - [$code, $response] = $this->execute(
3035 - 'LISTRIGHTS',
3036 - [$this->escape($mailbox), $this->escape($user)],
3037 - 0,
3038 - '/^\* LISTRIGHTS /i'
3039 - );
3040 -
3041 - if ($code == self::ERROR_OK && $response) {
3042 - // Parse server response (remove "* LISTRIGHTS ")
3043 - $response = substr($response, 13);
3044 -
3045 - $ret_mbox = $this->tokenizeResponse($response, 1);
3046 - $ret_user = $this->tokenizeResponse($response, 1);
3047 - $granted = $this->tokenizeResponse($response, 1);
3048 - $optional = trim($response);
3049 -
3050 - return [
3051 - 'granted' => str_split($granted),
3052 - 'optional' => explode(' ', $optional),
3053 - ];
3054 - }
3055 - }
3056 -
3057 - /**
3058 - * Send the MYRIGHTS command (RFC4314)
3059 - *
3060 - * @param string $mailbox Mailbox name
3061 - *
3062 - * @return array MYRIGHTS response on success, NULL on error
3063 - * @since 0.5-beta
3064 - */
3065 - public function myRights($mailbox)
3066 - {
3067 - [$code, $response] = $this->execute('MYRIGHTS', [$this->escape($mailbox)], 0, '/^\* MYRIGHTS /i');
3068 -
3069 - if ($code == self::ERROR_OK && $response) {
3070 - // Parse server response (remove "* MYRIGHTS ")
3071 - $response = substr($response, 11);
3072 -
3073 - $ret_mbox = $this->tokenizeResponse($response, 1);
3074 - $rights = $this->tokenizeResponse($response, 1);
3075 -
3076 - return str_split($rights);
3077 - }
3078 - }
3079 -
3080 - /**
3081 - * Send the SETMETADATA command (RFC5464)
3082 - *
3083 - * @param string $mailbox Mailbox name
3084 - * @param array $entries Entry-value array (use NULL value as NIL)
3085 - *
3086 - * @return boolean True on success, False on failure
3087 - * @since 0.5-beta
3088 - */
3089 - public function setMetadata($mailbox, $entries)
3090 - {
3091 - if (!is_array($entries) || empty($entries)) {
3092 - $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
3093 -
3094 - return false;
3095 - }
3096 -
3097 - foreach ($entries as $name => $value) {
3098 - $entries[$name] = $this->escape($name).' '.$this->escape($value, true);
3099 - }
3100 -
3101 - $entries = implode(' ', $entries);
3102 - $result = $this->execute(
3103 - 'SETMETADATA',
3104 - [
3105 - $this->escape($mailbox),
3106 - '('.$entries.')',
3107 - ],
3108 - self::COMMAND_NORESPONSE
3109 - );
3110 -
3111 - return ($result == self::ERROR_OK);
3112 - }
3113 -
3114 - /**
3115 - * Send the SETMETADATA command with NIL values (RFC5464)
3116 - *
3117 - * @param string $mailbox Mailbox name
3118 - * @param array $entries Entry names array
3119 - *
3120 - * @return boolean True on success, False on failure
3121 - *
3122 - * @since 0.5-beta
3123 - */
3124 - public function deleteMetadata($mailbox, $entries)
3125 - {
3126 - if (!is_array($entries) && !empty($entries)) {
3127 - $entries = explode(' ', $entries);
3128 - }
3129 -
3130 - if (empty($entries)) {
3131 - $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
3132 -
3133 - return false;
3134 - }
3135 -
3136 - foreach ($entries as $entry) {
3137 - $data[$entry] = null;
3138 - }
3139 -
3140 - return $this->setMetadata($mailbox, $data);
3141 - }
3142 -
3143 - /**
3144 - * Send the GETMETADATA command (RFC5464)
3145 - *
3146 - * @param string $mailbox Mailbox name
3147 - * @param array $entries Entries
3148 - * @param array $options Command options (with MAXSIZE and DEPTH keys)
3149 - *
3150 - * @return array GETMETADATA result on success, NULL on error
3151 - *
3152 - * @since 0.5-beta
3153 - */
3154 - public function getMetadata($mailbox, $entries, $options = [])
3155 - {
3156 - if (!is_array($entries)) {
3157 - $entries = [$entries];
3158 - }
3159 -
3160 - // create entries string
3161 - foreach ($entries as $idx => $name) {
3162 - $entries[$idx] = $this->escape($name);
3163 - }
3164 -
3165 - $optlist = '';
3166 - $entlist = '('.implode(' ', $entries).')';
3167 -
3168 - // create options string
3169 - if (is_array($options)) {
3170 - $options = array_change_key_case($options, CASE_UPPER);
3171 - $opts = [];
3172 -
3173 - if (!empty($options['MAXSIZE'])) {
3174 - $opts[] = 'MAXSIZE '.intval($options['MAXSIZE']);
3175 - }
3176 - if (!empty($options['DEPTH'])) {
3177 - $opts[] = 'DEPTH '.intval($options['DEPTH']);
3178 - }
3179 -
3180 - if ($opts) {
3181 - $optlist = '('.implode(' ', $opts).')';
3182 - }
3183 - }
3184 -
3185 - $optlist .= ($optlist ? ' ' : '').$entlist;
3186 -
3187 - [$code, $response] = $this->execute('GETMETADATA', [
3188 - $this->escape($mailbox),
3189 - $optlist,
3190 - ]);
3191 -
3192 - if ($code == self::ERROR_OK) {
3193 - $result = [];
3194 - $data = $this->tokenizeResponse($response);
3195 -
3196 - // The METADATA response can contain multiple entries in a single
3197 - // response or multiple responses for each entry or group of entries
3198 - for ($i = 0, $size = count($data) ; $i < $size ; $i++) {
3199 - if ($data[$i] === '*'
3200 - && $data[++$i] === 'METADATA'
3201 - && is_string($mbox = $data[++$i])
3202 - && is_array($data[++$i])
3203 - ) {
3204 - for ($x = 0, $size2 = count($data[$i]) ; $x < $size2 ; $x += 2) {
3205 - if ($data[$i][$x + 1] !== null) {
3206 - $result[$mbox][$data[$i][$x]] = $data[$i][$x + 1];
3207 - }
3208 - }
3209 - }
3210 - }
3211 -
3212 - return $result;
3213 - }
3214 - }
3215 -
3216 - /**
3217 - * Send the SETANNOTATION command (draft-daboo-imap-annotatemore)
3218 - *
3219 - * @param string $mailbox Mailbox name
3220 - * @param array $data Data array where each item is an array with
3221 - * three elements: entry name, attribute name, value
3222 - *
3223 - * @return boolean True on success, False on failure
3224 - * @since 0.5-beta
3225 - */
3226 - public function setAnnotation($mailbox, $data)
3227 - {
3228 - if (!is_array($data) || empty($data)) {
3229 - $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
3230 -
3231 - return false;
3232 - }
3233 -
3234 - foreach ($data as $entry) {
3235 - // ANNOTATEMORE drafts before version 08 require quoted parameters
3236 - $entries[] = sprintf(
3237 - '%s (%s %s)',
3238 - $this->escape($entry[0], true),
3239 - $this->escape($entry[1], true),
3240 - $this->escape($entry[2], true)
3241 - );
3242 - }
3243 -
3244 - $entries = implode(' ', $entries);
3245 - $result = $this->execute('SETANNOTATION', [
3246 - $this->escape($mailbox),
3247 - $entries,
3248 - ], self::COMMAND_NORESPONSE);
3249 -
3250 - return ($result == self::ERROR_OK);
3251 - }
3252 -
3253 - /**
3254 - * Send the SETANNOTATION command with NIL values (draft-daboo-imap-annotatemore)
3255 - *
3256 - * @param string $mailbox Mailbox name
3257 - * @param array $data Data array where each item is an array with
3258 - * two elements: entry name and attribute name
3259 - *
3260 - * @return boolean True on success, False on failure
3261 - *
3262 - * @since 0.5-beta
3263 - */
3264 - public function deleteAnnotation($mailbox, $data)
3265 - {
3266 - if (!is_array($data) || empty($data)) {
3267 - $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
3268 -
3269 - return false;
3270 - }
3271 -
3272 - return $this->setAnnotation($mailbox, $data);
3273 - }
3274 -
3275 - /**
3276 - * Send the GETANNOTATION command (draft-daboo-imap-annotatemore)
3277 - *
3278 - * @param string $mailbox Mailbox name
3279 - * @param array $entries Entries names
3280 - * @param array $attribs Attribs names
3281 - *
3282 - * @return array Annotations result on success, NULL on error
3283 - *
3284 - * @since 0.5-beta
3285 - */
3286 - public function getAnnotation($mailbox, $entries, $attribs)
3287 - {
3288 - if (!is_array($entries)) {
3289 - $entries = [$entries];
3290 - }
3291 -
3292 - // create entries string
3293 - // ANNOTATEMORE drafts before version 08 require quoted parameters
3294 - foreach ($entries as $idx => $name) {
3295 - $entries[$idx] = $this->escape($name, true);
3296 - }
3297 - $entries = '('.implode(' ', $entries).')';
3298 -
3299 - if (!is_array($attribs)) {
3300 - $attribs = [$attribs];
3301 - }
3302 -
3303 - // create attributes string
3304 - foreach ($attribs as $idx => $name) {
3305 - $attribs[$idx] = $this->escape($name, true);
3306 - }
3307 - $attribs = '('.implode(' ', $attribs).')';
3308 -
3309 - [$code, $response] = $this->execute('GETANNOTATION', [
3310 - $this->escape($mailbox),
3311 - $entries,
3312 - $attribs,
3313 - ]);
3314 -
3315 - if ($code == self::ERROR_OK) {
3316 - $result = [];
3317 - $data = $this->tokenizeResponse($response);
3318 -
3319 - // Here we returns only data compatible with METADATA result format
3320 - if (!empty($data) && ($size = count($data))) {
3321 - for ($i = 0 ; $i < $size ; $i++) {
3322 - $entry = $data[$i];
3323 - if (isset($mbox) && is_array($entry)) {
3324 - $attribs = $entry;
3325 - $entry = $last_entry;
3326 - } elseif ($entry == '*') {
3327 - if ($data[$i + 1] == 'ANNOTATION') {
3328 - $mbox = $data[$i + 2];
3329 - unset($data[$i]); // "*"
3330 - unset($data[++$i]); // "ANNOTATION"
3331 - unset($data[++$i]); // Mailbox
3332 - } // get rid of other untagged responses
3333 - else {
3334 - unset($mbox);
3335 - unset($data[$i]);
3336 - }
3337 - continue;
3338 - } elseif (isset($mbox)) {
3339 - $attribs = $data[++$i];
3340 - } else {
3341 - unset($data[$i]);
3342 - continue;
3343 - }
3344 -
3345 - if (!empty($attribs)) {
3346 - for ($x = 0, $len = count($attribs) ; $x < $len ;) {
3347 - $attr = $attribs[$x++];
3348 - $value = $attribs[$x++];
3349 - if ($attr == 'value.priv' && $value !== null) {
3350 - $result[$mbox]['/private'.$entry] = $value;
3351 - } elseif ($attr == 'value.shared' && $value !== null) {
3352 - $result[$mbox]['/shared'.$entry] = $value;
3353 - }
3354 - }
3355 - }
3356 - $last_entry = $entry;
3357 - unset($data[$i]);
3358 - }
3359 - }
3360 -
3361 - return $result;
3362 - }
3363 - }
3364 -
3365 - /**
3366 - * Returns BODYSTRUCTURE for the specified message.
3367 - *
3368 - * @param string $mailbox Folder name
3369 - * @param int $id Message sequence number or UID
3370 - * @param bool $is_uid True if $id is an UID
3371 - *
3372 - * @return array/bool Body structure array or False on error.
3373 - * @since 0.6
3374 - */
3375 - public function getStructure($mailbox, $id, $is_uid = false)
3376 - {
3377 - $result = $this->fetch($mailbox, $id, $is_uid, ['BODYSTRUCTURE']);
3378 -
3379 - if (is_array($result)) {
3380 - $result = array_shift($result);
3381 -
3382 - return $result->bodystructure;
3383 - }
3384 -
3385 - return false;
3386 - }
3387 -
3388 - /**
3389 - * Returns data of a message part according to specified structure.
3390 - *
3391 - * @param array $structure Message structure (getStructure() result)
3392 - * @param string $part Message part identifier
3393 - *
3394 - * @return array Part data as hash array (type, encoding, charset, size)
3395 - */
3396 - public static function getStructurePartData($structure, $part)
3397 - {
3398 - $part_a = self::getStructurePartArray($structure, $part);
3399 - $data = [];
3400 -
3401 - if (empty($part_a)) {
3402 - return $data;
3403 - }
3404 -
3405 - // content-type
3406 - if (is_array($part_a[0])) {
3407 - $data['type'] = 'multipart';
3408 - } else {
3409 - $data['type'] = strtolower($part_a[0]);
3410 - $data['subtype'] = strtolower($part_a[1]);
3411 - $data['encoding'] = strtolower($part_a[5]);
3412 -
3413 - // charset
3414 - if (is_array($part_a[2])) {
3415 - foreach ($part_a[2] as $key => $val) {
3416 - if (strcasecmp($val, 'charset') == 0) {
3417 - $data['charset'] = $part_a[2][$key + 1];
3418 - break;
3419 - }
3420 - }
3421 - }
3422 - }
3423 -
3424 - // size
3425 - $data['size'] = intval($part_a[6]);
3426 -
3427 - return $data;
3428 - }
3429 -
3430 - public static function getStructurePartArray($a, $part)
3431 - {
3432 - if (!is_array($a)) {
3433 - return false;
3434 - }
3435 -
3436 - if (empty($part)) {
3437 - return $a;
3438 - }
3439 -
3440 - $ctype = is_string($a[0]) && is_string($a[1]) ? $a[0].'/'.$a[1] : '';
3441 -
3442 - if (strcasecmp($ctype, 'message/rfc822') == 0) {
3443 - $a = $a[8];
3444 - }
3445 -
3446 - if (strpos($part, '.') > 0) {
3447 - $orig_part = $part;
3448 - $pos = strpos($part, '.');
3449 - $rest = substr($orig_part, $pos + 1);
3450 - $part = substr($orig_part, 0, $pos);
3451 -
3452 - return self::getStructurePartArray($a[$part - 1], $rest);
3453 - } elseif ($part > 0) {
3454 - return (is_array($a[$part - 1])) ? $a[$part - 1] : $a;
3455 - }
3456 - }
3457 -
3458 - /**
3459 - * Creates next command identifier (tag)
3460 - *
3461 - * @return string Command identifier
3462 - * @since 0.5-beta
3463 - */
3464 - public function nextTag()
3465 - {
3466 - $this->cmd_num++;
3467 - $this->cmd_tag = sprintf('A%04d', $this->cmd_num);
3468 -
3469 - return $this->cmd_tag;
3470 - }
3471 -
3472 - /**
3473 - * Sends IMAP command and parses result
3474 - *
3475 - * @param string $command IMAP command
3476 - * @param array $arguments Command arguments
3477 - * @param int $options Execution options
3478 - * @param string $filter Line filter (regexp)
3479 - *
3480 - * @return mixed Response code or list of response code and data
3481 - * @since 0.5-beta
3482 - */
3483 - public function execute($command, $arguments = [], $options = 0, $filter = null)
3484 - {
3485 - $tag = $this->nextTag();
3486 - $query = $tag.' '.$command;
3487 - $noresp = ($options & self::COMMAND_NORESPONSE);
3488 - $response = $noresp ? null : '';
3489 -
3490 - if (!empty($arguments)) {
3491 - foreach ($arguments as $arg) {
3492 - $query .= ' '.self::r_implode($arg);
3493 - }
3494 - }
3495 -
3496 - // Send command
3497 - if (!$this->putLineC($query, true, ($options & self::COMMAND_ANONYMIZED))) {
3498 - preg_match('/^[A-Z0-9]+ ((UID )?[A-Z]+)/', $query, $matches);
3499 - $cmd = $matches[1] ? : 'UNKNOWN';
3500 - $this->setError(self::ERROR_COMMAND, "Failed to send $cmd command");
3501 -
3502 - return $noresp ? self::ERROR_COMMAND : [self::ERROR_COMMAND, ''];
3503 - }
3504 -
3505 - // Parse response
3506 - do {
3507 - $line = $this->readFullLine(4096);
3508 -
3509 - if ($response !== null) {
3510 - if (!$filter || preg_match($filter, $line)) {
3511 - $response .= $line;
3512 - }
3513 - }
3514 -
3515 - // parse untagged response for [COPYUID 1204196876 3456:3457 123:124] (RFC6851)
3516 - if ($line && $command == 'UID MOVE') {
3517 - if (preg_match("/^\* OK \[COPYUID [0-9]+ ([0-9,:]+) ([0-9,:]+)\]/i", $line, $m)) {
3518 - $this->data['COPYUID'] = [$m[1], $m[2]];
3519 - }
3520 - }
3521 - } while (!$this->startsWith($line, $tag.' ', true, true));
3522 -
3523 - $code = $this->parseResult($line, $command.': ');
3524 -
3525 - // Remove last line from response
3526 - if ($response) {
3527 - if (!$filter) {
3528 - $line_len = min(strlen($response), strlen($line));
3529 - $response = substr($response, 0, -$line_len);
3530 - }
3531 -
3532 - $response = rtrim($response, "\r\n");
3533 - }
3534 -
3535 - // optional CAPABILITY response
3536 - if (($options & self::COMMAND_CAPABILITY) && $code == self::ERROR_OK
3537 - && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)
3538 - ) {
3539 - $this->parseCapability($matches[1], true);
3540 - }
3541 -
3542 - // return last line only (without command tag, result and response code)
3543 - if ($line && ($options & self::COMMAND_LASTLINE)) {
3544 - $response = preg_replace("/^$tag (OK|NO|BAD|BYE|PREAUTH)?\s*(\[[a-z-]+\])?\s*/i", '', trim($line));
3545 - }
3546 -
3547 - // return raw last line only (without command tag, result and response code)
3548 - if ($line && ($options & self::COMMAND_RAW_LASTLINE)) {
3549 - $response = preg_replace("/^$tag (OK|NO|BAD|BYE|PREAUTH)?\s*/i", '', trim($line));
3550 - }
3551 -
3552 - return $noresp ? $code : [$code, $response];
3553 - }
3554 -
3555 - /**
3556 - * Splits IMAP response into string tokens
3557 - *
3558 - * @param string &$str The IMAP's server response
3559 - * @param int $num Number of tokens to return
3560 - *
3561 - * @return mixed Tokens array or string if $num=1
3562 - * @since 0.5-beta
3563 - */
3564 - public static function tokenizeResponse(&$str, $num = 0)
3565 - {
3566 - $result = [];
3567 -
3568 - while (!$num || count($result) < $num) {
3569 - // remove spaces from the beginning of the string
3570 - $str = ltrim($str);
3571 -
3572 - switch ($str[0]) {
3573 -
3574 - // String literal
3575 - case '{':
3576 - if (($epos = strpos($str, "}\r\n", 1)) == false) {
3577 - // error
3578 - }
3579 - if (!is_numeric(($bytes = substr($str, 1, $epos - 1)))) {
3580 - // error
3581 - }
3582 -
3583 - $result[] = $bytes ? substr($str, $epos + 3, $bytes) : '';
3584 - $str = substr($str, $epos + 3 + $bytes);
3585 - break;
3586 -
3587 - // Quoted string
3588 - case '"':
3589 - $len = strlen($str);
3590 -
3591 - for ($pos = 1 ; $pos < $len ; $pos++) {
3592 - if ($str[$pos] == '"') {
3593 - break;
3594 - }
3595 - if ($str[$pos] == "\\") {
3596 - if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") {
3597 - $pos++;
3598 - }
3599 - }
3600 - }
3601 -
3602 - // we need to strip slashes for a quoted string
3603 - $result[] = stripslashes(substr($str, 1, $pos - 1));
3604 - $str = substr($str, $pos + 1);
3605 - break;
3606 -
3607 - // Parenthesized list
3608 - case '(':
3609 - $str = substr($str, 1);
3610 - $result[] = self::tokenizeResponse($str);
3611 - break;
3612 -
3613 - case ')':
3614 - $str = substr($str, 1);
3615 -
3616 - return $result;
3617 -
3618 - // String atom, number, astring, NIL, *, %
3619 - default:
3620 - // empty string
3621 - if ($str === '' || $str === null) {
3622 - break 2;
3623 - }
3624 -
3625 - // excluded chars: SP, CTL, ), DEL
3626 - // we do not exclude [ and ] (#1489223)
3627 - if (preg_match('/^([^\x00-\x20\x29\x7F]+)/', $str, $m)) {
3628 - $result[] = $m[1] == 'NIL' ? null : $m[1];
3629 - $str = substr($str, strlen($m[1]));
3630 - }
3631 - break;
3632 - }
3633 - }
3634 -
3635 - return $num == 1 ? $result[0] : $result;
3636 - }
3637 -
3638 - /**
3639 - * Joins IMAP command line elements (recursively)
3640 - */
3641 - protected static function r_implode($element)
3642 - {
3643 - $string = '';
3644 -
3645 - if (is_array($element)) {
3646 - reset($element);
3647 - foreach ($element as $value) {
3648 - $string .= ' '.self::r_implode($value);
3649 - }
3650 - } else {
3651 - return $element;
3652 - }
3653 -
3654 - return '('.trim($string).')';
3655 - }
3656 -
3657 - /**
3658 - * Converts message identifiers array into sequence-set syntax
3659 - *
3660 - * @param array $messages Message identifiers
3661 - * @param bool $force Forces compression of any size
3662 - *
3663 - * @return string Compressed sequence-set
3664 - */
3665 - public static function compressMessageSet($messages, $force = false)
3666 - {
3667 - // given a comma delimited list of independent mid's,
3668 - // compresses by grouping sequences together
3669 - if (!is_array($messages)) {
3670 - // if less than 255 bytes long, let's not bother
3671 - if (!$force && strlen($messages) < 255) {
3672 - return preg_match('/[^0-9:,*]/', $messages) ? 'INVALID' : $messages;
3673 - }
3674 -
3675 - // see if it's already been compressed
3676 - if (strpos($messages, ':') !== false) {
3677 - return preg_match('/[^0-9:,*]/', $messages) ? 'INVALID' : $messages;
3678 - }
3679 -
3680 - // separate, then sort
3681 - $messages = explode(',', $messages);
3682 - }
3683 -
3684 - sort($messages);
3685 -
3686 - $result = [];
3687 - $start = $prev = $messages[0];
3688 -
3689 - foreach ($messages as $id) {
3690 - $incr = $id - $prev;
3691 - if ($incr > 1) { // found a gap
3692 - if ($start == $prev) {
3693 - $result[] = $prev; // push single id
3694 - } else {
3695 - $result[] = $start.':'.$prev; // push sequence as start_id:end_id
3696 - }
3697 - $start = $id; // start of new sequence
3698 - }
3699 - $prev = $id;
3700 - }
3701 -
3702 - // handle the last sequence/id
3703 - if ($start == $prev) {
3704 - $result[] = $prev;
3705 - } else {
3706 - $result[] = $start.':'.$prev;
3707 - }
3708 -
3709 - // return as comma separated string
3710 - $result = implode(',', $result);
3711 -
3712 - return preg_match('/[^0-9:,*]/', $result) ? 'INVALID' : $result;
3713 - }
3714 -
3715 - /**
3716 - * Converts message sequence-set into array
3717 - *
3718 - * @param string $messages Message identifiers
3719 - *
3720 - * @return array List of message identifiers
3721 - */
3722 - public static function uncompressMessageSet($messages)
3723 - {
3724 - if (empty($messages)) {
3725 - return [];
3726 - }
3727 -
3728 - $result = [];
3729 - $messages = explode(',', $messages);
3730 -
3731 - foreach ($messages as $idx => $part) {
3732 - $items = explode(':', $part);
3733 - $max = max($items[0], $items[1] ?? $items[0]);
3734 -
3735 - for ($x = $items[0] ; $x <= $max ; $x++) {
3736 - $result[] = (int)$x;
3737 - }
3738 - unset($messages[$idx]);
3739 - }
3740 -
3741 - return $result;
3742 - }
3743 -
3744 - /**
3745 - * Clear internal status cache
3746 - */
3747 - protected function clear_status_cache($mailbox)
3748 - {
3749 - unset($this->data['STATUS:'.$mailbox]);
3750 -
3751 - $keys = ['EXISTS', 'RECENT', 'UNSEEN', 'UID-MAP'];
3752 -
3753 - foreach ($keys as $key) {
3754 - unset($this->data[$key]);
3755 - }
3756 - }
3757 -
3758 - /**
3759 - * Clear internal cache of the current mailbox
3760 - */
3761 - protected function clear_mailbox_cache()
3762 - {
3763 - $this->clear_status_cache($this->selected);
3764 -
3765 - $keys = [
3766 - 'UIDNEXT',
3767 - 'UIDVALIDITY',
3768 - 'HIGHESTMODSEQ',
3769 - 'NOMODSEQ',
3770 - 'PERMANENTFLAGS',
3771 - 'QRESYNC',
3772 - 'VANISHED',
3773 - 'READ-WRITE',
3774 - ];
3775 -
3776 - foreach ($keys as $key) {
3777 - unset($this->data[$key]);
3778 - }
3779 - }
3780 -
3781 - /**
3782 - * Converts flags array into string for inclusion in IMAP command
3783 - *
3784 - * @param array $flags Flags (see self::flags)
3785 - *
3786 - * @return string Space-separated list of flags
3787 - */
3788 - protected function flagsToStr($flags)
3789 - {
3790 - foreach ((array)$flags as $idx => $flag) {
3791 - if ($flag = $this->flags[strtoupper($flag)]) {
3792 - $flags[$idx] = $flag;
3793 - }
3794 - }
3795 -
3796 - return implode(' ', (array)$flags);
3797 - }
3798 -
3799 - /**
3800 - * CAPABILITY response parser
3801 - */
3802 - protected function parseCapability($str, $trusted = false)
3803 - {
3804 - $str = preg_replace('/^\* CAPABILITY /i', '', $str);
3805 -
3806 - $this->capability = explode(' ', strtoupper($str));
3807 -
3808 - if (!empty($this->prefs['disabled_caps'])) {
3809 - $this->capability = array_diff($this->capability, $this->prefs['disabled_caps']);
3810 - }
3811 -
3812 - if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) {
3813 - $this->prefs['literal+'] = true;
3814 - }
3815 -
3816 - if ($trusted) {
3817 - $this->capability_readed = true;
3818 - }
3819 - }
3820 -
3821 - /**
3822 - * Escapes a string when it contains special characters (RFC3501)
3823 - *
3824 - * @param string $string IMAP string
3825 - * @param boolean $force_quotes Forces string quoting (for atoms)
3826 - *
3827 - * @return string String atom, quoted-string or string literal
3828 - * @todo lists
3829 - */
3830 - public static function escape($string, $force_quotes = false)
3831 - {
3832 - if ($string === null) {
3833 - return 'NIL';
3834 - }
3835 -
3836 - if ($string === '') {
3837 - return '""';
3838 - }
3839 -
3840 - // atom-string (only safe characters)
3841 - if (!$force_quotes && !preg_match('/[\x00-\x20\x22\x25\x28-\x2A\x5B-\x5D\x7B\x7D\x80-\xFF]/', $string)) {
3842 - return $string;
3843 - }
3844 -
3845 - // quoted-string
3846 - if (!preg_match('/[\r\n\x00\x80-\xFF]/', $string)) {
3847 - return '"'.addcslashes($string, '\\"').'"';
3848 - }
3849 -
3850 - // literal-string
3851 - return sprintf("{%d}\r\n%s", strlen($string), $string);
3852 - }
3853 -
3854 - /**
3855 - * Set the value of the debugging flag.
3856 - *
3857 - * @param boolean $debug New value for the debugging flag.
3858 - * @param callback $handler Logging handler function
3859 - *
3860 - * @since 0.5-stable
3861 - */
3862 - public function setDebug($debug, $handler = null)
3863 - {
3864 - $this->debug = $debug;
3865 - $this->debug_handler = $handler;
3866 - }
3867 -
3868 - /**
3869 - * Write the given debug text to the current debug output handler.
3870 - *
3871 - * @param string $message Debug message text.
3872 - *
3873 - * @since 0.5-stable
3874 - */
3875 - protected function debug($message)
3876 - {
3877 - if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) {
3878 - $diff = $len - self::DEBUG_LINE_LENGTH;
3879 - $message = substr($message, 0, self::DEBUG_LINE_LENGTH)
3880 - ."... [truncated $diff bytes]";
3881 - }
3882 -
3883 - if ($this->resourceid) {
3884 - $message = sprintf('[%s] %s', $this->resourceid, $message);
3885 - }
3886 -
3887 - if ($this->debug_handler) {
3888 - call_user_func_array($this->debug_handler, [&$this, $message]);
3889 - } else {
3890 - echo "DEBUG: $message\n";
3891 - }
3892 - }
3893 -
3894 - public function getRawLastLine()
3895 - {
3896 - return $this->rawLastLine;
3897 - }
3898 -}