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

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

3,899 lines 120.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * +-----------------------------------------------------------------------+
5 * | This file is part of the Roundcube Webmail client |
6 * | |
7 * | Copyright (C) The Roundcube Dev Team |
8 * | Copyright (C) Kolab Systems AG |
9 * | |
10 * | Licensed under the GNU General Public License version 3 or |
11 * | any later version with exceptions for skins & plugins. |
12 * | See the README file for a full license statement. |
13 * | |
14 * | PURPOSE: |
15 * | 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 }
3899