]> git.donarmstrong.com Git - roundcube.git/blob - program/include/rcube_imap_generic.php
Imported Upstream version 0.5.2+dfsg
[roundcube.git] / program / include / rcube_imap_generic.php
1 <?php
2
3 /**
4  +-----------------------------------------------------------------------+
5  | program/include/rcube_imap_generic.php                                |
6  |                                                                       |
7  | This file is part of the Roundcube Webmail client                     |
8  | Copyright (C) 2005-2010, Roundcube Dev. - Switzerland                 |
9  | Licensed under the GNU GPL                                            |
10  |                                                                       |
11  | PURPOSE:                                                              |
12  |   Provide alternative IMAP library that doesn't rely on the standard  |
13  |   C-Client based version. This allows to function regardless          |
14  |   of whether or not the PHP build it's running on has IMAP            |
15  |   functionality built-in.                                             |
16  |                                                                       |
17  |   Based on Iloha IMAP Library. See http://ilohamail.org/ for details  |
18  |                                                                       |
19  +-----------------------------------------------------------------------+
20  | Author: Aleksander Machniak <alec@alec.pl>                            |
21  | Author: Ryo Chijiiwa <Ryo@IlohaMail.org>                              |
22  +-----------------------------------------------------------------------+
23
24  $Id: rcube_imap_generic.php 4643 2011-04-11 12:24:00Z alec $
25
26 */
27
28
29 /**
30  * Struct representing an e-mail message header
31  *
32  * @package Mail
33  * @author  Aleksander Machniak <alec@alec.pl>
34  */
35 class rcube_mail_header
36 {
37     public $id;
38     public $uid;
39     public $subject;
40     public $from;
41     public $to;
42     public $cc;
43     public $replyto;
44     public $in_reply_to;
45     public $date;
46     public $messageID;
47     public $size;
48     public $encoding;
49     public $charset;
50     public $ctype;
51     public $flags;
52     public $timestamp;
53     public $body_structure;
54     public $internaldate;
55     public $references;
56     public $priority;
57     public $mdn_to;
58     public $mdn_sent = false;
59     public $is_draft = false;
60     public $seen = false;
61     public $deleted = false;
62     public $recent = false;
63     public $answered = false;
64     public $forwarded = false;
65     public $junk = false;
66     public $flagged = false;
67     public $has_children = false;
68     public $depth = 0;
69     public $unread_children = 0;
70     public $others = array();
71 }
72
73 // For backward compatibility with cached messages (#1486602)
74 class iilBasicHeader extends rcube_mail_header
75 {
76 }
77
78 /**
79  * PHP based wrapper class to connect to an IMAP server
80  *
81  * @package Mail
82  * @author  Aleksander Machniak <alec@alec.pl>
83  */
84 class rcube_imap_generic
85 {
86     public $error;
87     public $errornum;
88     public $result;
89     public $resultcode;
90     public $data = array();
91     public $flags = array(
92         'SEEN'     => '\\Seen',
93         'DELETED'  => '\\Deleted',
94         'RECENT'   => '\\Recent',
95         'ANSWERED' => '\\Answered',
96         'DRAFT'    => '\\Draft',
97         'FLAGGED'  => '\\Flagged',
98         'FORWARDED' => '$Forwarded',
99         'MDNSENT'  => '$MDNSent',
100         '*'        => '\\*',
101     );
102
103     private $selected;
104     private $fp;
105     private $host;
106     private $logged = false;
107     private $capability = array();
108     private $capability_readed = false;
109     private $prefs;
110     private $cmd_tag;
111     private $cmd_num = 0;
112     private $_debug = false;
113     private $_debug_handler = false;
114
115     const ERROR_OK = 0;
116     const ERROR_NO = -1;
117     const ERROR_BAD = -2;
118     const ERROR_BYE = -3;
119     const ERROR_UNKNOWN = -4;
120     const ERROR_COMMAND = -5;
121     const ERROR_READONLY = -6;
122
123     const COMMAND_NORESPONSE = 1;
124     const COMMAND_CAPABILITY = 2;
125     const COMMAND_LASTLINE   = 4;
126
127     /**
128      * Object constructor
129      */
130     function __construct()
131     {
132     }
133
134     /**
135      * Send simple (one line) command to the connection stream
136      *
137      * @param string $string Command string
138      * @param bool   $endln  True if CRLF need to be added at the end of command
139      *
140      * @param int Number of bytes sent, False on error
141      */
142     function putLine($string, $endln=true)
143     {
144         if (!$this->fp)
145             return false;
146
147         if ($this->_debug) {
148             $this->debug('C: '. rtrim($string));
149         }
150
151         $res = fwrite($this->fp, $string . ($endln ? "\r\n" : ''));
152
153         if ($res === false) {
154             @fclose($this->fp);
155             $this->fp = null;
156         }
157
158         return $res;
159     }
160
161     /**
162      * Send command to the connection stream with Command Continuation
163      * Requests (RFC3501 7.5) and LITERAL+ (RFC2088) support
164      *
165      * @param string $string Command string
166      * @param bool   $endln  True if CRLF need to be added at the end of command
167      *
168      * @param int Number of bytes sent, False on error
169      */
170     function putLineC($string, $endln=true)
171     {
172         if (!$this->fp)
173             return false;
174
175         if ($endln)
176             $string .= "\r\n";
177
178         $res = 0;
179         if ($parts = preg_split('/(\{[0-9]+\}\r\n)/m', $string, -1, PREG_SPLIT_DELIM_CAPTURE)) {
180             for ($i=0, $cnt=count($parts); $i<$cnt; $i++) {
181                 if (preg_match('/^\{[0-9]+\}\r\n$/', $parts[$i+1])) {
182                     // LITERAL+ support
183                     if ($this->prefs['literal+'])
184                         $parts[$i+1] = preg_replace('/([0-9]+)/', '\\1+', $parts[$i+1]);
185
186                     $bytes = $this->putLine($parts[$i].$parts[$i+1], false);
187                     if ($bytes === false)
188                         return false;
189                     $res += $bytes;
190
191                     // don't wait if server supports LITERAL+ capability
192                     if (!$this->prefs['literal+']) {
193                         $line = $this->readLine(1000);
194                         // handle error in command
195                         if ($line[0] != '+')
196                             return false;
197                     }
198                     $i++;
199                 }
200                 else {
201                     $bytes = $this->putLine($parts[$i], false);
202                     if ($bytes === false)
203                         return false;
204                     $res += $bytes;
205                 }
206             }
207         }
208
209         return $res;
210     }
211
212     function readLine($size=1024)
213     {
214         $line = '';
215
216         if (!$size) {
217             $size = 1024;
218         }
219
220         do {
221             if ($this->eof()) {
222                 return $line ? $line : NULL;
223             }
224
225             $buffer = fgets($this->fp, $size);
226
227             if ($buffer === false) {
228                 $this->closeSocket();
229                 break;
230             }
231             if ($this->_debug) {
232                 $this->debug('S: '. rtrim($buffer));
233             }
234             $line .= $buffer;
235         } while (substr($buffer, -1) != "\n");
236
237         return $line;
238     }
239
240     function multLine($line, $escape=false)
241     {
242         $line = rtrim($line);
243         if (preg_match('/\{[0-9]+\}$/', $line)) {
244             $out = '';
245
246             preg_match_all('/(.*)\{([0-9]+)\}$/', $line, $a);
247             $bytes = $a[2][0];
248             while (strlen($out) < $bytes) {
249                 $line = $this->readBytes($bytes);
250                 if ($line === NULL)
251                     break;
252                 $out .= $line;
253             }
254
255             $line = $a[1][0] . ($escape ? $this->escape($out) : $out);
256         }
257
258         return $line;
259     }
260
261     function readBytes($bytes)
262     {
263         $data = '';
264         $len  = 0;
265         while ($len < $bytes && !$this->eof())
266         {
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     function readReply(&$untagged=null)
283     {
284         do {
285             $line = trim($this->readLine(1024));
286             // store untagged response lines
287             if ($line[0] == '*')
288                 $untagged[] = $line;
289         } while ($line[0] == '*');
290
291         if ($untagged)
292             $untagged = join("\n", $untagged);
293
294         return $line;
295     }
296
297     function parseResult($string, $err_prefix='')
298     {
299         if (preg_match('/^[a-z0-9*]+ (OK|NO|BAD|BYE)(.*)$/i', trim($string), $matches)) {
300             $res = strtoupper($matches[1]);
301             $str = trim($matches[2]);
302
303             if ($res == 'OK') {
304                 $this->errornum = self::ERROR_OK;
305             } else if ($res == 'NO') {
306                 $this->errornum = self::ERROR_NO;
307             } else if ($res == 'BAD') {
308                 $this->errornum = self::ERROR_BAD;
309             } else if ($res == 'BYE') {
310                 $this->closeSocket();
311                 $this->errornum = self::ERROR_BYE;
312             }
313
314             if ($str) {
315                 $str = trim($str);
316                 // get response string and code (RFC5530)
317                 if (preg_match("/^\[([a-z-]+)\]/i", $str, $m)) {
318                     $this->resultcode = strtoupper($m[1]);
319                     $str = trim(substr($str, strlen($m[1]) + 2));
320                 }
321                 else {
322                     $this->resultcode = null;
323                 }
324                 $this->result = $str;
325
326                 if ($this->errornum != self::ERROR_OK) {
327                     $this->error = $err_prefix ? $err_prefix.$str : $str;
328                 }
329             }
330
331             return $this->errornum;
332         }
333         return self::ERROR_UNKNOWN;
334     }
335
336     private function eof()
337     {
338         if (!is_resource($this->fp)) {
339             return true;
340         }
341
342         // If a connection opened by fsockopen() wasn't closed
343         // by the server, feof() will hang.
344         $start = microtime(true);
345
346         if (feof($this->fp) || 
347             ($this->prefs['timeout'] && (microtime(true) - $start > $this->prefs['timeout']))
348         ) {
349             $this->closeSocket();
350             return true;
351         }
352
353         return false;
354     }
355
356     private function closeSocket()
357     {
358         @fclose($this->fp);
359         $this->fp = null;
360     }
361
362     function setError($code, $msg='')
363     {
364         $this->errornum = $code;
365         $this->error    = $msg;
366     }
367
368     // check if $string starts with $match (or * BYE/BAD)
369     function startsWith($string, $match, $error=false, $nonempty=false)
370     {
371         $len = strlen($match);
372         if ($len == 0) {
373             return false;
374         }
375         if (!$this->fp) {
376             return true;
377         }
378         if (strncmp($string, $match, $len) == 0) {
379             return true;
380         }
381         if ($error && preg_match('/^\* (BYE|BAD) /i', $string, $m)) {
382             if (strtoupper($m[1]) == 'BYE') {
383                 $this->closeSocket();
384             }
385             return true;
386         }
387         if ($nonempty && !strlen($string)) {
388             return true;
389         }
390         return false;
391     }
392
393     private function hasCapability($name)
394     {
395         if (empty($this->capability) || $name == '') {
396             return false;
397         }
398
399         if (in_array($name, $this->capability)) {
400             return true;
401         }
402         else if (strpos($name, '=')) {
403             return false;
404         }
405
406         $result = array();
407         foreach ($this->capability as $cap) {
408             $entry = explode('=', $cap);
409             if ($entry[0] == $name) {
410                 $result[] = $entry[1];
411             }
412         }
413
414         return !empty($result) ? $result : false;
415     }
416
417     /**
418      * Capabilities checker
419      *
420      * @param string $name Capability name
421      *
422      * @return mixed Capability values array for key=value pairs, true/false for others
423      */
424     function getCapability($name)
425     {
426         $result = $this->hasCapability($name);
427
428         if (!empty($result)) {
429             return $result;
430         }
431         else if ($this->capability_readed) {
432             return false;
433         }
434
435         // get capabilities (only once) because initial
436         // optional CAPABILITY response may differ
437         $result = $this->execute('CAPABILITY');
438
439         if ($result[0] == self::ERROR_OK) {
440             $this->parseCapability($result[1]);
441         }
442
443         $this->capability_readed = true;
444
445         return $this->hasCapability($name);
446     }
447
448     function clearCapability()
449     {
450         $this->capability = array();
451         $this->capability_readed = false;
452     }
453
454     /**
455      * DIGEST-MD5/CRAM-MD5/PLAIN Authentication
456      *
457      * @param string $user
458      * @param string $pass
459      * @param string $type Authentication type (PLAIN/CRAM-MD5/DIGEST-MD5)
460      *
461      * @return resource Connection resourse on success, error code on error
462      */
463     function authenticate($user, $pass, $type='PLAIN')
464     {
465         if ($type == 'CRAM-MD5' || $type == 'DIGEST-MD5') {
466             if ($type == 'DIGEST-MD5' && !class_exists('Auth_SASL')) {
467                 $this->setError(self::ERROR_BYE,
468                     "The Auth_SASL package is required for DIGEST-MD5 authentication");
469                 return self::ERROR_BAD;
470             }
471
472             $this->putLine($this->nextTag() . " AUTHENTICATE $type");
473             $line = trim($this->readReply());
474
475             if ($line[0] == '+') {
476                 $challenge = substr($line, 2);
477             }
478             else {
479                 return $this->parseResult($line);
480             }
481
482             if ($type == 'CRAM-MD5') {
483                 // RFC2195: CRAM-MD5
484                 $ipad = '';
485                 $opad = '';
486
487                 // initialize ipad, opad
488                 for ($i=0; $i<64; $i++) {
489                     $ipad .= chr(0x36);
490                     $opad .= chr(0x5C);
491                 }
492
493                 // pad $pass so it's 64 bytes
494                 $padLen = 64 - strlen($pass);
495                 for ($i=0; $i<$padLen; $i++) {
496                     $pass .= chr(0);
497                 }
498
499                 // generate hash
500                 $hash  = md5($this->_xor($pass, $opad) . pack("H*",
501                     md5($this->_xor($pass, $ipad) . base64_decode($challenge))));
502                 $reply = base64_encode($user . ' ' . $hash);
503
504                 // send result
505                 $this->putLine($reply);
506             }
507             else {
508                 // RFC2831: DIGEST-MD5
509                 // proxy authorization
510                 if (!empty($this->prefs['auth_cid'])) {
511                     $authc = $this->prefs['auth_cid'];
512                     $pass  = $this->prefs['auth_pw'];
513                 }
514                 else {
515                     $authc = $user;
516                 }
517                 $auth_sasl = Auth_SASL::factory('digestmd5');
518                 $reply = base64_encode($auth_sasl->getResponse($authc, $pass,
519                     base64_decode($challenge), $this->host, 'imap', $user));
520
521                 // send result
522                 $this->putLine($reply);
523                 $line = trim($this->readReply());
524
525                 if ($line[0] == '+') {
526                     $challenge = substr($line, 2);
527                 }
528                 else {
529                     return $this->parseResult($line);
530                 }
531
532                 // check response
533                 $challenge = base64_decode($challenge);
534                 if (strpos($challenge, 'rspauth=') === false) {
535                     $this->setError(self::ERROR_BAD,
536                         "Unexpected response from server to DIGEST-MD5 response");
537                     return self::ERROR_BAD;
538                 }
539
540                 $this->putLine('');
541             }
542
543             $line = $this->readReply();
544             $result = $this->parseResult($line);
545         }
546         else { // PLAIN
547             // proxy authorization
548             if (!empty($this->prefs['auth_cid'])) {
549                 $authc = $this->prefs['auth_cid'];
550                 $pass  = $this->prefs['auth_pw'];
551             }
552             else {
553                 $authc = $user;
554             }
555
556             $reply = base64_encode($user . chr(0) . $authc . chr(0) . $pass);
557
558             // RFC 4959 (SASL-IR): save one round trip
559             if ($this->getCapability('SASL-IR')) {
560                 list($result, $line) = $this->execute("AUTHENTICATE PLAIN", array($reply),
561                     self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY);
562             }
563             else {
564                 $this->putLine($this->nextTag() . " AUTHENTICATE PLAIN");
565                 $line = trim($this->readReply());
566
567                 if ($line[0] != '+') {
568                     return $this->parseResult($line);
569                 }
570
571                 // send result, get reply and process it
572                 $this->putLine($reply);
573                 $line = $this->readReply();
574                 $result = $this->parseResult($line);
575             }
576         }
577
578         if ($result == self::ERROR_OK) {
579             // optional CAPABILITY response
580             if ($line && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
581                 $this->parseCapability($matches[1], true);
582             }
583             return $this->fp;
584         }
585         else {
586             $this->setError($result, "AUTHENTICATE $type: $line");
587         }
588
589         return $result;
590     }
591
592     /**
593      * LOGIN Authentication
594      *
595      * @param string $user
596      * @param string $pass
597      *
598      * @return resource Connection resourse on success, error code on error
599      */
600     function login($user, $password)
601     {
602         list($code, $response) = $this->execute('LOGIN', array(
603             $this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY);
604
605         // re-set capabilities list if untagged CAPABILITY response provided
606         if (preg_match('/\* CAPABILITY (.+)/i', $response, $matches)) {
607             $this->parseCapability($matches[1], true);
608         }
609
610         if ($code == self::ERROR_OK) {
611             return $this->fp;
612         }
613
614         return $code;
615     }
616
617     /**
618      * Gets the delimiter
619      *
620      * @return string The delimiter
621      */
622     function getHierarchyDelimiter()
623     {
624         if ($this->prefs['delimiter']) {
625             return $this->prefs['delimiter'];
626         }
627
628         // try (LIST "" ""), should return delimiter (RFC2060 Sec 6.3.8)
629         list($code, $response) = $this->execute('LIST',
630             array($this->escape(''), $this->escape('')));
631
632         if ($code == self::ERROR_OK) {
633             $args = $this->tokenizeResponse($response, 4);
634             $delimiter = $args[3];
635
636             if (strlen($delimiter) > 0) {
637                 return ($this->prefs['delimiter'] = $delimiter);
638             }
639         }
640
641         return NULL;
642     }
643
644     /**
645      * NAMESPACE handler (RFC 2342)
646      *
647      * @return array Namespace data hash (personal, other, shared)
648      */
649     function getNamespace()
650     {
651         if (array_key_exists('namespace', $this->prefs)) {
652             return $this->prefs['namespace'];
653         }
654
655         if (!$this->getCapability('NAMESPACE')) {
656             return self::ERROR_BAD;
657         }
658
659         list($code, $response) = $this->execute('NAMESPACE');
660
661         if ($code == self::ERROR_OK && preg_match('/^\* NAMESPACE /', $response)) {
662             $data = $this->tokenizeResponse(substr($response, 11));
663         }
664
665         if (!is_array($data)) {
666             return $code;
667         }
668
669         $this->prefs['namespace'] = array(
670             'personal' => $data[0],
671             'other'    => $data[1],
672             'shared'   => $data[2],
673         );
674
675         return $this->prefs['namespace'];
676     }
677
678     function connect($host, $user, $password, $options=null)
679     {
680         // set options
681         if (is_array($options)) {
682             $this->prefs = $options;
683         }
684         // set auth method
685         if (!empty($this->prefs['auth_method'])) {
686             $auth_method = strtoupper($this->prefs['auth_method']);
687         } else {
688             $auth_method = 'CHECK';
689         }
690
691         $result = false;
692
693         // initialize connection
694         $this->error    = '';
695         $this->errornum = self::ERROR_OK;
696         $this->selected = '';
697         $this->user     = $user;
698         $this->host     = $host;
699         $this->logged   = false;
700
701         // check input
702         if (empty($host)) {
703             $this->setError(self::ERROR_BAD, "Empty host");
704             return false;
705         }
706         if (empty($user)) {
707             $this->setError(self::ERROR_NO, "Empty user");
708             return false;
709         }
710         if (empty($password)) {
711             $this->setError(self::ERROR_NO, "Empty password");
712             return false;
713         }
714
715         if (!$this->prefs['port']) {
716             $this->prefs['port'] = 143;
717         }
718         // check for SSL
719         if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') {
720             $host = $this->prefs['ssl_mode'] . '://' . $host;
721         }
722
723         if ($this->prefs['timeout'] <= 0) {
724             $this->prefs['timeout'] = ini_get('default_socket_timeout');
725         }
726
727         // Connect
728         $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']);
729
730         if (!$this->fp) {
731             $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s", $host, $this->prefs['port'], $errstr));
732             return false;
733         }
734
735         if ($this->prefs['timeout'] > 0)
736             stream_set_timeout($this->fp, $this->prefs['timeout']);
737
738         $line = trim(fgets($this->fp, 8192));
739
740         if ($this->_debug && $line) {
741             $this->debug('S: '. $line);
742         }
743
744         // Connected to wrong port or connection error?
745         if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) {
746             if ($line)
747                 $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line);
748             else
749                 $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']);
750
751             $this->setError(self::ERROR_BAD, $error);
752             $this->closeConnection();
753             return false;
754         }
755
756         // RFC3501 [7.1] optional CAPABILITY response
757         if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
758             $this->parseCapability($matches[1], true);
759         }
760
761         // TLS connection
762         if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
763             if (version_compare(PHP_VERSION, '5.1.0', '>=')) {
764                 $res = $this->execute('STARTTLS');
765
766                 if ($res[0] != self::ERROR_OK) {
767                     $this->closeConnection();
768                     return false;
769                 }
770
771                 if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
772                     $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
773                     $this->closeConnection();
774                     return false;
775                 }
776
777                 // Now we're secure, capabilities need to be reread
778                 $this->clearCapability();
779             }
780
781             // Use best (for security) supported authentication method
782             foreach (array('DIGEST-MD5', 'CRAM-MD5', 'CRAM_MD5', 'PLAIN', 'LOGIN') as $auth_method) {
783                 if (in_array($auth_method, $auth_methods)) {
784                     break;
785                 }
786             }
787         }
788
789         // Send ID info
790         if (!empty($this->prefs['ident']) && $this->getCapability('ID')) {
791             $this->id($this->prefs['ident']);
792         }
793
794         $auth_methods = array();
795         $result       = null;
796
797         // check for supported auth methods
798         if ($auth_method == 'CHECK') {
799             if ($auth_caps = $this->getCapability('AUTH')) {
800                 $auth_methods = $auth_caps;
801             }
802             // RFC 2595 (LOGINDISABLED) LOGIN disabled when connection is not secure
803             $login_disabled = $this->getCapability('LOGINDISABLED');
804             if (($key = array_search('LOGIN', $auth_methods)) !== false) {
805                 if ($login_disabled) {
806                     unset($auth_methods[$key]);
807                 }
808             }
809             else if (!$login_disabled) {
810                 $auth_methods[] = 'LOGIN';
811             }
812 \r
813             // Use best (for security) supported authentication method\r
814             foreach (array('DIGEST-MD5', 'CRAM-MD5', 'CRAM_MD5', 'PLAIN', 'LOGIN') as $auth_method) {\r
815                 if (in_array($auth_method, $auth_methods)) {\r
816                     break;\r
817                 }\r
818             }\r
819         }
820         else {
821             // Prevent from sending credentials in plain text when connection is not secure
822             if ($auth_method == 'LOGIN' && $this->getCapability('LOGINDISABLED')) {
823                 $this->setError(self::ERROR_BAD, "Login disabled by IMAP server");
824                 $this->closeConnection();
825                 return false;
826             }
827             // replace AUTH with CRAM-MD5 for backward compat.
828             if ($auth_method == 'AUTH') {
829                 $auth_method = 'CRAM-MD5';
830             }
831         }
832
833         // pre-login capabilities can be not complete
834         $this->capability_readed = false;
835
836         // Authenticate
837         switch ($auth_method) {
838             case 'CRAM_MD5':
839                 $auth_method = 'CRAM-MD5';
840             case 'CRAM-MD5':
841             case 'DIGEST-MD5':
842             case 'PLAIN':
843                 $result = $this->authenticate($user, $password, $auth_method);
844                 break;
845             case 'LOGIN':
846                 $result = $this->login($user, $password);
847                 break;
848             default:
849                 $this->setError(self::ERROR_BAD, "Configuration error. Unknown auth method: $auth_method");
850         }
851
852         // Connected and authenticated
853         if (is_resource($result)) {
854             if ($this->prefs['force_caps']) {
855                 $this->clearCapability();
856             }
857             $this->logged = true;
858
859             return true;
860         }
861
862         $this->closeConnection();
863
864         return false;
865     }
866
867     function connected()
868     {
869         return ($this->fp && $this->logged) ? true : false;
870     }
871
872     function closeConnection()
873     {
874         if ($this->putLine($this->nextTag() . ' LOGOUT')) {
875             $this->readReply();
876         }
877
878         $this->closeSocket();
879     }
880
881     /**
882      * Executes SELECT command (if mailbox is already not in selected state)
883      *
884      * @param string $mailbox Mailbox name
885      *
886      * @return boolean True on success, false on error
887      * @access public
888      */
889     function select($mailbox)
890     {
891         if (!strlen($mailbox)) {
892             return false;
893         }
894
895         if ($this->selected == $mailbox) {
896             return true;
897         }
898 /*
899     Temporary commented out because Courier returns \Noselect for INBOX
900     Requires more investigation
901
902         if (is_array($this->data['LIST']) && is_array($opts = $this->data['LIST'][$mailbox])) {
903             if (in_array('\\Noselect', $opts)) {
904                 return false;
905             }
906         }
907 */
908         list($code, $response) = $this->execute('SELECT', array($this->escape($mailbox)));
909
910         if ($code == self::ERROR_OK) {
911             $response = explode("\r\n", $response);
912             foreach ($response as $line) {
913                 if (preg_match('/^\* ([0-9]+) (EXISTS|RECENT)$/i', $line, $m)) {
914                     $this->data[strtoupper($m[2])] = (int) $m[1];
915                 }
916                 else if (preg_match('/^\* OK \[(UIDNEXT|UIDVALIDITY|UNSEEN) ([0-9]+)\]/i', $line, $match)) {
917                     $this->data[strtoupper($match[1])] = (int) $match[2];
918                 }
919                 else if (preg_match('/^\* OK \[PERMANENTFLAGS \(([^\)]+)\)\]/iU', $line, $match)) {
920                     $this->data['PERMANENTFLAGS'] = explode(' ', $match[1]);
921                 }
922             }
923
924             $this->data['READ-WRITE'] = $this->resultcode != 'READ-ONLY';
925
926             $this->selected = $mailbox;
927             return true;
928         }
929
930         return false;
931     }
932
933     /**
934      * Executes STATUS command
935      *
936      * @param string $mailbox Mailbox name
937      * @param array  $items   Additional requested item names. By default
938      *                        MESSAGES and UNSEEN are requested. Other defined
939      *                        in RFC3501: UIDNEXT, UIDVALIDITY, RECENT
940      *
941      * @return array Status item-value hash
942      * @access public
943      * @since 0.5-beta
944      */
945     function status($mailbox, $items=array())
946     {
947         if (!strlen($mailbox)) {
948             return false;
949         }
950
951         if (!in_array('MESSAGES', $items)) {
952             $items[] = 'MESSAGES';
953         }
954         if (!in_array('UNSEEN', $items)) {
955             $items[] = 'UNSEEN';
956         }
957
958         list($code, $response) = $this->execute('STATUS', array($this->escape($mailbox),
959             '(' . implode(' ', (array) $items) . ')'));
960
961         if ($code == self::ERROR_OK && preg_match('/\* STATUS /i', $response)) {
962             $result   = array();
963             $response = substr($response, 9); // remove prefix "* STATUS "
964
965             list($mbox, $items) = $this->tokenizeResponse($response, 2);
966
967             // Fix for #1487859. Some buggy server returns not quoted
968             // folder name with spaces. Let's try to handle this situation
969             if (!is_array($items) && ($pos = strpos($response, '(')) !== false) {
970                 $response = substr($response, $pos);
971                 $items = $this->tokenizeResponse($response, 1);
972                 if (!is_array($items)) {
973                     return $result;
974                 }
975             }
976
977             for ($i=0, $len=count($items); $i<$len; $i += 2) {
978                 $result[$items[$i]] = (int) $items[$i+1];
979             }
980
981             $this->data['STATUS:'.$mailbox] = $result;
982
983             return $result;
984         }
985
986         return false;
987     }
988
989     /**
990      * Executes EXPUNGE command
991      *
992      * @param string $mailbox  Mailbox name
993      * @param string $messages Message UIDs to expunge
994      *
995      * @return boolean True on success, False on error
996      * @access public
997      */
998     function expunge($mailbox, $messages=NULL)
999     {
1000         if (!$this->select($mailbox)) {
1001             return false;
1002         }
1003
1004         if (!$this->data['READ-WRITE']) {
1005             $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'EXPUNGE');
1006             return false;
1007         }
1008
1009         // Clear internal status cache
1010         unset($this->data['STATUS:'.$mailbox]);
1011
1012         if ($messages)
1013             $result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE);
1014         else
1015             $result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE);
1016
1017         if ($result == self::ERROR_OK) {
1018             $this->selected = ''; // state has changed, need to reselect
1019             return true;
1020         }
1021
1022         return false;
1023     }
1024
1025     /**
1026      * Executes CLOSE command
1027      *
1028      * @return boolean True on success, False on error
1029      * @access public
1030      * @since 0.5
1031      */
1032     function close()
1033     {
1034         $result = $this->execute('CLOSE', NULL, self::COMMAND_NORESPONSE);
1035
1036         if ($result == self::ERROR_OK) {
1037             $this->selected = '';
1038             return true;
1039         }
1040
1041         return false;
1042     }
1043
1044     /**
1045      * Executes SUBSCRIBE command
1046      *
1047      * @param string $mailbox Mailbox name
1048      *
1049      * @return boolean True on success, False on error
1050      * @access public
1051      */
1052     function subscribe($mailbox)
1053     {
1054         $result = $this->execute('SUBSCRIBE', array($this->escape($mailbox)),
1055             self::COMMAND_NORESPONSE);
1056
1057         return ($result == self::ERROR_OK);
1058     }
1059
1060     /**
1061      * Executes UNSUBSCRIBE command
1062      *
1063      * @param string $mailbox Mailbox name
1064      *
1065      * @return boolean True on success, False on error
1066      * @access public
1067      */
1068     function unsubscribe($mailbox)
1069     {
1070         $result = $this->execute('UNSUBSCRIBE', array($this->escape($mailbox)),
1071             self::COMMAND_NORESPONSE);
1072
1073         return ($result == self::ERROR_OK);
1074     }
1075
1076     /**
1077      * Executes DELETE command
1078      *
1079      * @param string $mailbox Mailbox name
1080      *
1081      * @return boolean True on success, False on error
1082      * @access public
1083      */
1084     function deleteFolder($mailbox)
1085     {
1086         $result = $this->execute('DELETE', array($this->escape($mailbox)),
1087             self::COMMAND_NORESPONSE);
1088
1089         return ($result == self::ERROR_OK);
1090     }
1091
1092     /**
1093      * Removes all messages in a folder
1094      *
1095      * @param string $mailbox Mailbox name
1096      *
1097      * @return boolean True on success, False on error
1098      * @access public
1099      */
1100     function clearFolder($mailbox)
1101     {
1102         $num_in_trash = $this->countMessages($mailbox);
1103         if ($num_in_trash > 0) {
1104             $res = $this->delete($mailbox, '1:*');
1105         }
1106
1107         if ($res) {
1108             if ($this->selected == $mailbox)
1109                 $res = $this->close();
1110             else
1111                 $res = $this->expunge($mailbox);
1112         }
1113
1114         return $res;
1115     }
1116
1117     /**
1118      * Returns count of all messages in a folder
1119      *
1120      * @param string $mailbox Mailbox name
1121      *
1122      * @return int Number of messages, False on error
1123      * @access public
1124      */
1125     function countMessages($mailbox, $refresh = false)
1126     {
1127         if ($refresh) {
1128             $this->selected = '';
1129         }
1130
1131         if ($this->selected == $mailbox) {
1132             return $this->data['EXISTS'];
1133         }
1134
1135         // Check internal cache
1136         $cache = $this->data['STATUS:'.$mailbox];
1137         if (!empty($cache) && isset($cache['MESSAGES'])) {
1138             return (int) $cache['MESSAGES'];
1139         }
1140
1141         // Try STATUS (should be faster than SELECT)
1142         $counts = $this->status($mailbox);
1143         if (is_array($counts)) {
1144             return (int) $counts['MESSAGES'];
1145         }
1146
1147         return false;
1148     }
1149
1150     /**
1151      * Returns count of messages with \Recent flag in a folder
1152      *
1153      * @param string $mailbox Mailbox name
1154      *
1155      * @return int Number of messages, False on error
1156      * @access public
1157      */
1158     function countRecent($mailbox)
1159     {
1160         if (!strlen($mailbox)) {
1161             $mailbox = 'INBOX';
1162         }
1163
1164         $this->select($mailbox);
1165
1166         if ($this->selected == $mailbox) {
1167             return $this->data['RECENT'];
1168         }
1169
1170         return false;
1171     }
1172
1173     /**
1174      * Returns count of messages without \Seen flag in a specified folder
1175      *
1176      * @param string $mailbox Mailbox name
1177      *
1178      * @return int Number of messages, False on error
1179      * @access public
1180      */
1181     function countUnseen($mailbox)
1182     {
1183         // Check internal cache
1184         $cache = $this->data['STATUS:'.$mailbox];
1185         if (!empty($cache) && isset($cache['UNSEEN'])) {
1186             return (int) $cache['UNSEEN'];
1187         }
1188
1189         // Try STATUS (should be faster than SELECT+SEARCH)
1190         $counts = $this->status($mailbox);
1191         if (is_array($counts)) {
1192             return (int) $counts['UNSEEN'];
1193         }
1194
1195         // Invoke SEARCH as a fallback
1196         $index = $this->search($mailbox, 'ALL UNSEEN', false, array('COUNT'));
1197         if (is_array($index)) {
1198             return (int) $index['COUNT'];
1199         }
1200
1201         return false;
1202     }
1203
1204     /**
1205      * Executes ID command (RFC2971)
1206      *
1207      * @param array $items Client identification information key/value hash
1208      *
1209      * @return array Server identification information key/value hash
1210      * @access public
1211      * @since 0.6
1212      */
1213     function id($items=array())
1214     {
1215         if (is_array($items) && !empty($items)) {
1216             foreach ($items as $key => $value) {
1217                 $args[] = $this->escape($key);
1218                 $args[] = $this->escape($value);
1219             }
1220         }
1221
1222         list($code, $response) = $this->execute('ID', array(
1223             !empty($args) ? '(' . implode(' ', (array) $args) . ')' : $this->escape(null)
1224         ));
1225
1226
1227         if ($code == self::ERROR_OK && preg_match('/\* ID /i', $response)) {
1228             $response = substr($response, 5); // remove prefix "* ID "
1229             $items    = $this->tokenizeResponse($response);
1230             $result   = null;
1231
1232             for ($i=0, $len=count($items); $i<$len; $i += 2) {
1233                 $result[$items[$i]] = $items[$i+1];
1234             }
1235
1236             return $result;
1237         }
1238
1239         return false;
1240     }
1241
1242     function sort($mailbox, $field, $add='', $is_uid=FALSE, $encoding = 'US-ASCII')
1243     {
1244         $field = strtoupper($field);
1245         if ($field == 'INTERNALDATE') {
1246             $field = 'ARRIVAL';
1247         }
1248
1249         $fields = array('ARRIVAL' => 1,'CC' => 1,'DATE' => 1,
1250             'FROM' => 1, 'SIZE' => 1, 'SUBJECT' => 1, 'TO' => 1);
1251
1252         if (!$fields[$field]) {
1253             return false;
1254         }
1255
1256         if (!$this->select($mailbox)) {
1257             return false;
1258         }
1259
1260         // message IDs
1261         if (!empty($add))
1262             $add = $this->compressMessageSet($add);
1263
1264         list($code, $response) = $this->execute($is_uid ? 'UID SORT' : 'SORT',
1265             array("($field)", $encoding, 'ALL' . (!empty($add) ? ' '.$add : '')));
1266
1267         if ($code == self::ERROR_OK) {
1268             // remove prefix and unilateral untagged server responses
1269             $response = substr($response, stripos($response, '* SORT') + 7);
1270             if ($pos = strpos($response, '*')) {
1271                 $response = substr($response, 0, $pos);
1272             }
1273             return preg_split('/[\s\r\n]+/', $response, -1, PREG_SPLIT_NO_EMPTY);
1274         }
1275
1276         return false;
1277     }
1278
1279     function fetchHeaderIndex($mailbox, $message_set, $index_field='', $skip_deleted=true, $uidfetch=false)
1280     {
1281         if (is_array($message_set)) {
1282             if (!($message_set = $this->compressMessageSet($message_set)))
1283                 return false;
1284         } else {
1285             list($from_idx, $to_idx) = explode(':', $message_set);
1286             if (empty($message_set) ||
1287                 (isset($to_idx) && $to_idx != '*' && (int)$from_idx > (int)$to_idx)) {
1288                 return false;
1289             }
1290         }
1291
1292         $index_field = empty($index_field) ? 'DATE' : strtoupper($index_field);
1293
1294         $fields_a['DATE']         = 1;
1295         $fields_a['INTERNALDATE'] = 4;
1296         $fields_a['ARRIVAL']      = 4;
1297         $fields_a['FROM']         = 1;
1298         $fields_a['REPLY-TO']     = 1;
1299         $fields_a['SENDER']       = 1;
1300         $fields_a['TO']           = 1;
1301         $fields_a['CC']           = 1;
1302         $fields_a['SUBJECT']      = 1;
1303         $fields_a['UID']          = 2;
1304         $fields_a['SIZE']         = 2;
1305         $fields_a['SEEN']         = 3;
1306         $fields_a['RECENT']       = 3;
1307         $fields_a['DELETED']      = 3;
1308
1309         if (!($mode = $fields_a[$index_field])) {
1310             return false;
1311         }
1312
1313         /*  Do "SELECT" command */
1314         if (!$this->select($mailbox)) {
1315             return false;
1316         }
1317
1318         // build FETCH command string
1319         $key     = $this->nextTag();
1320         $cmd     = $uidfetch ? 'UID FETCH' : 'FETCH';
1321         $deleted = $skip_deleted ? ' FLAGS' : '';
1322
1323         if ($mode == 1 && $index_field == 'DATE')
1324             $request = " $cmd $message_set (INTERNALDATE BODY.PEEK[HEADER.FIELDS (DATE)]$deleted)";
1325         else if ($mode == 1)
1326             $request = " $cmd $message_set (BODY.PEEK[HEADER.FIELDS ($index_field)]$deleted)";
1327         else if ($mode == 2) {
1328             if ($index_field == 'SIZE')
1329                 $request = " $cmd $message_set (RFC822.SIZE$deleted)";
1330             else
1331                 $request = " $cmd $message_set ($index_field$deleted)";
1332         } else if ($mode == 3)
1333             $request = " $cmd $message_set (FLAGS)";
1334         else // 4
1335             $request = " $cmd $message_set (INTERNALDATE$deleted)";
1336
1337         $request = $key . $request;
1338
1339         if (!$this->putLine($request)) {
1340             $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
1341             return false;
1342         }
1343
1344         $result = array();
1345
1346         do {
1347             $line = rtrim($this->readLine(200));
1348             $line = $this->multLine($line);
1349
1350             if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
1351                 $id     = $m[1];
1352                 $flags  = NULL;
1353
1354                 if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
1355                     $flags = explode(' ', strtoupper($matches[1]));
1356                     if (in_array('\\DELETED', $flags)) {
1357                         $deleted[$id] = $id;
1358                         continue;
1359                     }
1360                 }
1361
1362                 if ($mode == 1 && $index_field == 'DATE') {
1363                     if (preg_match('/BODY\[HEADER\.FIELDS \("*DATE"*\)\] (.*)/', $line, $matches)) {
1364                         $value = preg_replace(array('/^"*[a-z]+:/i'), '', $matches[1]);
1365                         $value = trim($value);
1366                         $result[$id] = $this->strToTime($value);
1367                     }
1368                     // non-existent/empty Date: header, use INTERNALDATE
1369                     if (empty($result[$id])) {
1370                         if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches))
1371                             $result[$id] = $this->strToTime($matches[1]);
1372                         else
1373                             $result[$id] = 0;
1374                     }
1375                 } else if ($mode == 1) {
1376                     if (preg_match('/BODY\[HEADER\.FIELDS \("?(FROM|REPLY-TO|SENDER|TO|SUBJECT)"?\)\] (.*)/', $line, $matches)) {
1377                         $value = preg_replace(array('/^"*[a-z]+:/i', '/\s+$/sm'), array('', ''), $matches[2]);
1378                         $result[$id] = trim($value);
1379                     } else {
1380                         $result[$id] = '';
1381                     }
1382                 } else if ($mode == 2) {
1383                     if (preg_match('/(UID|RFC822\.SIZE) ([0-9]+)/', $line, $matches)) {
1384                         $result[$id] = trim($matches[2]);
1385                     } else {
1386                         $result[$id] = 0;
1387                     }
1388                 } else if ($mode == 3) {
1389                     if (!$flags && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
1390                         $flags = explode(' ', $matches[1]);
1391                     }
1392                     $result[$id] = in_array('\\'.$index_field, $flags) ? 1 : 0;
1393                 } else if ($mode == 4) {
1394                     if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) {
1395                         $result[$id] = $this->strToTime($matches[1]);
1396                     } else {
1397                         $result[$id] = 0;
1398                     }
1399                 }
1400             }
1401         } while (!$this->startsWith($line, $key, true, true));
1402
1403         return $result;
1404     }
1405
1406     static function compressMessageSet($messages, $force=false)
1407     {
1408         // given a comma delimited list of independent mid's,
1409         // compresses by grouping sequences together
1410
1411         if (!is_array($messages)) {
1412             // if less than 255 bytes long, let's not bother
1413             if (!$force && strlen($messages)<255) {
1414                 return $messages;
1415            }
1416
1417             // see if it's already been compressed
1418             if (strpos($messages, ':') !== false) {
1419                 return $messages;
1420             }
1421
1422             // separate, then sort
1423             $messages = explode(',', $messages);
1424         }
1425
1426         sort($messages);
1427
1428         $result = array();
1429         $start  = $prev = $messages[0];
1430
1431         foreach ($messages as $id) {
1432             $incr = $id - $prev;
1433             if ($incr > 1) { // found a gap
1434                 if ($start == $prev) {
1435                     $result[] = $prev; // push single id
1436                 } else {
1437                     $result[] = $start . ':' . $prev; // push sequence as start_id:end_id
1438                 }
1439                 $start = $id; // start of new sequence
1440             }
1441             $prev = $id;
1442         }
1443
1444         // handle the last sequence/id
1445         if ($start == $prev) {
1446             $result[] = $prev;
1447         } else {
1448             $result[] = $start.':'.$prev;
1449         }
1450
1451         // return as comma separated string
1452         return implode(',', $result);
1453     }
1454
1455     static function uncompressMessageSet($messages)
1456     {
1457         $result   = array();
1458         $messages = explode(',', $messages);
1459
1460         foreach ($messages as $part) {
1461             $items = explode(':', $part);
1462             $max   = max($items[0], $items[1]);
1463
1464             for ($x=$items[0]; $x<=$max; $x++) {
1465                 $result[] = $x;
1466             }
1467         }
1468
1469         return $result;
1470     }
1471
1472     /**
1473      * Returns message sequence identifier
1474      *
1475      * @param string $mailbox Mailbox name
1476      * @param int    $uid     Message unique identifier (UID)
1477      *
1478      * @return int Message sequence identifier
1479      * @access public
1480      */
1481     function UID2ID($mailbox, $uid)
1482     {
1483         if ($uid > 0) {
1484             $id_a = $this->search($mailbox, "UID $uid");
1485             if (is_array($id_a) && count($id_a) == 1) {
1486                 return (int) $id_a[0];
1487             }
1488         }
1489         return null;
1490     }
1491
1492     /**
1493      * Returns message unique identifier (UID)
1494      *
1495      * @param string $mailbox Mailbox name
1496      * @param int    $uid     Message sequence identifier
1497      *
1498      * @return int Message unique identifier
1499      * @access public
1500      */
1501     function ID2UID($mailbox, $id)
1502     {
1503         if (empty($id) || $id < 0) {
1504             return      null;
1505         }
1506
1507         if (!$this->select($mailbox)) {
1508             return null;
1509         }
1510
1511         list($code, $response) = $this->execute('FETCH', array($id, '(UID)'));
1512
1513         if ($code == self::ERROR_OK && preg_match("/^\* $id FETCH \(UID (.*)\)/i", $response, $m)) {
1514             return (int) $m[1];
1515         }
1516
1517         return null;
1518     }
1519
1520     function fetchUIDs($mailbox, $message_set=null)
1521     {
1522         if (is_array($message_set))
1523             $message_set = join(',', $message_set);
1524         else if (empty($message_set))
1525             $message_set = '1:*';
1526
1527         return $this->fetchHeaderIndex($mailbox, $message_set, 'UID', false);
1528     }
1529
1530     function fetchHeaders($mailbox, $message_set, $uidfetch=false, $bodystr=false, $add='')
1531     {
1532         $result = array();
1533
1534         if (!$this->select($mailbox)) {
1535             return false;
1536         }
1537
1538         $message_set = $this->compressMessageSet($message_set);
1539
1540         if ($add)
1541             $add = ' '.trim($add);
1542
1543         /* FETCH uid, size, flags and headers */
1544         $key      = $this->nextTag();
1545         $request  = $key . ($uidfetch ? ' UID' : '') . " FETCH $message_set ";
1546         $request .= "(UID RFC822.SIZE FLAGS INTERNALDATE ";
1547         if ($bodystr)
1548             $request .= "BODYSTRUCTURE ";
1549         $request .= "BODY.PEEK[HEADER.FIELDS (DATE FROM TO SUBJECT CONTENT-TYPE ";
1550         $request .= "LIST-POST DISPOSITION-NOTIFICATION-TO".$add.")])";
1551
1552         if (!$this->putLine($request)) {
1553             $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
1554             return false;
1555         }
1556         do {
1557             $line = $this->readLine(4096);
1558             $line = $this->multLine($line);
1559
1560             if (!$line)
1561                 break;
1562
1563             if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
1564                 $id = intval($m[1]);
1565
1566                 $result[$id]            = new rcube_mail_header;
1567                 $result[$id]->id        = $id;
1568                 $result[$id]->subject   = '';
1569                 $result[$id]->messageID = 'mid:' . $id;
1570
1571                 $lines = array();
1572                 $ln = 0;
1573
1574                 // Sample reply line:
1575                 // * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen)
1576                 // INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...)
1577                 // BODY[HEADER.FIELDS ...
1578
1579                 if (preg_match('/^\* [0-9]+ FETCH \((.*) BODY/sU', $line, $matches)) {
1580                     $str = $matches[1];
1581
1582                     while (list($name, $value) = $this->tokenizeResponse($str, 2)) {
1583                         if ($name == 'UID') {
1584                             $result[$id]->uid = intval($value);
1585                         }
1586                         else if ($name == 'RFC822.SIZE') {
1587                             $result[$id]->size = intval($value);
1588                         }
1589                         else if ($name == 'INTERNALDATE') {
1590                             $result[$id]->internaldate = $value;
1591                             $result[$id]->date         = $value;
1592                             $result[$id]->timestamp    = $this->StrToTime($value);
1593                         }
1594                         else if ($name == 'FLAGS') {
1595                             $flags_a = $value;
1596                         }
1597                     }
1598
1599                     // BODYSTRUCTURE
1600                     if ($bodystr) {
1601                         while (!preg_match('/ BODYSTRUCTURE (.*) BODY\[HEADER.FIELDS/sU', $line, $m)) {
1602                             $line2 = $this->readLine(1024);
1603                             $line .= $this->multLine($line2, true);
1604                         }
1605                         $result[$id]->body_structure = $m[1];
1606                     }
1607
1608                     // the rest of the result
1609                     if (preg_match('/ BODY\[HEADER.FIELDS \(.*?\)\]\s*(.*)$/s', $line, $m)) {
1610                         $reslines = explode("\n", trim($m[1], '"'));
1611                         // re-parse (see below)
1612                         foreach ($reslines as $resln) {
1613                             if (ord($resln[0])<=32) {
1614                                 $lines[$ln] .= (empty($lines[$ln])?'':"\n").trim($resln);
1615                             } else {
1616                                 $lines[++$ln] = trim($resln);
1617                             }
1618                         }
1619                     }
1620                 }
1621
1622                 // Start parsing headers.  The problem is, some header "lines" take up multiple lines.
1623                 // So, we'll read ahead, and if the one we're reading now is a valid header, we'll
1624                 // process the previous line.  Otherwise, we'll keep adding the strings until we come
1625                 // to the next valid header line.
1626
1627                 do {
1628                     $line = rtrim($this->readLine(300), "\r\n");
1629
1630                     // The preg_match below works around communigate imap, which outputs " UID <number>)".
1631                     // Without this, the while statement continues on and gets the "FH0 OK completed" message.
1632                     // If this loop gets the ending message, then the outer loop does not receive it from radline on line 1249.
1633                     // This in causes the if statement on line 1278 to never be true, which causes the headers to end up missing
1634                     // If the if statement was changed to pick up the fh0 from this loop, then it causes the outer loop to spin
1635                     // An alternative might be:
1636                     // if (!preg_match("/:/",$line) && preg_match("/\)$/",$line)) break;
1637                     // however, unsure how well this would work with all imap clients.
1638                     if (preg_match("/^\s*UID [0-9]+\)$/", $line)) {
1639                         break;
1640                     }
1641
1642                     // handle FLAGS reply after headers (AOL, Zimbra?)
1643                     if (preg_match('/\s+FLAGS \((.*)\)\)$/', $line, $matches)) {
1644                         $flags_a = $this->tokenizeResponse($matches[1]);
1645                         break;
1646                     }
1647
1648                     if (ord($line[0])<=32) {
1649                         $lines[$ln] .= (empty($lines[$ln])?'':"\n").trim($line);
1650                     } else {
1651                         $lines[++$ln] = trim($line);
1652                     }
1653                 // patch from "Maksim Rubis" <siburny@hotmail.com>
1654                 } while ($line[0] != ')' && !$this->startsWith($line, $key, true));
1655
1656                 if (strncmp($line, $key, strlen($key))) {
1657                     // process header, fill rcube_mail_header obj.
1658                     // initialize
1659                     if (is_array($headers)) {
1660                         reset($headers);
1661                         while (list($k, $bar) = each($headers)) {
1662                             $headers[$k] = '';
1663                         }
1664                     }
1665
1666                     // create array with header field:data
1667                     while (list($lines_key, $str) = each($lines)) {
1668                         list($field, $string) = explode(':', $str, 2);
1669
1670                         $field  = strtolower($field);
1671                         $string = preg_replace('/\n[\t\s]*/', ' ', trim($string));
1672
1673                         switch ($field) {
1674                         case 'date';
1675                             $result[$id]->date = $string;
1676                             $result[$id]->timestamp = $this->strToTime($string);
1677                             break;
1678                         case 'from':
1679                             $result[$id]->from = $string;
1680                             break;
1681                         case 'to':
1682                             $result[$id]->to = preg_replace('/undisclosed-recipients:[;,]*/', '', $string);
1683                             break;
1684                         case 'subject':
1685                             $result[$id]->subject = $string;
1686                             break;
1687                         case 'reply-to':
1688                             $result[$id]->replyto = $string;
1689                             break;
1690                         case 'cc':
1691                             $result[$id]->cc = $string;
1692                             break;
1693                         case 'bcc':
1694                             $result[$id]->bcc = $string;
1695                             break;
1696                         case 'content-transfer-encoding':
1697                             $result[$id]->encoding = $string;
1698                         break;
1699                         case 'content-type':
1700                             $ctype_parts = preg_split('/[; ]/', $string);
1701                             $result[$id]->ctype = strtolower(array_shift($ctype_parts));
1702                             if (preg_match('/charset\s*=\s*"?([a-z0-9\-\.\_]+)"?/i', $string, $regs)) {
1703                                 $result[$id]->charset = $regs[1];
1704                             }
1705                             break;
1706                         case 'in-reply-to':
1707                             $result[$id]->in_reply_to = str_replace(array("\n", '<', '>'), '', $string);
1708                             break;
1709                         case 'references':
1710                             $result[$id]->references = $string;
1711                             break;
1712                         case 'return-receipt-to':
1713                         case 'disposition-notification-to':
1714                         case 'x-confirm-reading-to':
1715                             $result[$id]->mdn_to = $string;
1716                             break;
1717                         case 'message-id':
1718                             $result[$id]->messageID = $string;
1719                             break;
1720                         case 'x-priority':
1721                             if (preg_match('/^(\d+)/', $string, $matches)) {
1722                                 $result[$id]->priority = intval($matches[1]);
1723                             }
1724                             break;
1725                         default:
1726                             if (strlen($field) > 2) {
1727                                 $result[$id]->others[$field] = $string;
1728                             }
1729                             break;
1730                         } // end switch ()
1731                     } // end while ()
1732                 }
1733
1734                 // process flags
1735                 if (!empty($flags_a)) {
1736                     foreach ($flags_a as $flag) {
1737                         $flag = str_replace('\\', '', $flag);
1738                         $result[$id]->flags[] = $flag;
1739
1740                         switch (strtoupper($flag)) {
1741                         case 'SEEN':
1742                             $result[$id]->seen = true;
1743                             break;
1744                         case 'DELETED':
1745                             $result[$id]->deleted = true;
1746                             break;
1747                         case 'ANSWERED':
1748                             $result[$id]->answered = true;
1749                             break;
1750                         case '$FORWARDED':
1751                             $result[$id]->forwarded = true;
1752                             break;
1753                         case '$MDNSENT':
1754                             $result[$id]->mdn_sent = true;
1755                             break;
1756                         case 'FLAGGED':
1757                             $result[$id]->flagged = true;
1758                             break;
1759                         }
1760                     }
1761                 }
1762             }
1763         } while (!$this->startsWith($line, $key, true));
1764
1765         return $result;
1766     }
1767
1768     function fetchHeader($mailbox, $id, $uidfetch=false, $bodystr=false, $add='')
1769     {
1770         $a  = $this->fetchHeaders($mailbox, $id, $uidfetch, $bodystr, $add);
1771         if (is_array($a)) {
1772             return array_shift($a);
1773         }
1774         return false;
1775     }
1776
1777     function sortHeaders($a, $field, $flag)
1778     {
1779         if (empty($field)) {
1780             $field = 'uid';
1781         }
1782         else {
1783             $field = strtolower($field);
1784         }
1785
1786         if ($field == 'date' || $field == 'internaldate') {
1787             $field = 'timestamp';
1788         }
1789
1790         if (empty($flag)) {
1791             $flag = 'ASC';
1792         } else {
1793             $flag = strtoupper($flag);
1794         }
1795
1796         $c = count($a);
1797         if ($c > 0) {
1798             // Strategy:
1799             // First, we'll create an "index" array.
1800             // Then, we'll use sort() on that array,
1801             // and use that to sort the main array.
1802
1803             // create "index" array
1804             $index = array();
1805             reset($a);
1806             while (list($key, $val) = each($a)) {
1807                 if ($field == 'timestamp') {
1808                     $data = $this->strToTime($val->date);
1809                     if (!$data) {
1810                         $data = $val->timestamp;
1811                     }
1812                 } else {
1813                     $data = $val->$field;
1814                     if (is_string($data)) {
1815                         $data = str_replace('"', '', $data);
1816                         if ($field == 'subject') {
1817                             $data = preg_replace('/^(Re: \s*|Fwd:\s*|Fw:\s*)+/i', '', $data);
1818                         }
1819                         $data = strtoupper($data);
1820                     }
1821                 }
1822                 $index[$key] = $data;
1823             }
1824
1825             // sort index
1826             if ($flag == 'ASC') {
1827                 asort($index);
1828             } else {
1829                 arsort($index);
1830             }
1831
1832             // form new array based on index
1833             $result = array();
1834             reset($index);
1835             while (list($key, $val) = each($index)) {
1836                 $result[$key] = $a[$key];
1837             }
1838         }
1839
1840         return $result;
1841     }
1842
1843
1844     function modFlag($mailbox, $messages, $flag, $mod)
1845     {
1846         if ($mod != '+' && $mod != '-') {
1847             $mod = '+';
1848         }
1849
1850         if (!$this->select($mailbox)) {
1851             return false;
1852         }
1853
1854         if (!$this->data['READ-WRITE']) {
1855             $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE');
1856             return false;
1857         }
1858
1859         // Clear internal status cache
1860         if ($flag == 'SEEN') {
1861             unset($this->data['STATUS:'.$mailbox]['UNSEEN']);
1862         }
1863
1864         $flag   = $this->flags[strtoupper($flag)];
1865         $result = $this->execute('UID STORE', array(
1866             $this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"),
1867             self::COMMAND_NORESPONSE);
1868
1869         return ($result == self::ERROR_OK);
1870     }
1871
1872     function flag($mailbox, $messages, $flag) {
1873         return $this->modFlag($mailbox, $messages, $flag, '+');
1874     }
1875
1876     function unflag($mailbox, $messages, $flag) {
1877         return $this->modFlag($mailbox, $messages, $flag, '-');
1878     }
1879
1880     function delete($mailbox, $messages) {
1881         return $this->modFlag($mailbox, $messages, 'DELETED', '+');
1882     }
1883
1884     function copy($messages, $from, $to)
1885     {
1886         if (!$this->select($from)) {
1887             return false;
1888         }
1889
1890         // Clear internal status cache
1891         unset($this->data['STATUS:'.$to]);
1892
1893         $result = $this->execute('UID COPY', array(
1894             $this->compressMessageSet($messages), $this->escape($to)),
1895             self::COMMAND_NORESPONSE);
1896
1897         return ($result == self::ERROR_OK);
1898     }
1899
1900     function move($messages, $from, $to)
1901     {
1902         if (!$this->select($from)) {
1903             return false;
1904         }
1905
1906         if (!$this->data['READ-WRITE']) {
1907             $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE');
1908             return false;
1909         }
1910
1911         $r = $this->copy($messages, $from, $to);
1912
1913         if ($r) {
1914             // Clear internal status cache
1915             unset($this->data['STATUS:'.$from]);
1916
1917             return $this->delete($from, $messages);
1918         }
1919         return $r;
1920     }
1921
1922     // Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about
1923     // 7 times instead :-) See comments on http://uk2.php.net/references and this article:
1924     // http://derickrethans.nl/files/phparch-php-variables-article.pdf
1925     private function parseThread($str, $begin, $end, $root, $parent, $depth, &$depthmap, &$haschildren)
1926     {
1927         $node = array();
1928         if ($str[$begin] != '(') {
1929             $stop = $begin + strspn($str, '1234567890', $begin, $end - $begin);
1930             $msg = substr($str, $begin, $stop - $begin);
1931             if ($msg == 0)
1932                 return $node;
1933             if (is_null($root))
1934                 $root = $msg;
1935             $depthmap[$msg] = $depth;
1936             $haschildren[$msg] = false;
1937             if (!is_null($parent))
1938                 $haschildren[$parent] = true;
1939             if ($stop + 1 < $end)
1940                 $node[$msg] = $this->parseThread($str, $stop + 1, $end, $root, $msg, $depth + 1, $depthmap, $haschildren);
1941             else
1942                 $node[$msg] = array();
1943         } else {
1944             $off = $begin;
1945             while ($off < $end) {
1946                 $start = $off;
1947                 $off++;
1948                 $n = 1;
1949                 while ($n > 0) {
1950                     $p = strpos($str, ')', $off);
1951                     if ($p === false) {
1952                         error_log("Mismatched brackets parsing IMAP THREAD response:");
1953                         error_log(substr($str, ($begin < 10) ? 0 : ($begin - 10), $end - $begin + 20));
1954                         error_log(str_repeat(' ', $off - (($begin < 10) ? 0 : ($begin - 10))));
1955                         return $node;
1956                     }
1957                     $p1 = strpos($str, '(', $off);
1958                     if ($p1 !== false && $p1 < $p) {
1959                         $off = $p1 + 1;
1960                         $n++;
1961                     } else {
1962                         $off = $p + 1;
1963                         $n--;
1964                     }
1965                 }
1966                 $node += $this->parseThread($str, $start + 1, $off - 1, $root, $parent, $depth, $depthmap, $haschildren);
1967             }
1968         }
1969
1970         return $node;
1971     }
1972
1973     function thread($mailbox, $algorithm='REFERENCES', $criteria='', $encoding='US-ASCII')
1974     {
1975         $old_sel = $this->selected;
1976
1977         if (!$this->select($mailbox)) {
1978             return false;
1979         }
1980
1981         // return empty result when folder is empty and we're just after SELECT
1982         if ($old_sel != $mailbox && !$this->data['EXISTS']) {
1983             return array(array(), array(), array());
1984         }
1985
1986         $encoding  = $encoding ? trim($encoding) : 'US-ASCII';
1987         $algorithm = $algorithm ? trim($algorithm) : 'REFERENCES';
1988         $criteria  = $criteria ? 'ALL '.trim($criteria) : 'ALL';
1989         $data      = '';
1990
1991         list($code, $response) = $this->execute('THREAD', array(
1992             $algorithm, $encoding, $criteria));
1993
1994         if ($code == self::ERROR_OK) {
1995             // remove prefix...
1996             $response = substr($response, stripos($response, '* THREAD') + 9);
1997             // ...unilateral untagged server responses
1998             if ($pos = strpos($response, '*')) {
1999                 $response = substr($response, 0, $pos);
2000             }
2001
2002             $response    = str_replace("\r\n", '', $response);
2003             $depthmap    = array();
2004             $haschildren = array();
2005
2006             $tree = $this->parseThread($response, 0, strlen($response),
2007                 null, null, 0, $depthmap, $haschildren);
2008
2009             return array($tree, $depthmap, $haschildren);
2010         }
2011
2012         return false;
2013     }
2014
2015     /**
2016      * Executes SEARCH command
2017      *
2018      * @param string $mailbox    Mailbox name
2019      * @param string $criteria   Searching criteria
2020      * @param bool   $return_uid Enable UID in result instead of sequence ID
2021      * @param array  $items      Return items (MIN, MAX, COUNT, ALL)
2022      *
2023      * @return array Message identifiers or item-value hash 
2024      */
2025     function search($mailbox, $criteria, $return_uid=false, $items=array())
2026     {
2027         $old_sel = $this->selected;
2028
2029         if (!$this->select($mailbox)) {
2030             return false;
2031         }
2032
2033         // return empty result when folder is empty and we're just after SELECT
2034         if ($old_sel != $mailbox && !$this->data['EXISTS']) {
2035             if (!empty($items))
2036                 return array_combine($items, array_fill(0, count($items), 0));
2037             else
2038                 return array();
2039         }
2040
2041         $esearch  = empty($items) ? false : $this->getCapability('ESEARCH');
2042         $criteria = trim($criteria);
2043         $params   = '';
2044
2045         // RFC4731: ESEARCH
2046         if (!empty($items) && $esearch) {
2047             $params .= 'RETURN (' . implode(' ', $items) . ')';
2048         }
2049         if (!empty($criteria)) {
2050             $params .= ($params ? ' ' : '') . $criteria;
2051         }
2052         else {
2053             $params .= 'ALL';
2054         }
2055
2056         list($code, $response) = $this->execute($return_uid ? 'UID SEARCH' : 'SEARCH',
2057             array($params));
2058
2059         if ($code == self::ERROR_OK) {
2060             // remove prefix...
2061             $response = substr($response, stripos($response, 
2062                 $esearch ? '* ESEARCH' : '* SEARCH') + ($esearch ? 10 : 9));
2063             // ...and unilateral untagged server responses
2064             if ($pos = strpos($response, '*')) {
2065                 $response = rtrim(substr($response, 0, $pos));
2066             }
2067
2068             if ($esearch) {
2069                 // Skip prefix: ... (TAG "A285") UID ...
2070                 $this->tokenizeResponse($response, $return_uid ? 2 : 1);
2071
2072                 $result = array();
2073                 for ($i=0; $i<count($items); $i++) {
2074                     // If the SEARCH results in no matches, the server MUST NOT
2075                     // include the item result option in the ESEARCH response
2076                     if ($ret = $this->tokenizeResponse($response, 2)) {
2077                         list ($name, $value) = $ret;
2078                         $result[$name] = $value;
2079                     }
2080                 }
2081
2082                 return $result;
2083             }
2084             else {
2085                 $response = preg_split('/[\s\r\n]+/', $response, -1, PREG_SPLIT_NO_EMPTY);
2086
2087                 if (!empty($items)) {
2088                     $result = array();
2089                     if (in_array('COUNT', $items)) {
2090                         $result['COUNT'] = count($response);
2091                     }
2092                     if (in_array('MIN', $items)) {
2093                         $result['MIN'] = !empty($response) ? min($response) : 0;
2094                     }
2095                     if (in_array('MAX', $items)) {
2096                         $result['MAX'] = !empty($response) ? max($response) : 0;
2097                     }
2098                     if (in_array('ALL', $items)) {
2099                         $result['ALL'] = $this->compressMessageSet($response, true);
2100                     }
2101
2102                     return $result;
2103                 }
2104                 else {
2105                     return $response;
2106                 }
2107             }
2108         }
2109
2110         return false;
2111     }
2112
2113     /**
2114      * Returns list of mailboxes
2115      *
2116      * @param string $ref         Reference name
2117      * @param string $mailbox     Mailbox name
2118      * @param array  $status_opts (see self::_listMailboxes)
2119      * @param array  $select_opts (see self::_listMailboxes)
2120      *
2121      * @return array List of mailboxes or hash of options if $status_opts argument
2122      *               is non-empty.
2123      * @access public
2124      */
2125     function listMailboxes($ref, $mailbox, $status_opts=array(), $select_opts=array())
2126     {
2127         return $this->_listMailboxes($ref, $mailbox, false, $status_opts, $select_opts);
2128     }
2129
2130     /**
2131      * Returns list of subscribed mailboxes
2132      *
2133      * @param string $ref         Reference name
2134      * @param string $mailbox     Mailbox name
2135      * @param array  $status_opts (see self::_listMailboxes)
2136      *
2137      * @return array List of mailboxes or hash of options if $status_opts argument
2138      *               is non-empty.
2139      * @access public
2140      */
2141     function listSubscribed($ref, $mailbox, $status_opts=array())
2142     {
2143         return $this->_listMailboxes($ref, $mailbox, true, $status_opts, NULL);
2144     }
2145
2146     /**
2147      * IMAP LIST/LSUB command
2148      *
2149      * @param string $ref         Reference name
2150      * @param string $mailbox     Mailbox name
2151      * @param bool   $subscribed  Enables returning subscribed mailboxes only
2152      * @param array  $status_opts List of STATUS options (RFC5819: LIST-STATUS)
2153      *                            Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN
2154      * @param array  $select_opts List of selection options (RFC5258: LIST-EXTENDED)
2155      *                            Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE
2156      *
2157      * @return array List of mailboxes or hash of options if $status_ops argument
2158      *               is non-empty.
2159      * @access private
2160      */
2161     private function _listMailboxes($ref, $mailbox, $subscribed=false,
2162         $status_opts=array(), $select_opts=array())
2163     {
2164         if (!strlen($mailbox)) {
2165             $mailbox = '*';
2166         }
2167
2168         $args = array();
2169
2170         if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) {
2171             $select_opts = (array) $select_opts;
2172
2173             $args[] = '(' . implode(' ', $select_opts) . ')';
2174         }
2175
2176         $args[] = $this->escape($ref);
2177         $args[] = $this->escape($mailbox);
2178
2179         if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) {
2180             $status_opts = (array) $status_opts;
2181             $lstatus = true;
2182
2183             $args[] = 'RETURN (STATUS (' . implode(' ', $status_opts) . '))';
2184         }
2185
2186         list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args);
2187
2188         if ($code == self::ERROR_OK) {
2189             $folders = array();
2190             while ($this->tokenizeResponse($response, 1) == '*') {
2191                 $cmd = strtoupper($this->tokenizeResponse($response, 1));
2192                 // * LIST (<options>) <delimiter> <mailbox>
2193                 if (!$lstatus || $cmd == 'LIST' || $cmd == 'LSUB') {
2194                     list($opts, $delim, $mailbox) = $this->tokenizeResponse($response, 3);
2195
2196                     // Add to result array
2197                     if (!$lstatus) {
2198                         $folders[] = $mailbox;
2199                     }
2200                     else {
2201                         $folders[$mailbox] = array();
2202                     }
2203
2204                     // Add to options array
2205                     if (!empty($opts)) {
2206                         if (empty($this->data['LIST'][$mailbox]))
2207                             $this->data['LIST'][$mailbox] = $opts;
2208                         else
2209                             $this->data['LIST'][$mailbox] = array_unique(array_merge(
2210                                 $this->data['LIST'][$mailbox], $opts));
2211                     }
2212                 }
2213                 // * STATUS <mailbox> (<result>)
2214                 else if ($cmd == 'STATUS') {
2215                     list($mailbox, $status) = $this->tokenizeResponse($response, 2);
2216
2217                     for ($i=0, $len=count($status); $i<$len; $i += 2) {
2218                         list($name, $value) = $this->tokenizeResponse($status, 2);
2219                         $folders[$mailbox][$name] = $value;
2220                     }
2221                 }
2222             }
2223
2224             return $folders;
2225         }
2226
2227         return false;
2228     }
2229
2230     function fetchMIMEHeaders($mailbox, $id, $parts, $mime=true)
2231     {
2232         if (!$this->select($mailbox)) {
2233             return false;
2234         }
2235
2236         $result = false;
2237         $parts  = (array) $parts;
2238         $key    = $this->nextTag();
2239         $peeks  = '';
2240         $idx    = 0;
2241         $type   = $mime ? 'MIME' : 'HEADER';
2242
2243         // format request
2244         foreach($parts as $part) {
2245             $peeks[] = "BODY.PEEK[$part.$type]";
2246         }
2247
2248         $request = "$key FETCH $id (" . implode(' ', $peeks) . ')';
2249
2250         // send request
2251         if (!$this->putLine($request)) {
2252             $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
2253             return false;
2254         }
2255
2256         do {
2257             $line = $this->readLine(1024);
2258             $line = $this->multLine($line);
2259
2260             if (preg_match('/BODY\[([0-9\.]+)\.'.$type.'\]/', $line, $matches)) {
2261                 $idx = $matches[1];
2262                 $result[$idx] = preg_replace('/^(\* '.$id.' FETCH \()?\s*BODY\['.$idx.'\.'.$type.'\]\s+/', '', $line);
2263                 $result[$idx] = trim($result[$idx], '"');
2264                 $result[$idx] = rtrim($result[$idx], "\t\r\n\0\x0B");
2265             }
2266         } while (!$this->startsWith($line, $key, true));
2267
2268         return $result;
2269     }
2270
2271     function fetchPartHeader($mailbox, $id, $is_uid=false, $part=NULL)
2272     {
2273         $part = empty($part) ? 'HEADER' : $part.'.MIME';
2274
2275         return $this->handlePartBody($mailbox, $id, $is_uid, $part);
2276     }
2277
2278     function handlePartBody($mailbox, $id, $is_uid=false, $part='', $encoding=NULL, $print=NULL, $file=NULL)
2279     {
2280         if (!$this->select($mailbox)) {
2281             return false;
2282         }
2283
2284         switch ($encoding) {
2285         case 'base64':
2286             $mode = 1;
2287             break;
2288         case 'quoted-printable':
2289             $mode = 2;
2290             break;
2291         case 'x-uuencode':
2292         case 'x-uue':
2293         case 'uue':
2294         case 'uuencode':
2295             $mode = 3;
2296             break;
2297         default:
2298             $mode = 0;
2299         }
2300
2301         // format request
2302         $reply_key = '* ' . $id;
2303         $key       = $this->nextTag();
2304         $request   = $key . ($is_uid ? ' UID' : '') . " FETCH $id (BODY.PEEK[$part])";
2305
2306         // send request
2307         if (!$this->putLine($request)) {
2308             $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
2309             return false;
2310         }
2311
2312         // receive reply line
2313         do {
2314             $line = rtrim($this->readLine(1024));
2315             $a    = explode(' ', $line);
2316         } while (!($end = $this->startsWith($line, $key, true)) && $a[2] != 'FETCH');
2317
2318         $len    = strlen($line);
2319         $result = false;
2320
2321         // handle empty "* X FETCH ()" response
2322         if ($line[$len-1] == ')' && $line[$len-2] != '(') {
2323             // one line response, get everything between first and last quotes
2324             if (substr($line, -4, 3) == 'NIL') {
2325                 // NIL response
2326                 $result = '';
2327             } else {
2328                 $from = strpos($line, '"') + 1;
2329                 $to   = strrpos($line, '"');
2330                 $len  = $to - $from;
2331                 $result = substr($line, $from, $len);
2332             }
2333
2334             if ($mode == 1) {
2335                 $result = base64_decode($result);
2336             }
2337             else if ($mode == 2) {
2338                 $result = quoted_printable_decode($result);
2339             }
2340             else if ($mode == 3) {
2341                 $result = convert_uudecode($result);
2342             }
2343
2344         } else if ($line[$len-1] == '}') {
2345             // multi-line request, find sizes of content and receive that many bytes
2346             $from     = strpos($line, '{') + 1;
2347             $to       = strrpos($line, '}');
2348             $len      = $to - $from;
2349             $sizeStr  = substr($line, $from, $len);
2350             $bytes    = (int)$sizeStr;
2351             $prev     = '';
2352
2353             while ($bytes > 0) {
2354                 $line = $this->readLine(4096);
2355
2356                 if ($line === NULL) {
2357                     break;
2358                 }
2359
2360                 $len  = strlen($line);
2361
2362                 if ($len > $bytes) {
2363                     $line = substr($line, 0, $bytes);
2364                     $len = strlen($line);
2365                 }
2366                 $bytes -= $len;
2367
2368                 // BASE64
2369                 if ($mode == 1) {
2370                     $line = rtrim($line, "\t\r\n\0\x0B");
2371                     // create chunks with proper length for base64 decoding
2372                     $line = $prev.$line;
2373                     $length = strlen($line);
2374                     if ($length % 4) {
2375                         $length = floor($length / 4) * 4;
2376                         $prev = substr($line, $length);
2377                         $line = substr($line, 0, $length);
2378                     }
2379                     else
2380                         $prev = '';
2381                     $line = base64_decode($line);
2382                 // QUOTED-PRINTABLE
2383                 } else if ($mode == 2) {
2384                     $line = rtrim($line, "\t\r\0\x0B");
2385                     $line = quoted_printable_decode($line);
2386                     // Remove NULL characters (#1486189)
2387                     $line = str_replace("\x00", '', $line);
2388                 // UUENCODE
2389                 } else if ($mode == 3) {
2390                     $line = rtrim($line, "\t\r\n\0\x0B");
2391                     if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line))
2392                         continue;
2393                     $line = convert_uudecode($line);
2394                 // default
2395                 } else {
2396                     $line = rtrim($line, "\t\r\n\0\x0B") . "\n";
2397                 }
2398
2399                 if ($file)
2400                     fwrite($file, $line);
2401                 else if ($print)
2402                     echo $line;
2403                 else
2404                     $result .= $line;
2405             }
2406         }
2407
2408         // read in anything up until last line
2409         if (!$end)
2410             do {
2411                 $line = $this->readLine(1024);
2412             } while (!$this->startsWith($line, $key, true));
2413
2414         if ($result !== false) {
2415             if ($file) {
2416                 fwrite($file, $result);
2417             } else if ($print) {
2418                 echo $result;
2419             } else
2420                 return $result;
2421             return true;
2422         }
2423
2424         return false;
2425     }
2426
2427     function createFolder($mailbox)
2428     {
2429         $result = $this->execute('CREATE', array($this->escape($mailbox)),
2430             self::COMMAND_NORESPONSE);
2431
2432         return ($result == self::ERROR_OK);
2433     }
2434
2435     function renameFolder($from, $to)
2436     {
2437         $result = $this->execute('RENAME', array($this->escape($from), $this->escape($to)),
2438             self::COMMAND_NORESPONSE);
2439
2440         return ($result == self::ERROR_OK);
2441     }
2442
2443     function append($mailbox, &$message)
2444     {
2445         if (!$mailbox) {
2446             return false;
2447         }
2448
2449         $message = str_replace("\r", '', $message);
2450         $message = str_replace("\n", "\r\n", $message);
2451
2452         $len = strlen($message);
2453         if (!$len) {
2454             return false;
2455         }
2456
2457         $key = $this->nextTag();
2458         $request = sprintf("$key APPEND %s (\\Seen) {%d%s}", $this->escape($mailbox),
2459             $len, ($this->prefs['literal+'] ? '+' : ''));
2460
2461         if ($this->putLine($request)) {
2462             // Don't wait when LITERAL+ is supported
2463             if (!$this->prefs['literal+']) {
2464                 $line = $this->readReply();
2465
2466                 if ($line[0] != '+') {
2467                     $this->parseResult($line, 'APPEND: ');
2468                     return false;
2469                 }
2470             }
2471
2472             if (!$this->putLine($message)) {
2473                 return false;
2474             }
2475
2476             do {
2477                 $line = $this->readLine();
2478             } while (!$this->startsWith($line, $key, true, true));
2479
2480             // Clear internal status cache
2481             unset($this->data['STATUS:'.$mailbox]);
2482
2483             return ($this->parseResult($line, 'APPEND: ') == self::ERROR_OK);
2484         }
2485         else {
2486             $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
2487         }
2488
2489         return false;
2490     }
2491
2492     function appendFromFile($mailbox, $path, $headers=null)
2493     {
2494         if (!$mailbox) {
2495             return false;
2496         }
2497
2498         // open message file
2499         $in_fp = false;
2500         if (file_exists(realpath($path))) {
2501             $in_fp = fopen($path, 'r');
2502         }
2503         if (!$in_fp) {
2504             $this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading");
2505             return false;
2506         }
2507
2508         $body_separator = "\r\n\r\n";
2509         $len = filesize($path);
2510
2511         if (!$len) {
2512             return false;
2513         }
2514
2515         if ($headers) {
2516             $headers = preg_replace('/[\r\n]+$/', '', $headers);
2517             $len += strlen($headers) + strlen($body_separator);
2518         }
2519
2520         // send APPEND command
2521         $key = $this->nextTag();
2522         $request = sprintf("$key APPEND %s (\\Seen) {%d%s}", $this->escape($mailbox),
2523             $len, ($this->prefs['literal+'] ? '+' : ''));
2524
2525         if ($this->putLine($request)) {
2526             // Don't wait when LITERAL+ is supported
2527             if (!$this->prefs['literal+']) {
2528                 $line = $this->readReply();
2529
2530                 if ($line[0] != '+') {
2531                     $this->parseResult($line, 'APPEND: ');
2532                     return false;
2533                 }
2534             }
2535
2536             // send headers with body separator
2537             if ($headers) {
2538                 $this->putLine($headers . $body_separator, false);
2539             }
2540
2541             // send file
2542             while (!feof($in_fp) && $this->fp) {
2543                 $buffer = fgets($in_fp, 4096);
2544                 $this->putLine($buffer, false);
2545             }
2546             fclose($in_fp);
2547
2548             if (!$this->putLine('')) { // \r\n
2549                 return false;
2550             }
2551
2552             // read response
2553             do {
2554                 $line = $this->readLine();
2555             } while (!$this->startsWith($line, $key, true, true));
2556
2557             // Clear internal status cache
2558             unset($this->data['STATUS:'.$mailbox]);
2559
2560             return ($this->parseResult($line, 'APPEND: ') == self::ERROR_OK);
2561         }
2562         else {
2563             $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
2564         }
2565
2566         return false;
2567     }
2568
2569     function fetchStructureString($mailbox, $id, $is_uid=false)
2570     {
2571         if (!$this->select($mailbox)) {
2572             return false;
2573         }
2574
2575         $key = $this->nextTag();
2576         $result = false;
2577         $command = $key . ($is_uid ? ' UID' : '') ." FETCH $id (BODYSTRUCTURE)";
2578
2579         if ($this->putLine($command)) {
2580             do {
2581                 $line = $this->readLine(5000);
2582                 $line = $this->multLine($line, true);
2583                 if (!preg_match("/^$key /", $line))
2584                     $result .= $line;
2585             } while (!$this->startsWith($line, $key, true, true));
2586
2587             $result = trim(substr($result, strpos($result, 'BODYSTRUCTURE')+13, -1));
2588         }
2589         else {
2590             $this->setError(self::ERROR_COMMAND, "Unable to send command: $command");
2591         }
2592
2593         return $result;
2594     }
2595
2596     function getQuota()
2597     {
2598         /*
2599          * GETQUOTAROOT "INBOX"
2600          * QUOTAROOT INBOX user/rchijiiwa1
2601          * QUOTA user/rchijiiwa1 (STORAGE 654 9765)
2602          * OK Completed
2603          */
2604         $result      = false;
2605         $quota_lines = array();
2606         $key         = $this->nextTag();
2607         $command     = $key . ' GETQUOTAROOT INBOX';
2608
2609         // get line(s) containing quota info
2610         if ($this->putLine($command)) {
2611             do {
2612                 $line = rtrim($this->readLine(5000));
2613                 if (preg_match('/^\* QUOTA /', $line)) {
2614                     $quota_lines[] = $line;
2615                 }
2616             } while (!$this->startsWith($line, $key, true, true));
2617         }
2618         else {
2619             $this->setError(self::ERROR_COMMAND, "Unable to send command: $command");
2620         }
2621
2622         // return false if not found, parse if found
2623         $min_free = PHP_INT_MAX;
2624         foreach ($quota_lines as $key => $quota_line) {
2625             $quota_line   = str_replace(array('(', ')'), '', $quota_line);
2626             $parts        = explode(' ', $quota_line);
2627             $storage_part = array_search('STORAGE', $parts);
2628
2629             if (!$storage_part) {
2630                 continue;
2631             }
2632
2633             $used  = intval($parts[$storage_part+1]);
2634             $total = intval($parts[$storage_part+2]);
2635             $free  = $total - $used;
2636
2637             // return lowest available space from all quotas
2638             if ($free < $min_free) {
2639                 $min_free          = $free;
2640                 $result['used']    = $used;
2641                 $result['total']   = $total;
2642                 $result['percent'] = min(100, round(($used/max(1,$total))*100));
2643                 $result['free']    = 100 - $result['percent'];
2644             }
2645         }
2646
2647         return $result;
2648     }
2649
2650     /**
2651      * Send the SETACL command (RFC4314)
2652      *
2653      * @param string $mailbox Mailbox name
2654      * @param string $user    User name
2655      * @param mixed  $acl     ACL string or array
2656      *
2657      * @return boolean True on success, False on failure
2658      *
2659      * @access public
2660      * @since 0.5-beta
2661      */
2662     function setACL($mailbox, $user, $acl)
2663     {
2664         if (is_array($acl)) {
2665             $acl = implode('', $acl);
2666         }
2667
2668         $result = $this->execute('SETACL', array(
2669             $this->escape($mailbox), $this->escape($user), strtolower($acl)),
2670             self::COMMAND_NORESPONSE);
2671
2672         return ($result == self::ERROR_OK);
2673     }
2674
2675     /**
2676      * Send the DELETEACL command (RFC4314)
2677      *
2678      * @param string $mailbox Mailbox name
2679      * @param string $user    User name
2680      *
2681      * @return boolean True on success, False on failure
2682      *
2683      * @access public
2684      * @since 0.5-beta
2685      */
2686     function deleteACL($mailbox, $user)
2687     {
2688         $result = $this->execute('DELETEACL', array(
2689             $this->escape($mailbox), $this->escape($user)),
2690             self::COMMAND_NORESPONSE);
2691
2692         return ($result == self::ERROR_OK);
2693     }
2694
2695     /**
2696      * Send the GETACL command (RFC4314)
2697      *
2698      * @param string $mailbox Mailbox name
2699      *
2700      * @return array User-rights array on success, NULL on error
2701      * @access public
2702      * @since 0.5-beta
2703      */
2704     function getACL($mailbox)
2705     {
2706         list($code, $response) = $this->execute('GETACL', array($this->escape($mailbox)));
2707
2708         if ($code == self::ERROR_OK && preg_match('/^\* ACL /i', $response)) {
2709             // Parse server response (remove "* ACL ")
2710             $response = substr($response, 6);
2711             $ret  = $this->tokenizeResponse($response);
2712             $mbox = array_shift($ret);
2713             $size = count($ret);
2714
2715             // Create user-rights hash array
2716             // @TODO: consider implementing fixACL() method according to RFC4314.2.1.1
2717             // so we could return only standard rights defined in RFC4314,
2718             // excluding 'c' and 'd' defined in RFC2086.
2719             if ($size % 2 == 0) {
2720                 for ($i=0; $i<$size; $i++) {
2721                     $ret[$ret[$i]] = str_split($ret[++$i]);
2722                     unset($ret[$i-1]);
2723                     unset($ret[$i]);
2724                 }
2725                 return $ret;
2726             }
2727
2728             $this->setError(self::ERROR_COMMAND, "Incomplete ACL response");
2729             return NULL;
2730         }
2731
2732         return NULL;
2733     }
2734
2735     /**
2736      * Send the LISTRIGHTS command (RFC4314)
2737      *
2738      * @param string $mailbox Mailbox name
2739      * @param string $user    User name
2740      *
2741      * @return array List of user rights
2742      * @access public
2743      * @since 0.5-beta
2744      */
2745     function listRights($mailbox, $user)
2746     {
2747         list($code, $response) = $this->execute('LISTRIGHTS', array(
2748             $this->escape($mailbox), $this->escape($user)));
2749
2750         if ($code == self::ERROR_OK && preg_match('/^\* LISTRIGHTS /i', $response)) {
2751             // Parse server response (remove "* LISTRIGHTS ")
2752             $response = substr($response, 13);
2753
2754             $ret_mbox = $this->tokenizeResponse($response, 1);
2755             $ret_user = $this->tokenizeResponse($response, 1);
2756             $granted  = $this->tokenizeResponse($response, 1);
2757             $optional = trim($response);
2758
2759             return array(
2760                 'granted'  => str_split($granted),
2761                 'optional' => explode(' ', $optional),
2762             );
2763         }
2764
2765         return NULL;
2766     }
2767
2768     /**
2769      * Send the MYRIGHTS command (RFC4314)
2770      *
2771      * @param string $mailbox Mailbox name
2772      *
2773      * @return array MYRIGHTS response on success, NULL on error
2774      * @access public
2775      * @since 0.5-beta
2776      */
2777     function myRights($mailbox)
2778     {
2779         list($code, $response) = $this->execute('MYRIGHTS', array($this->escape($mailbox)));
2780
2781         if ($code == self::ERROR_OK && preg_match('/^\* MYRIGHTS /i', $response)) {
2782             // Parse server response (remove "* MYRIGHTS ")
2783             $response = substr($response, 11);
2784
2785             $ret_mbox = $this->tokenizeResponse($response, 1);
2786             $rights   = $this->tokenizeResponse($response, 1);
2787
2788             return str_split($rights);
2789         }
2790
2791         return NULL;
2792     }
2793
2794     /**
2795      * Send the SETMETADATA command (RFC5464)
2796      *
2797      * @param string $mailbox Mailbox name
2798      * @param array  $entries Entry-value array (use NULL value as NIL)
2799      *
2800      * @return boolean True on success, False on failure
2801      * @access public
2802      * @since 0.5-beta
2803      */
2804     function setMetadata($mailbox, $entries)
2805     {
2806         if (!is_array($entries) || empty($entries)) {
2807             $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
2808             return false;
2809         }
2810
2811         foreach ($entries as $name => $value) {
2812             if ($value === null) {
2813                 $value = 'NIL';
2814             }
2815             else {
2816                 $value = sprintf("{%d}\r\n%s", strlen($value), $value);
2817             }
2818             $entries[$name] = $this->escape($name) . ' ' . $value;
2819         }
2820
2821         $entries = implode(' ', $entries);
2822         $result = $this->execute('SETMETADATA', array(
2823             $this->escape($mailbox), '(' . $entries . ')'),
2824             self::COMMAND_NORESPONSE);
2825
2826         return ($result == self::ERROR_OK);
2827     }
2828
2829     /**
2830      * Send the SETMETADATA command with NIL values (RFC5464)
2831      *
2832      * @param string $mailbox Mailbox name
2833      * @param array  $entries Entry names array
2834      *
2835      * @return boolean True on success, False on failure
2836      *
2837      * @access public
2838      * @since 0.5-beta
2839      */
2840     function deleteMetadata($mailbox, $entries)
2841     {
2842         if (!is_array($entries) && !empty($entries)) {
2843             $entries = explode(' ', $entries);
2844         }
2845
2846         if (empty($entries)) {
2847             $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
2848             return false;
2849         }
2850
2851         foreach ($entries as $entry) {
2852             $data[$entry] = NULL;
2853         }
2854
2855         return $this->setMetadata($mailbox, $data);
2856     }
2857
2858     /**
2859      * Send the GETMETADATA command (RFC5464)
2860      *
2861      * @param string $mailbox Mailbox name
2862      * @param array  $entries Entries
2863      * @param array  $options Command options (with MAXSIZE and DEPTH keys)
2864      *
2865      * @return array GETMETADATA result on success, NULL on error
2866      *
2867      * @access public
2868      * @since 0.5-beta
2869      */
2870     function getMetadata($mailbox, $entries, $options=array())
2871     {
2872         if (!is_array($entries)) {
2873             $entries = array($entries);
2874         }
2875
2876         // create entries string
2877         foreach ($entries as $idx => $name) {
2878             $entries[$idx] = $this->escape($name);
2879         }
2880
2881         $optlist = '';
2882         $entlist = '(' . implode(' ', $entries) . ')';
2883
2884         // create options string
2885         if (is_array($options)) {
2886             $options = array_change_key_case($options, CASE_UPPER);
2887             $opts = array();
2888
2889             if (!empty($options['MAXSIZE'])) {
2890                 $opts[] = 'MAXSIZE '.intval($options['MAXSIZE']);
2891             }
2892             if (!empty($options['DEPTH'])) {
2893                 $opts[] = 'DEPTH '.intval($options['DEPTH']);
2894             }
2895
2896             if ($opts) {
2897                 $optlist = '(' . implode(' ', $opts) . ')';
2898             }
2899         }
2900
2901         $optlist .= ($optlist ? ' ' : '') . $entlist;
2902
2903         list($code, $response) = $this->execute('GETMETADATA', array(
2904             $this->escape($mailbox), $optlist));
2905
2906         if ($code == self::ERROR_OK) {
2907             $result = array();
2908             $data   = $this->tokenizeResponse($response);
2909
2910             // The METADATA response can contain multiple entries in a single
2911             // response or multiple responses for each entry or group of entries
2912             if (!empty($data) && ($size = count($data))) {
2913                 for ($i=0; $i<$size; $i++) {
2914                     if (isset($mbox) && is_array($data[$i])) {
2915                         $size_sub = count($data[$i]);
2916                         for ($x=0; $x<$size_sub; $x++) {
2917                             $result[$mbox][$data[$i][$x]] = $data[$i][++$x];
2918                         }
2919                         unset($data[$i]);
2920                     }
2921                     else if ($data[$i] == '*') {
2922                         if ($data[$i+1] == 'METADATA') {
2923                             $mbox = $data[$i+2];
2924                             unset($data[$i]);   // "*"
2925                             unset($data[++$i]); // "METADATA"
2926                             unset($data[++$i]); // Mailbox
2927                         }
2928                         // get rid of other untagged responses
2929                         else {
2930                             unset($mbox);
2931                             unset($data[$i]);
2932                         }
2933                     }
2934                     else if (isset($mbox)) {
2935                         $result[$mbox][$data[$i]] = $data[++$i];
2936                         unset($data[$i]);
2937                         unset($data[$i-1]);
2938                     }
2939                     else {
2940                         unset($data[$i]);
2941                     }
2942                 }
2943             }
2944
2945             return $result;
2946         }
2947
2948         return NULL;
2949     }
2950
2951     /**
2952      * Send the SETANNOTATION command (draft-daboo-imap-annotatemore)
2953      *
2954      * @param string $mailbox Mailbox name
2955      * @param array  $data    Data array where each item is an array with
2956      *                        three elements: entry name, attribute name, value
2957      *
2958      * @return boolean True on success, False on failure
2959      * @access public
2960      * @since 0.5-beta
2961      */
2962     function setAnnotation($mailbox, $data)
2963     {
2964         if (!is_array($data) || empty($data)) {
2965             $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
2966             return false;
2967         }
2968
2969         foreach ($data as $entry) {
2970             $name  = $entry[0];
2971             $attr  = $entry[1];
2972             $value = $entry[2];
2973
2974             if ($value === null) {
2975                 $value = 'NIL';
2976             }
2977             else {
2978                 $value = sprintf("{%d}\r\n%s", strlen($value), $value);
2979             }
2980
2981             // ANNOTATEMORE drafts before version 08 require quoted parameters
2982             $entries[] = sprintf('%s (%s %s)',
2983                 $this->escape($name, true), $this->escape($attr, true), $value);
2984         }
2985
2986         $entries = implode(' ', $entries);
2987         $result  = $this->execute('SETANNOTATION', array(
2988             $this->escape($mailbox), $entries), self::COMMAND_NORESPONSE);
2989
2990         return ($result == self::ERROR_OK);
2991     }
2992
2993     /**
2994      * Send the SETANNOTATION command with NIL values (draft-daboo-imap-annotatemore)
2995      *
2996      * @param string $mailbox Mailbox name
2997      * @param array  $data    Data array where each item is an array with
2998      *                        two elements: entry name and attribute name
2999      *
3000      * @return boolean True on success, False on failure
3001      *
3002      * @access public
3003      * @since 0.5-beta
3004      */
3005     function deleteAnnotation($mailbox, $data)
3006     {
3007         if (!is_array($data) || empty($data)) {
3008             $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
3009             return false;
3010         }
3011
3012         return $this->setAnnotation($mailbox, $data);
3013     }
3014
3015     /**
3016      * Send the GETANNOTATION command (draft-daboo-imap-annotatemore)
3017      *
3018      * @param string $mailbox Mailbox name
3019      * @param array  $entries Entries names
3020      * @param array  $attribs Attribs names
3021      *
3022      * @return array Annotations result on success, NULL on error
3023      *
3024      * @access public
3025      * @since 0.5-beta
3026      */
3027     function getAnnotation($mailbox, $entries, $attribs)
3028     {
3029         if (!is_array($entries)) {
3030             $entries = array($entries);
3031         }
3032         // create entries string
3033         // ANNOTATEMORE drafts before version 08 require quoted parameters
3034         foreach ($entries as $idx => $name) {
3035             $entries[$idx] = $this->escape($name, true);
3036         }
3037         $entries = '(' . implode(' ', $entries) . ')';
3038
3039         if (!is_array($attribs)) {
3040             $attribs = array($attribs);
3041         }
3042         // create entries string
3043         foreach ($attribs as $idx => $name) {
3044             $attribs[$idx] = $this->escape($name, true);
3045         }
3046         $attribs = '(' . implode(' ', $attribs) . ')';
3047
3048         list($code, $response) = $this->execute('GETANNOTATION', array(
3049             $this->escape($mailbox), $entries, $attribs));
3050
3051         if ($code == self::ERROR_OK) {
3052             $result = array();
3053             $data   = $this->tokenizeResponse($response);
3054
3055             // Here we returns only data compatible with METADATA result format
3056             if (!empty($data) && ($size = count($data))) {
3057                 for ($i=0; $i<$size; $i++) {
3058                     $entry = $data[$i];
3059                     if (isset($mbox) && is_array($entry)) {
3060                         $attribs = $entry;
3061                         $entry   = $last_entry;
3062                     }
3063                     else if ($entry == '*') {
3064                         if ($data[$i+1] == 'ANNOTATION') {
3065                             $mbox = $data[$i+2];
3066                             unset($data[$i]);   // "*"
3067                             unset($data[++$i]); // "ANNOTATION"
3068                             unset($data[++$i]); // Mailbox
3069                         }
3070                         // get rid of other untagged responses
3071                         else {
3072                             unset($mbox);
3073                             unset($data[$i]);
3074                         }
3075                         continue;
3076                     }
3077                     else if (isset($mbox)) {
3078                         $attribs = $data[++$i];
3079                     }
3080                     else {
3081                         unset($data[$i]);
3082                         continue;
3083                     }
3084
3085                     if (!empty($attribs)) {
3086                         for ($x=0, $len=count($attribs); $x<$len;) {
3087                             $attr  = $attribs[$x++];
3088                             $value = $attribs[$x++];
3089                             if ($attr == 'value.priv') {
3090                                 $result[$mbox]['/private' . $entry] = $value;
3091                             }
3092                             else if ($attr == 'value.shared') {
3093                                 $result[$mbox]['/shared' . $entry] = $value;
3094                             }
3095                         }
3096                     }
3097                     $last_entry = $entry;
3098                     unset($data[$i]);
3099                 }
3100             }
3101
3102             return $result;
3103         }
3104
3105         return NULL;
3106     }
3107
3108     /**
3109      * Creates next command identifier (tag)
3110      *
3111      * @return string Command identifier
3112      * @access public
3113      * @since 0.5-beta
3114      */
3115     function nextTag()
3116     {
3117         $this->cmd_num++;
3118         $this->cmd_tag = sprintf('A%04d', $this->cmd_num);
3119
3120         return $this->cmd_tag;
3121     }
3122
3123     /**
3124      * Sends IMAP command and parses result
3125      *
3126      * @param string $command   IMAP command
3127      * @param array  $arguments Command arguments
3128      * @param int    $options   Execution options
3129      *
3130      * @return mixed Response code or list of response code and data
3131      * @access public
3132      * @since 0.5-beta
3133      */
3134     function execute($command, $arguments=array(), $options=0)
3135     {
3136         $tag      = $this->nextTag();
3137         $query    = $tag . ' ' . $command;
3138         $noresp   = ($options & self::COMMAND_NORESPONSE);
3139         $response = $noresp ? null : '';
3140
3141         if (!empty($arguments)) {
3142             $query .= ' ' . implode(' ', $arguments);
3143         }
3144
3145         // Send command
3146         if (!$this->putLineC($query)) {
3147             $this->setError(self::ERROR_COMMAND, "Unable to send command: $query");
3148             return $noresp ? self::ERROR_COMMAND : array(self::ERROR_COMMAND, '');
3149         }
3150
3151         // Parse response
3152         do {
3153             $line = $this->readLine(4096);
3154             if ($response !== null) {
3155                 $response .= $line;
3156             }
3157         } while (!$this->startsWith($line, $tag . ' ', true, true));
3158
3159         $code = $this->parseResult($line, $command . ': ');
3160
3161         // Remove last line from response
3162         if ($response) {
3163             $line_len = min(strlen($response), strlen($line) + 2);
3164             $response = substr($response, 0, -$line_len);
3165         }
3166
3167         // optional CAPABILITY response
3168         if (($options & self::COMMAND_CAPABILITY) && $code == self::ERROR_OK
3169             && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)
3170         ) {
3171             $this->parseCapability($matches[1], true);
3172         }
3173
3174         // return last line only (without command tag, result and response code)
3175         if ($line && ($options & self::COMMAND_LASTLINE)) {
3176             $response = preg_replace("/^$tag (OK|NO|BAD|BYE|PREAUTH)?\s*(\[[a-z-]+\])?\s*/i", '', trim($line));
3177         }
3178
3179         return $noresp ? $code : array($code, $response);
3180     }
3181
3182     /**
3183      * Splits IMAP response into string tokens
3184      *
3185      * @param string &$str The IMAP's server response
3186      * @param int    $num  Number of tokens to return
3187      *
3188      * @return mixed Tokens array or string if $num=1
3189      * @access public
3190      * @since 0.5-beta
3191      */
3192     static function tokenizeResponse(&$str, $num=0)
3193     {
3194         $result = array();
3195
3196         while (!$num || count($result) < $num) {
3197             // remove spaces from the beginning of the string
3198             $str = ltrim($str);
3199
3200             switch ($str[0]) {
3201
3202             // String literal
3203             case '{':
3204                 if (($epos = strpos($str, "}\r\n", 1)) == false) {
3205                     // error
3206                 }
3207                 if (!is_numeric(($bytes = substr($str, 1, $epos - 1)))) {
3208                     // error
3209                 }
3210                 $result[] = substr($str, $epos + 3, $bytes);
3211                 // Advance the string
3212                 $str = substr($str, $epos + 3 + $bytes);
3213                 break;
3214
3215             // Quoted string
3216             case '"':
3217                 $len = strlen($str);
3218
3219                 for ($pos=1; $pos<$len; $pos++) {
3220                     if ($str[$pos] == '"') {
3221                         break;
3222                     }
3223                     if ($str[$pos] == "\\") {
3224                         if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") {
3225                             $pos++;
3226                         }
3227                     }
3228                 }
3229                 if ($str[$pos] != '"') {
3230                     // error
3231                 }
3232                 // we need to strip slashes for a quoted string
3233                 $result[] = stripslashes(substr($str, 1, $pos - 1));
3234                 $str      = substr($str, $pos + 1);
3235                 break;
3236
3237             // Parenthesized list
3238             case '(':
3239                 $str = substr($str, 1);
3240                 $result[] = self::tokenizeResponse($str);
3241                 break;
3242             case ')':
3243                 $str = substr($str, 1);
3244                 return $result;
3245                 break;
3246
3247             // String atom, number, NIL, *, %
3248             default:
3249                 // empty or one character
3250                 if ($str === '') {
3251                     break 2;
3252                 }
3253                 if (strlen($str) < 2) {
3254                     $result[] = $str;
3255                     $str = '';
3256                     break;
3257                 }
3258
3259                 // excluded chars: SP, CTL, (, ), {, ", ], %
3260                 if (preg_match('/^([\x21\x23\x24\x26\x27\x2A-\x5C\x5E-\x7A\x7C-\x7E]+)/', $str, $m)) {
3261                     $result[] = $m[1] == 'NIL' ? NULL : $m[1];
3262                     $str = substr($str, strlen($m[1]));
3263                 }
3264                 break;
3265             }
3266         }
3267
3268         return $num == 1 ? $result[0] : $result;
3269     }
3270
3271     private function _xor($string, $string2)
3272     {
3273         $result = '';
3274         $size   = strlen($string);
3275
3276         for ($i=0; $i<$size; $i++) {
3277             $result .= chr(ord($string[$i]) ^ ord($string2[$i]));
3278         }
3279
3280         return $result;
3281     }
3282
3283     /**
3284      * Converts datetime string into unix timestamp
3285      *
3286      * @param string $date Date string
3287      *
3288      * @return int Unix timestamp
3289      */
3290     private function strToTime($date)
3291     {
3292         // support non-standard "GMTXXXX" literal
3293         $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date);
3294
3295         // if date parsing fails, we have a date in non-rfc format.
3296         // remove token from the end and try again
3297         while (($ts = intval(@strtotime($date))) <= 0) {
3298             $d = explode(' ', $date);
3299             array_pop($d);
3300             if (!$d) {
3301                 break;
3302             }
3303             $date = implode(' ', $d);
3304         }
3305
3306         $ts = (int) $ts;
3307
3308         return $ts < 0 ? 0 : $ts;
3309     }
3310
3311     private function parseCapability($str, $trusted=false)
3312     {
3313         $str = preg_replace('/^\* CAPABILITY /i', '', $str);
3314
3315         $this->capability = explode(' ', strtoupper($str));
3316
3317         if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) {
3318             $this->prefs['literal+'] = true;
3319         }
3320
3321         if ($trusted) {
3322             $this->capability_readed = true;
3323         }
3324     }
3325
3326     /**
3327      * Escapes a string when it contains special characters (RFC3501)
3328      *
3329      * @param string  $string       IMAP string
3330      * @param boolean $force_quotes Forces string quoting
3331      *
3332      * @return string Escaped string
3333      * @todo String literals, lists
3334      */
3335     static function escape($string, $force_quotes=false)
3336     {
3337         if ($string === null) {
3338             return 'NIL';
3339         }
3340         else if ($string === '') {
3341             return '""';
3342         }
3343         // need quoted-string? find special chars: SP, CTL, (, ), {, %, *, ", \, ]
3344         // plus [ character as a workaround for DBMail's bug (#1487766)
3345         else if ($force_quotes ||
3346             preg_match('/([\x00-\x20\x28-\x29\x7B\x25\x2A\x22\x5B\x5C\x5D\x7F]+)/', $string)
3347         ) {
3348             return '"' . strtr($string, array('"'=>'\\"', '\\' => '\\\\')) . '"';
3349         }
3350
3351         // atom
3352         return $string;
3353     }
3354
3355     static function unEscape($string)
3356     {
3357         return strtr($string, array('\\"'=>'"', '\\\\' => '\\'));
3358     }
3359
3360     /**
3361      * Set the value of the debugging flag.
3362      *
3363      * @param   boolean $debug      New value for the debugging flag.
3364      *
3365      * @access  public
3366      * @since   0.5-stable
3367      */
3368     function setDebug($debug, $handler = null)
3369     {
3370         $this->_debug = $debug;
3371         $this->_debug_handler = $handler;
3372     }
3373
3374     /**
3375      * Write the given debug text to the current debug output handler.
3376      *
3377      * @param   string  $message    Debug mesage text.
3378      *
3379      * @access  private
3380      * @since   0.5-stable
3381      */
3382     private function debug($message)
3383     {
3384         if ($this->_debug_handler) {
3385             call_user_func_array($this->_debug_handler, array(&$this, $message));
3386         } else {
3387             echo "DEBUG: $message\n";
3388         }
3389     }
3390
3391 }