comparison program/lib/Roundcube/rcube_imap_generic.php @ 0:4681f974d28b

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