]> git.donarmstrong.com Git - roundcube.git/blob - program/include/rcube_imap_generic.php
Imported Upstream version 0.5.3+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 4729 2011-05-04 18:53:11Z 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
782         // Send ID info
783         if (!empty($this->prefs['ident']) && $this->getCapability('ID')) {
784             $this->id($this->prefs['ident']);
785         }
786
787         $auth_methods = array();
788         $result       = null;
789
790         // check for supported auth methods
791         if ($auth_method == 'CHECK') {
792             if ($auth_caps = $this->getCapability('AUTH')) {
793                 $auth_methods = $auth_caps;
794             }
795             // RFC 2595 (LOGINDISABLED) LOGIN disabled when connection is not secure
796             $login_disabled = $this->getCapability('LOGINDISABLED');
797             if (($key = array_search('LOGIN', $auth_methods)) !== false) {
798                 if ($login_disabled) {
799                     unset($auth_methods[$key]);
800                 }
801             }
802             else if (!$login_disabled) {
803                 $auth_methods[] = 'LOGIN';
804             }
805
806             // Use best (for security) supported authentication method
807             foreach (array('DIGEST-MD5', 'CRAM-MD5', 'CRAM_MD5', 'PLAIN', 'LOGIN') as $auth_method) {
808                 if (in_array($auth_method, $auth_methods)) {
809                     break;
810                 }
811             }
812         }
813         else {
814             // Prevent from sending credentials in plain text when connection is not secure
815             if ($auth_method == 'LOGIN' && $this->getCapability('LOGINDISABLED')) {
816                 $this->setError(self::ERROR_BAD, "Login disabled by IMAP server");
817                 $this->closeConnection();
818                 return false;
819             }
820             // replace AUTH with CRAM-MD5 for backward compat.
821             if ($auth_method == 'AUTH') {
822                 $auth_method = 'CRAM-MD5';
823             }
824         }
825
826         // pre-login capabilities can be not complete
827         $this->capability_readed = false;
828
829         // Authenticate
830         switch ($auth_method) {
831             case 'CRAM_MD5':
832                 $auth_method = 'CRAM-MD5';
833             case 'CRAM-MD5':
834             case 'DIGEST-MD5':
835             case 'PLAIN':
836                 $result = $this->authenticate($user, $password, $auth_method);
837                 break;
838             case 'LOGIN':
839                 $result = $this->login($user, $password);
840                 break;
841             default:
842                 $this->setError(self::ERROR_BAD, "Configuration error. Unknown auth method: $auth_method");
843         }
844
845         // Connected and authenticated
846         if (is_resource($result)) {
847             if ($this->prefs['force_caps']) {
848                 $this->clearCapability();
849             }
850             $this->logged = true;
851
852             return true;
853         }
854
855         $this->closeConnection();
856
857         return false;
858     }
859
860     function connected()
861     {
862         return ($this->fp && $this->logged) ? true : false;
863     }
864
865     function closeConnection()
866     {
867         if ($this->putLine($this->nextTag() . ' LOGOUT')) {
868             $this->readReply();
869         }
870
871         $this->closeSocket();
872     }
873
874     /**
875      * Executes SELECT command (if mailbox is already not in selected state)
876      *
877      * @param string $mailbox Mailbox name
878      *
879      * @return boolean True on success, false on error
880      * @access public
881      */
882     function select($mailbox)
883     {
884         if (!strlen($mailbox)) {
885             return false;
886         }
887
888         if ($this->selected == $mailbox) {
889             return true;
890         }
891 /*
892     Temporary commented out because Courier returns \Noselect for INBOX
893     Requires more investigation
894
895         if (is_array($this->data['LIST']) && is_array($opts = $this->data['LIST'][$mailbox])) {
896             if (in_array('\\Noselect', $opts)) {
897                 return false;
898             }
899         }
900 */
901         list($code, $response) = $this->execute('SELECT', array($this->escape($mailbox)));
902
903         if ($code == self::ERROR_OK) {
904             $response = explode("\r\n", $response);
905             foreach ($response as $line) {
906                 if (preg_match('/^\* ([0-9]+) (EXISTS|RECENT)$/i', $line, $m)) {
907                     $this->data[strtoupper($m[2])] = (int) $m[1];
908                 }
909                 else if (preg_match('/^\* OK \[(UIDNEXT|UIDVALIDITY|UNSEEN) ([0-9]+)\]/i', $line, $match)) {
910                     $this->data[strtoupper($match[1])] = (int) $match[2];
911                 }
912                 else if (preg_match('/^\* OK \[PERMANENTFLAGS \(([^\)]+)\)\]/iU', $line, $match)) {
913                     $this->data['PERMANENTFLAGS'] = explode(' ', $match[1]);
914                 }
915             }
916
917             $this->data['READ-WRITE'] = $this->resultcode != 'READ-ONLY';
918
919             $this->selected = $mailbox;
920             return true;
921         }
922
923         return false;
924     }
925
926     /**
927      * Executes STATUS command
928      *
929      * @param string $mailbox Mailbox name
930      * @param array  $items   Additional requested item names. By default
931      *                        MESSAGES and UNSEEN are requested. Other defined
932      *                        in RFC3501: UIDNEXT, UIDVALIDITY, RECENT
933      *
934      * @return array Status item-value hash
935      * @access public
936      * @since 0.5-beta
937      */
938     function status($mailbox, $items=array())
939     {
940         if (!strlen($mailbox)) {
941             return false;
942         }
943
944         if (!in_array('MESSAGES', $items)) {
945             $items[] = 'MESSAGES';
946         }
947         if (!in_array('UNSEEN', $items)) {
948             $items[] = 'UNSEEN';
949         }
950
951         list($code, $response) = $this->execute('STATUS', array($this->escape($mailbox),
952             '(' . implode(' ', (array) $items) . ')'));
953
954         if ($code == self::ERROR_OK && preg_match('/\* STATUS /i', $response)) {
955             $result   = array();
956             $response = substr($response, 9); // remove prefix "* STATUS "
957
958             list($mbox, $items) = $this->tokenizeResponse($response, 2);
959
960             // Fix for #1487859. Some buggy server returns not quoted
961             // folder name with spaces. Let's try to handle this situation
962             if (!is_array($items) && ($pos = strpos($response, '(')) !== false) {
963                 $response = substr($response, $pos);
964                 $items = $this->tokenizeResponse($response, 1);
965                 if (!is_array($items)) {
966                     return $result;
967                 }
968             }
969
970             for ($i=0, $len=count($items); $i<$len; $i += 2) {
971                 $result[$items[$i]] = (int) $items[$i+1];
972             }
973
974             $this->data['STATUS:'.$mailbox] = $result;
975
976             return $result;
977         }
978
979         return false;
980     }
981
982     /**
983      * Executes EXPUNGE command
984      *
985      * @param string $mailbox  Mailbox name
986      * @param string $messages Message UIDs to expunge
987      *
988      * @return boolean True on success, False on error
989      * @access public
990      */
991     function expunge($mailbox, $messages=NULL)
992     {
993         if (!$this->select($mailbox)) {
994             return false;
995         }
996
997         if (!$this->data['READ-WRITE']) {
998             $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'EXPUNGE');
999             return false;
1000         }
1001
1002         // Clear internal status cache
1003         unset($this->data['STATUS:'.$mailbox]);
1004
1005         if ($messages)
1006             $result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE);
1007         else
1008             $result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE);
1009
1010         if ($result == self::ERROR_OK) {
1011             $this->selected = ''; // state has changed, need to reselect
1012             return true;
1013         }
1014
1015         return false;
1016     }
1017
1018     /**
1019      * Executes CLOSE command
1020      *
1021      * @return boolean True on success, False on error
1022      * @access public
1023      * @since 0.5
1024      */
1025     function close()
1026     {
1027         $result = $this->execute('CLOSE', NULL, self::COMMAND_NORESPONSE);
1028
1029         if ($result == self::ERROR_OK) {
1030             $this->selected = '';
1031             return true;
1032         }
1033
1034         return false;
1035     }
1036
1037     /**
1038      * Executes SUBSCRIBE command
1039      *
1040      * @param string $mailbox Mailbox name
1041      *
1042      * @return boolean True on success, False on error
1043      * @access public
1044      */
1045     function subscribe($mailbox)
1046     {
1047         $result = $this->execute('SUBSCRIBE', array($this->escape($mailbox)),
1048             self::COMMAND_NORESPONSE);
1049
1050         return ($result == self::ERROR_OK);
1051     }
1052
1053     /**
1054      * Executes UNSUBSCRIBE command
1055      *
1056      * @param string $mailbox Mailbox name
1057      *
1058      * @return boolean True on success, False on error
1059      * @access public
1060      */
1061     function unsubscribe($mailbox)
1062     {
1063         $result = $this->execute('UNSUBSCRIBE', array($this->escape($mailbox)),
1064             self::COMMAND_NORESPONSE);
1065
1066         return ($result == self::ERROR_OK);
1067     }
1068
1069     /**
1070      * Executes DELETE command
1071      *
1072      * @param string $mailbox Mailbox name
1073      *
1074      * @return boolean True on success, False on error
1075      * @access public
1076      */
1077     function deleteFolder($mailbox)
1078     {
1079         $result = $this->execute('DELETE', array($this->escape($mailbox)),
1080             self::COMMAND_NORESPONSE);
1081
1082         return ($result == self::ERROR_OK);
1083     }
1084
1085     /**
1086      * Removes all messages in a folder
1087      *
1088      * @param string $mailbox Mailbox name
1089      *
1090      * @return boolean True on success, False on error
1091      * @access public
1092      */
1093     function clearFolder($mailbox)
1094     {
1095         $num_in_trash = $this->countMessages($mailbox);
1096         if ($num_in_trash > 0) {
1097             $res = $this->delete($mailbox, '1:*');
1098         }
1099
1100         if ($res) {
1101             if ($this->selected == $mailbox)
1102                 $res = $this->close();
1103             else
1104                 $res = $this->expunge($mailbox);
1105         }
1106
1107         return $res;
1108     }
1109
1110     /**
1111      * Returns count of all messages in a folder
1112      *
1113      * @param string $mailbox Mailbox name
1114      *
1115      * @return int Number of messages, False on error
1116      * @access public
1117      */
1118     function countMessages($mailbox, $refresh = false)
1119     {
1120         if ($refresh) {
1121             $this->selected = '';
1122         }
1123
1124         if ($this->selected == $mailbox) {
1125             return $this->data['EXISTS'];
1126         }
1127
1128         // Check internal cache
1129         $cache = $this->data['STATUS:'.$mailbox];
1130         if (!empty($cache) && isset($cache['MESSAGES'])) {
1131             return (int) $cache['MESSAGES'];
1132         }
1133
1134         // Try STATUS (should be faster than SELECT)
1135         $counts = $this->status($mailbox);
1136         if (is_array($counts)) {
1137             return (int) $counts['MESSAGES'];
1138         }
1139
1140         return false;
1141     }
1142
1143     /**
1144      * Returns count of messages with \Recent flag in a folder
1145      *
1146      * @param string $mailbox Mailbox name
1147      *
1148      * @return int Number of messages, False on error
1149      * @access public
1150      */
1151     function countRecent($mailbox)
1152     {
1153         if (!strlen($mailbox)) {
1154             $mailbox = 'INBOX';
1155         }
1156
1157         $this->select($mailbox);
1158
1159         if ($this->selected == $mailbox) {
1160             return $this->data['RECENT'];
1161         }
1162
1163         return false;
1164     }
1165
1166     /**
1167      * Returns count of messages without \Seen flag in a specified folder
1168      *
1169      * @param string $mailbox Mailbox name
1170      *
1171      * @return int Number of messages, False on error
1172      * @access public
1173      */
1174     function countUnseen($mailbox)
1175     {
1176         // Check internal cache
1177         $cache = $this->data['STATUS:'.$mailbox];
1178         if (!empty($cache) && isset($cache['UNSEEN'])) {
1179             return (int) $cache['UNSEEN'];
1180         }
1181
1182         // Try STATUS (should be faster than SELECT+SEARCH)
1183         $counts = $this->status($mailbox);
1184         if (is_array($counts)) {
1185             return (int) $counts['UNSEEN'];
1186         }
1187
1188         // Invoke SEARCH as a fallback
1189         $index = $this->search($mailbox, 'ALL UNSEEN', false, array('COUNT'));
1190         if (is_array($index)) {
1191             return (int) $index['COUNT'];
1192         }
1193
1194         return false;
1195     }
1196
1197     /**
1198      * Executes ID command (RFC2971)
1199      *
1200      * @param array $items Client identification information key/value hash
1201      *
1202      * @return array Server identification information key/value hash
1203      * @access public
1204      * @since 0.6
1205      */
1206     function id($items=array())
1207     {
1208         if (is_array($items) && !empty($items)) {
1209             foreach ($items as $key => $value) {
1210                 $args[] = $this->escape($key, true);
1211                 $args[] = $this->escape($value, true);
1212             }
1213         }
1214
1215         list($code, $response) = $this->execute('ID', array(
1216             !empty($args) ? '(' . implode(' ', (array) $args) . ')' : $this->escape(null)
1217         ));
1218
1219
1220         if ($code == self::ERROR_OK && preg_match('/\* ID /i', $response)) {
1221             $response = substr($response, 5); // remove prefix "* ID "
1222             $items    = $this->tokenizeResponse($response, 1);
1223             $result   = null;
1224
1225             for ($i=0, $len=count($items); $i<$len; $i += 2) {
1226                 $result[$items[$i]] = $items[$i+1];
1227             }
1228
1229             return $result;
1230         }
1231
1232         return false;
1233     }
1234
1235     function sort($mailbox, $field, $add='', $is_uid=FALSE, $encoding = 'US-ASCII')
1236     {
1237         $field = strtoupper($field);
1238         if ($field == 'INTERNALDATE') {
1239             $field = 'ARRIVAL';
1240         }
1241
1242         $fields = array('ARRIVAL' => 1,'CC' => 1,'DATE' => 1,
1243             'FROM' => 1, 'SIZE' => 1, 'SUBJECT' => 1, 'TO' => 1);
1244
1245         if (!$fields[$field]) {
1246             return false;
1247         }
1248
1249         if (!$this->select($mailbox)) {
1250             return false;
1251         }
1252
1253         // message IDs
1254         if (!empty($add))
1255             $add = $this->compressMessageSet($add);
1256
1257         list($code, $response) = $this->execute($is_uid ? 'UID SORT' : 'SORT',
1258             array("($field)", $encoding, 'ALL' . (!empty($add) ? ' '.$add : '')));
1259
1260         if ($code == self::ERROR_OK) {
1261             // remove prefix and unilateral untagged server responses
1262             $response = substr($response, stripos($response, '* SORT') + 7);
1263             if ($pos = strpos($response, '*')) {
1264                 $response = substr($response, 0, $pos);
1265             }
1266             return preg_split('/[\s\r\n]+/', $response, -1, PREG_SPLIT_NO_EMPTY);
1267         }
1268
1269         return false;
1270     }
1271
1272     function fetchHeaderIndex($mailbox, $message_set, $index_field='', $skip_deleted=true, $uidfetch=false)
1273     {
1274         if (is_array($message_set)) {
1275             if (!($message_set = $this->compressMessageSet($message_set)))
1276                 return false;
1277         } else {
1278             list($from_idx, $to_idx) = explode(':', $message_set);
1279             if (empty($message_set) ||
1280                 (isset($to_idx) && $to_idx != '*' && (int)$from_idx > (int)$to_idx)) {
1281                 return false;
1282             }
1283         }
1284
1285         $index_field = empty($index_field) ? 'DATE' : strtoupper($index_field);
1286
1287         $fields_a['DATE']         = 1;
1288         $fields_a['INTERNALDATE'] = 4;
1289         $fields_a['ARRIVAL']      = 4;
1290         $fields_a['FROM']         = 1;
1291         $fields_a['REPLY-TO']     = 1;
1292         $fields_a['SENDER']       = 1;
1293         $fields_a['TO']           = 1;
1294         $fields_a['CC']           = 1;
1295         $fields_a['SUBJECT']      = 1;
1296         $fields_a['UID']          = 2;
1297         $fields_a['SIZE']         = 2;
1298         $fields_a['SEEN']         = 3;
1299         $fields_a['RECENT']       = 3;
1300         $fields_a['DELETED']      = 3;
1301
1302         if (!($mode = $fields_a[$index_field])) {
1303             return false;
1304         }
1305
1306         /*  Do "SELECT" command */
1307         if (!$this->select($mailbox)) {
1308             return false;
1309         }
1310
1311         // build FETCH command string
1312         $key     = $this->nextTag();
1313         $cmd     = $uidfetch ? 'UID FETCH' : 'FETCH';
1314         $deleted = $skip_deleted ? ' FLAGS' : '';
1315
1316         if ($mode == 1 && $index_field == 'DATE')
1317             $request = " $cmd $message_set (INTERNALDATE BODY.PEEK[HEADER.FIELDS (DATE)]$deleted)";
1318         else if ($mode == 1)
1319             $request = " $cmd $message_set (BODY.PEEK[HEADER.FIELDS ($index_field)]$deleted)";
1320         else if ($mode == 2) {
1321             if ($index_field == 'SIZE')
1322                 $request = " $cmd $message_set (RFC822.SIZE$deleted)";
1323             else
1324                 $request = " $cmd $message_set ($index_field$deleted)";
1325         } else if ($mode == 3)
1326             $request = " $cmd $message_set (FLAGS)";
1327         else // 4
1328             $request = " $cmd $message_set (INTERNALDATE$deleted)";
1329
1330         $request = $key . $request;
1331
1332         if (!$this->putLine($request)) {
1333             $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
1334             return false;
1335         }
1336
1337         $result = array();
1338
1339         do {
1340             $line = rtrim($this->readLine(200));
1341             $line = $this->multLine($line);
1342
1343             if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
1344                 $id     = $m[1];
1345                 $flags  = NULL;
1346
1347                 if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
1348                     $flags = explode(' ', strtoupper($matches[1]));
1349                     if (in_array('\\DELETED', $flags)) {
1350                         $deleted[$id] = $id;
1351                         continue;
1352                     }
1353                 }
1354
1355                 if ($mode == 1 && $index_field == 'DATE') {
1356                     if (preg_match('/BODY\[HEADER\.FIELDS \("*DATE"*\)\] (.*)/', $line, $matches)) {
1357                         $value = preg_replace(array('/^"*[a-z]+:/i'), '', $matches[1]);
1358                         $value = trim($value);
1359                         $result[$id] = $this->strToTime($value);
1360                     }
1361                     // non-existent/empty Date: header, use INTERNALDATE
1362                     if (empty($result[$id])) {
1363                         if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches))
1364                             $result[$id] = $this->strToTime($matches[1]);
1365                         else
1366                             $result[$id] = 0;
1367                     }
1368                 } else if ($mode == 1) {
1369                     if (preg_match('/BODY\[HEADER\.FIELDS \("?(FROM|REPLY-TO|SENDER|TO|SUBJECT)"?\)\] (.*)/', $line, $matches)) {
1370                         $value = preg_replace(array('/^"*[a-z]+:/i', '/\s+$/sm'), array('', ''), $matches[2]);
1371                         $result[$id] = trim($value);
1372                     } else {
1373                         $result[$id] = '';
1374                     }
1375                 } else if ($mode == 2) {
1376                     if (preg_match('/(UID|RFC822\.SIZE) ([0-9]+)/', $line, $matches)) {
1377                         $result[$id] = trim($matches[2]);
1378                     } else {
1379                         $result[$id] = 0;
1380                     }
1381                 } else if ($mode == 3) {
1382                     if (!$flags && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
1383                         $flags = explode(' ', $matches[1]);
1384                     }
1385                     $result[$id] = in_array('\\'.$index_field, $flags) ? 1 : 0;
1386                 } else if ($mode == 4) {
1387                     if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) {
1388                         $result[$id] = $this->strToTime($matches[1]);
1389                     } else {
1390                         $result[$id] = 0;
1391                     }
1392                 }
1393             }
1394         } while (!$this->startsWith($line, $key, true, true));
1395
1396         return $result;
1397     }
1398
1399     static function compressMessageSet($messages, $force=false)
1400     {
1401         // given a comma delimited list of independent mid's,
1402         // compresses by grouping sequences together
1403
1404         if (!is_array($messages)) {
1405             // if less than 255 bytes long, let's not bother
1406             if (!$force && strlen($messages)<255) {
1407                 return $messages;
1408            }
1409
1410             // see if it's already been compressed
1411             if (strpos($messages, ':') !== false) {
1412                 return $messages;
1413             }
1414
1415             // separate, then sort
1416             $messages = explode(',', $messages);
1417         }
1418
1419         sort($messages);
1420
1421         $result = array();
1422         $start  = $prev = $messages[0];
1423
1424         foreach ($messages as $id) {
1425             $incr = $id - $prev;
1426             if ($incr > 1) { // found a gap
1427                 if ($start == $prev) {
1428                     $result[] = $prev; // push single id
1429                 } else {
1430                     $result[] = $start . ':' . $prev; // push sequence as start_id:end_id
1431                 }
1432                 $start = $id; // start of new sequence
1433             }
1434             $prev = $id;
1435         }
1436
1437         // handle the last sequence/id
1438         if ($start == $prev) {
1439             $result[] = $prev;
1440         } else {
1441             $result[] = $start.':'.$prev;
1442         }
1443
1444         // return as comma separated string
1445         return implode(',', $result);
1446     }
1447
1448     static function uncompressMessageSet($messages)
1449     {
1450         $result   = array();
1451         $messages = explode(',', $messages);
1452
1453         foreach ($messages as $part) {
1454             $items = explode(':', $part);
1455             $max   = max($items[0], $items[1]);
1456
1457             for ($x=$items[0]; $x<=$max; $x++) {
1458                 $result[] = $x;
1459             }
1460         }
1461
1462         return $result;
1463     }
1464
1465     /**
1466      * Returns message sequence identifier
1467      *
1468      * @param string $mailbox Mailbox name
1469      * @param int    $uid     Message unique identifier (UID)
1470      *
1471      * @return int Message sequence identifier
1472      * @access public
1473      */
1474     function UID2ID($mailbox, $uid)
1475     {
1476         if ($uid > 0) {
1477             $id_a = $this->search($mailbox, "UID $uid");
1478             if (is_array($id_a) && count($id_a) == 1) {
1479                 return (int) $id_a[0];
1480             }
1481         }
1482         return null;
1483     }
1484
1485     /**
1486      * Returns message unique identifier (UID)
1487      *
1488      * @param string $mailbox Mailbox name
1489      * @param int    $uid     Message sequence identifier
1490      *
1491      * @return int Message unique identifier
1492      * @access public
1493      */
1494     function ID2UID($mailbox, $id)
1495     {
1496         if (empty($id) || $id < 0) {
1497             return      null;
1498         }
1499
1500         if (!$this->select($mailbox)) {
1501             return null;
1502         }
1503
1504         list($code, $response) = $this->execute('FETCH', array($id, '(UID)'));
1505
1506         if ($code == self::ERROR_OK && preg_match("/^\* $id FETCH \(UID (.*)\)/i", $response, $m)) {
1507             return (int) $m[1];
1508         }
1509
1510         return null;
1511     }
1512
1513     function fetchUIDs($mailbox, $message_set=null)
1514     {
1515         if (is_array($message_set))
1516             $message_set = join(',', $message_set);
1517         else if (empty($message_set))
1518             $message_set = '1:*';
1519
1520         return $this->fetchHeaderIndex($mailbox, $message_set, 'UID', false);
1521     }
1522
1523     function fetchHeaders($mailbox, $message_set, $uidfetch=false, $bodystr=false, $add='')
1524     {
1525         $result = array();
1526
1527         if (!$this->select($mailbox)) {
1528             return false;
1529         }
1530
1531         $message_set = $this->compressMessageSet($message_set);
1532
1533         if ($add)
1534             $add = ' '.trim($add);
1535
1536         /* FETCH uid, size, flags and headers */
1537         $key      = $this->nextTag();
1538         $request  = $key . ($uidfetch ? ' UID' : '') . " FETCH $message_set ";
1539         $request .= "(UID RFC822.SIZE FLAGS INTERNALDATE ";
1540         if ($bodystr)
1541             $request .= "BODYSTRUCTURE ";
1542         $request .= "BODY.PEEK[HEADER.FIELDS (DATE FROM TO SUBJECT CONTENT-TYPE ";
1543         $request .= "LIST-POST DISPOSITION-NOTIFICATION-TO".$add.")])";
1544
1545         if (!$this->putLine($request)) {
1546             $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
1547             return false;
1548         }
1549         do {
1550             $line = $this->readLine(4096);
1551             $line = $this->multLine($line);
1552
1553             if (!$line)
1554                 break;
1555
1556             if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
1557                 $id = intval($m[1]);
1558
1559                 $result[$id]            = new rcube_mail_header;
1560                 $result[$id]->id        = $id;
1561                 $result[$id]->subject   = '';
1562                 $result[$id]->messageID = 'mid:' . $id;
1563
1564                 $lines = array();
1565                 $ln = 0;
1566
1567                 // Sample reply line:
1568                 // * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen)
1569                 // INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...)
1570                 // BODY[HEADER.FIELDS ...
1571
1572                 if (preg_match('/^\* [0-9]+ FETCH \((.*) BODY/sU', $line, $matches)) {
1573                     $str = $matches[1];
1574
1575                     while (list($name, $value) = $this->tokenizeResponse($str, 2)) {
1576                         if ($name == 'UID') {
1577                             $result[$id]->uid = intval($value);
1578                         }
1579                         else if ($name == 'RFC822.SIZE') {
1580                             $result[$id]->size = intval($value);
1581                         }
1582                         else if ($name == 'INTERNALDATE') {
1583                             $result[$id]->internaldate = $value;
1584                             $result[$id]->date         = $value;
1585                             $result[$id]->timestamp    = $this->StrToTime($value);
1586                         }
1587                         else if ($name == 'FLAGS') {
1588                             $flags_a = $value;
1589                         }
1590                     }
1591
1592                     // BODYSTRUCTURE
1593                     if ($bodystr) {
1594                         while (!preg_match('/ BODYSTRUCTURE (.*) BODY\[HEADER.FIELDS/sU', $line, $m)) {
1595                             $line2 = $this->readLine(1024);
1596                             $line .= $this->multLine($line2, true);
1597                         }
1598                         $result[$id]->body_structure = $m[1];
1599                     }
1600
1601                     // the rest of the result
1602                     if (preg_match('/ BODY\[HEADER.FIELDS \(.*?\)\]\s*(.*)$/s', $line, $m)) {
1603                         $reslines = explode("\n", trim($m[1], '"'));
1604                         // re-parse (see below)
1605                         foreach ($reslines as $resln) {
1606                             if (ord($resln[0])<=32) {
1607                                 $lines[$ln] .= (empty($lines[$ln])?'':"\n").trim($resln);
1608                             } else {
1609                                 $lines[++$ln] = trim($resln);
1610                             }
1611                         }
1612                     }
1613                 }
1614
1615                 // Start parsing headers.  The problem is, some header "lines" take up multiple lines.
1616                 // So, we'll read ahead, and if the one we're reading now is a valid header, we'll
1617                 // process the previous line.  Otherwise, we'll keep adding the strings until we come
1618                 // to the next valid header line.
1619
1620                 do {
1621                     $line = rtrim($this->readLine(300), "\r\n");
1622
1623                     // The preg_match below works around communigate imap, which outputs " UID <number>)".
1624                     // Without this, the while statement continues on and gets the "FH0 OK completed" message.
1625                     // If this loop gets the ending message, then the outer loop does not receive it from radline on line 1249.
1626                     // This in causes the if statement on line 1278 to never be true, which causes the headers to end up missing
1627                     // If the if statement was changed to pick up the fh0 from this loop, then it causes the outer loop to spin
1628                     // An alternative might be:
1629                     // if (!preg_match("/:/",$line) && preg_match("/\)$/",$line)) break;
1630                     // however, unsure how well this would work with all imap clients.
1631                     if (preg_match("/^\s*UID [0-9]+\)$/", $line)) {
1632                         break;
1633                     }
1634
1635                     // handle FLAGS reply after headers (AOL, Zimbra?)
1636                     if (preg_match('/\s+FLAGS \((.*)\)\)$/', $line, $matches)) {
1637                         $flags_a = $this->tokenizeResponse($matches[1]);
1638                         break;
1639                     }
1640
1641                     if (ord($line[0])<=32) {
1642                         $lines[$ln] .= (empty($lines[$ln])?'':"\n").trim($line);
1643                     } else {
1644                         $lines[++$ln] = trim($line);
1645                     }
1646                 // patch from "Maksim Rubis" <siburny@hotmail.com>
1647                 } while ($line[0] != ')' && !$this->startsWith($line, $key, true));
1648
1649                 if (strncmp($line, $key, strlen($key))) {
1650                     // process header, fill rcube_mail_header obj.
1651                     // initialize
1652                     if (is_array($headers)) {
1653                         reset($headers);
1654                         while (list($k, $bar) = each($headers)) {
1655                             $headers[$k] = '';
1656                         }
1657                     }
1658
1659                     // create array with header field:data
1660                     while (list($lines_key, $str) = each($lines)) {
1661                         list($field, $string) = explode(':', $str, 2);
1662
1663                         $field  = strtolower($field);
1664                         $string = preg_replace('/\n[\t\s]*/', ' ', trim($string));
1665
1666                         switch ($field) {
1667                         case 'date';
1668                             $result[$id]->date = $string;
1669                             $result[$id]->timestamp = $this->strToTime($string);
1670                             break;
1671                         case 'from':
1672                             $result[$id]->from = $string;
1673                             break;
1674                         case 'to':
1675                             $result[$id]->to = preg_replace('/undisclosed-recipients:[;,]*/', '', $string);
1676                             break;
1677                         case 'subject':
1678                             $result[$id]->subject = $string;
1679                             break;
1680                         case 'reply-to':
1681                             $result[$id]->replyto = $string;
1682                             break;
1683                         case 'cc':
1684                             $result[$id]->cc = $string;
1685                             break;
1686                         case 'bcc':
1687                             $result[$id]->bcc = $string;
1688                             break;
1689                         case 'content-transfer-encoding':
1690                             $result[$id]->encoding = $string;
1691                         break;
1692                         case 'content-type':
1693                             $ctype_parts = preg_split('/[; ]/', $string);
1694                             $result[$id]->ctype = strtolower(array_shift($ctype_parts));
1695                             if (preg_match('/charset\s*=\s*"?([a-z0-9\-\.\_]+)"?/i', $string, $regs)) {
1696                                 $result[$id]->charset = $regs[1];
1697                             }
1698                             break;
1699                         case 'in-reply-to':
1700                             $result[$id]->in_reply_to = str_replace(array("\n", '<', '>'), '', $string);
1701                             break;
1702                         case 'references':
1703                             $result[$id]->references = $string;
1704                             break;
1705                         case 'return-receipt-to':
1706                         case 'disposition-notification-to':
1707                         case 'x-confirm-reading-to':
1708                             $result[$id]->mdn_to = $string;
1709                             break;
1710                         case 'message-id':
1711                             $result[$id]->messageID = $string;
1712                             break;
1713                         case 'x-priority':
1714                             if (preg_match('/^(\d+)/', $string, $matches)) {
1715                                 $result[$id]->priority = intval($matches[1]);
1716                             }
1717                             break;
1718                         default:
1719                             if (strlen($field) > 2) {
1720                                 $result[$id]->others[$field] = $string;
1721                             }
1722                             break;
1723                         } // end switch ()
1724                     } // end while ()
1725                 }
1726
1727                 // process flags
1728                 if (!empty($flags_a)) {
1729                     foreach ($flags_a as $flag) {
1730                         $flag = str_replace('\\', '', $flag);
1731                         $result[$id]->flags[] = $flag;
1732
1733                         switch (strtoupper($flag)) {
1734                         case 'SEEN':
1735                             $result[$id]->seen = true;
1736                             break;
1737                         case 'DELETED':
1738                             $result[$id]->deleted = true;
1739                             break;
1740                         case 'ANSWERED':
1741                             $result[$id]->answered = true;
1742                             break;
1743                         case '$FORWARDED':
1744                             $result[$id]->forwarded = true;
1745                             break;
1746                         case '$MDNSENT':
1747                             $result[$id]->mdn_sent = true;
1748                             break;
1749                         case 'FLAGGED':
1750                             $result[$id]->flagged = true;
1751                             break;
1752                         }
1753                     }
1754                 }
1755             }
1756         } while (!$this->startsWith($line, $key, true));
1757
1758         return $result;
1759     }
1760
1761     function fetchHeader($mailbox, $id, $uidfetch=false, $bodystr=false, $add='')
1762     {
1763         $a  = $this->fetchHeaders($mailbox, $id, $uidfetch, $bodystr, $add);
1764         if (is_array($a)) {
1765             return array_shift($a);
1766         }
1767         return false;
1768     }
1769
1770     function sortHeaders($a, $field, $flag)
1771     {
1772         if (empty($field)) {
1773             $field = 'uid';
1774         }
1775         else {
1776             $field = strtolower($field);
1777         }
1778
1779         if ($field == 'date' || $field == 'internaldate') {
1780             $field = 'timestamp';
1781         }
1782
1783         if (empty($flag)) {
1784             $flag = 'ASC';
1785         } else {
1786             $flag = strtoupper($flag);
1787         }
1788
1789         $c = count($a);
1790         if ($c > 0) {
1791             // Strategy:
1792             // First, we'll create an "index" array.
1793             // Then, we'll use sort() on that array,
1794             // and use that to sort the main array.
1795
1796             // create "index" array
1797             $index = array();
1798             reset($a);
1799             while (list($key, $val) = each($a)) {
1800                 if ($field == 'timestamp') {
1801                     $data = $this->strToTime($val->date);
1802                     if (!$data) {
1803                         $data = $val->timestamp;
1804                     }
1805                 } else {
1806                     $data = $val->$field;
1807                     if (is_string($data)) {
1808                         $data = str_replace('"', '', $data);
1809                         if ($field == 'subject') {
1810                             $data = preg_replace('/^(Re: \s*|Fwd:\s*|Fw:\s*)+/i', '', $data);
1811                         }
1812                         $data = strtoupper($data);
1813                     }
1814                 }
1815                 $index[$key] = $data;
1816             }
1817
1818             // sort index
1819             if ($flag == 'ASC') {
1820                 asort($index);
1821             } else {
1822                 arsort($index);
1823             }
1824
1825             // form new array based on index
1826             $result = array();
1827             reset($index);
1828             while (list($key, $val) = each($index)) {
1829                 $result[$key] = $a[$key];
1830             }
1831         }
1832
1833         return $result;
1834     }
1835
1836
1837     function modFlag($mailbox, $messages, $flag, $mod)
1838     {
1839         if ($mod != '+' && $mod != '-') {
1840             $mod = '+';
1841         }
1842
1843         if (!$this->select($mailbox)) {
1844             return false;
1845         }
1846
1847         if (!$this->data['READ-WRITE']) {
1848             $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE');
1849             return false;
1850         }
1851
1852         // Clear internal status cache
1853         if ($flag == 'SEEN') {
1854             unset($this->data['STATUS:'.$mailbox]['UNSEEN']);
1855         }
1856
1857         $flag   = $this->flags[strtoupper($flag)];
1858         $result = $this->execute('UID STORE', array(
1859             $this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"),
1860             self::COMMAND_NORESPONSE);
1861
1862         return ($result == self::ERROR_OK);
1863     }
1864
1865     function flag($mailbox, $messages, $flag) {
1866         return $this->modFlag($mailbox, $messages, $flag, '+');
1867     }
1868
1869     function unflag($mailbox, $messages, $flag) {
1870         return $this->modFlag($mailbox, $messages, $flag, '-');
1871     }
1872
1873     function delete($mailbox, $messages) {
1874         return $this->modFlag($mailbox, $messages, 'DELETED', '+');
1875     }
1876
1877     function copy($messages, $from, $to)
1878     {
1879         if (!$this->select($from)) {
1880             return false;
1881         }
1882
1883         // Clear internal status cache
1884         unset($this->data['STATUS:'.$to]);
1885
1886         $result = $this->execute('UID COPY', array(
1887             $this->compressMessageSet($messages), $this->escape($to)),
1888             self::COMMAND_NORESPONSE);
1889
1890         return ($result == self::ERROR_OK);
1891     }
1892
1893     function move($messages, $from, $to)
1894     {
1895         if (!$this->select($from)) {
1896             return false;
1897         }
1898
1899         if (!$this->data['READ-WRITE']) {
1900             $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE');
1901             return false;
1902         }
1903
1904         $r = $this->copy($messages, $from, $to);
1905
1906         if ($r) {
1907             // Clear internal status cache
1908             unset($this->data['STATUS:'.$from]);
1909
1910             return $this->delete($from, $messages);
1911         }
1912         return $r;
1913     }
1914
1915     // Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about
1916     // 7 times instead :-) See comments on http://uk2.php.net/references and this article:
1917     // http://derickrethans.nl/files/phparch-php-variables-article.pdf
1918     private function parseThread($str, $begin, $end, $root, $parent, $depth, &$depthmap, &$haschildren)
1919     {
1920         $node = array();
1921         if ($str[$begin] != '(') {
1922             $stop = $begin + strspn($str, '1234567890', $begin, $end - $begin);
1923             $msg = substr($str, $begin, $stop - $begin);
1924             if ($msg == 0)
1925                 return $node;
1926             if (is_null($root))
1927                 $root = $msg;
1928             $depthmap[$msg] = $depth;
1929             $haschildren[$msg] = false;
1930             if (!is_null($parent))
1931                 $haschildren[$parent] = true;
1932             if ($stop + 1 < $end)
1933                 $node[$msg] = $this->parseThread($str, $stop + 1, $end, $root, $msg, $depth + 1, $depthmap, $haschildren);
1934             else
1935                 $node[$msg] = array();
1936         } else {
1937             $off = $begin;
1938             while ($off < $end) {
1939                 $start = $off;
1940                 $off++;
1941                 $n = 1;
1942                 while ($n > 0) {
1943                     $p = strpos($str, ')', $off);
1944                     if ($p === false) {
1945                         error_log("Mismatched brackets parsing IMAP THREAD response:");
1946                         error_log(substr($str, ($begin < 10) ? 0 : ($begin - 10), $end - $begin + 20));
1947                         error_log(str_repeat(' ', $off - (($begin < 10) ? 0 : ($begin - 10))));
1948                         return $node;
1949                     }
1950                     $p1 = strpos($str, '(', $off);
1951                     if ($p1 !== false && $p1 < $p) {
1952                         $off = $p1 + 1;
1953                         $n++;
1954                     } else {
1955                         $off = $p + 1;
1956                         $n--;
1957                     }
1958                 }
1959                 $node += $this->parseThread($str, $start + 1, $off - 1, $root, $parent, $depth, $depthmap, $haschildren);
1960             }
1961         }
1962
1963         return $node;
1964     }
1965
1966     function thread($mailbox, $algorithm='REFERENCES', $criteria='', $encoding='US-ASCII')
1967     {
1968         $old_sel = $this->selected;
1969
1970         if (!$this->select($mailbox)) {
1971             return false;
1972         }
1973
1974         // return empty result when folder is empty and we're just after SELECT
1975         if ($old_sel != $mailbox && !$this->data['EXISTS']) {
1976             return array(array(), array(), array());
1977         }
1978
1979         $encoding  = $encoding ? trim($encoding) : 'US-ASCII';
1980         $algorithm = $algorithm ? trim($algorithm) : 'REFERENCES';
1981         $criteria  = $criteria ? 'ALL '.trim($criteria) : 'ALL';
1982         $data      = '';
1983
1984         list($code, $response) = $this->execute('THREAD', array(
1985             $algorithm, $encoding, $criteria));
1986
1987         if ($code == self::ERROR_OK) {
1988             // remove prefix...
1989             $response = substr($response, stripos($response, '* THREAD') + 9);
1990             // ...unilateral untagged server responses
1991             if ($pos = strpos($response, '*')) {
1992                 $response = substr($response, 0, $pos);
1993             }
1994
1995             $response    = str_replace("\r\n", '', $response);
1996             $depthmap    = array();
1997             $haschildren = array();
1998
1999             $tree = $this->parseThread($response, 0, strlen($response),
2000                 null, null, 0, $depthmap, $haschildren);
2001
2002             return array($tree, $depthmap, $haschildren);
2003         }
2004
2005         return false;
2006     }
2007
2008     /**
2009      * Executes SEARCH command
2010      *
2011      * @param string $mailbox    Mailbox name
2012      * @param string $criteria   Searching criteria
2013      * @param bool   $return_uid Enable UID in result instead of sequence ID
2014      * @param array  $items      Return items (MIN, MAX, COUNT, ALL)
2015      *
2016      * @return array Message identifiers or item-value hash 
2017      */
2018     function search($mailbox, $criteria, $return_uid=false, $items=array())
2019     {
2020         $old_sel = $this->selected;
2021
2022         if (!$this->select($mailbox)) {
2023             return false;
2024         }
2025
2026         // return empty result when folder is empty and we're just after SELECT
2027         if ($old_sel != $mailbox && !$this->data['EXISTS']) {
2028             if (!empty($items))
2029                 return array_combine($items, array_fill(0, count($items), 0));
2030             else
2031                 return array();
2032         }
2033
2034         $esearch  = empty($items) ? false : $this->getCapability('ESEARCH');
2035         $criteria = trim($criteria);
2036         $params   = '';
2037
2038         // RFC4731: ESEARCH
2039         if (!empty($items) && $esearch) {
2040             $params .= 'RETURN (' . implode(' ', $items) . ')';
2041         }
2042         if (!empty($criteria)) {
2043             $params .= ($params ? ' ' : '') . $criteria;
2044         }
2045         else {
2046             $params .= 'ALL';
2047         }
2048
2049         list($code, $response) = $this->execute($return_uid ? 'UID SEARCH' : 'SEARCH',
2050             array($params));
2051
2052         if ($code == self::ERROR_OK) {
2053             // remove prefix...
2054             $response = substr($response, stripos($response, 
2055                 $esearch ? '* ESEARCH' : '* SEARCH') + ($esearch ? 10 : 9));
2056             // ...and unilateral untagged server responses
2057             if ($pos = strpos($response, '*')) {
2058                 $response = rtrim(substr($response, 0, $pos));
2059             }
2060
2061             if ($esearch) {
2062                 // Skip prefix: ... (TAG "A285") UID ...
2063                 $this->tokenizeResponse($response, $return_uid ? 2 : 1);
2064
2065                 $result = array();
2066                 for ($i=0; $i<count($items); $i++) {
2067                     // If the SEARCH results in no matches, the server MUST NOT
2068                     // include the item result option in the ESEARCH response
2069                     if ($ret = $this->tokenizeResponse($response, 2)) {
2070                         list ($name, $value) = $ret;
2071                         $result[$name] = $value;
2072                     }
2073                 }
2074
2075                 return $result;
2076             }
2077             else {
2078                 $response = preg_split('/[\s\r\n]+/', $response, -1, PREG_SPLIT_NO_EMPTY);
2079
2080                 if (!empty($items)) {
2081                     $result = array();
2082                     if (in_array('COUNT', $items)) {
2083                         $result['COUNT'] = count($response);
2084                     }
2085                     if (in_array('MIN', $items)) {
2086                         $result['MIN'] = !empty($response) ? min($response) : 0;
2087                     }
2088                     if (in_array('MAX', $items)) {
2089                         $result['MAX'] = !empty($response) ? max($response) : 0;
2090                     }
2091                     if (in_array('ALL', $items)) {
2092                         $result['ALL'] = $this->compressMessageSet($response, true);
2093                     }
2094
2095                     return $result;
2096                 }
2097                 else {
2098                     return $response;
2099                 }
2100             }
2101         }
2102
2103         return false;
2104     }
2105
2106     /**
2107      * Returns list of mailboxes
2108      *
2109      * @param string $ref         Reference name
2110      * @param string $mailbox     Mailbox name
2111      * @param array  $status_opts (see self::_listMailboxes)
2112      * @param array  $select_opts (see self::_listMailboxes)
2113      *
2114      * @return array List of mailboxes or hash of options if $status_opts argument
2115      *               is non-empty.
2116      * @access public
2117      */
2118     function listMailboxes($ref, $mailbox, $status_opts=array(), $select_opts=array())
2119     {
2120         return $this->_listMailboxes($ref, $mailbox, false, $status_opts, $select_opts);
2121     }
2122
2123     /**
2124      * Returns list of subscribed mailboxes
2125      *
2126      * @param string $ref         Reference name
2127      * @param string $mailbox     Mailbox name
2128      * @param array  $status_opts (see self::_listMailboxes)
2129      *
2130      * @return array List of mailboxes or hash of options if $status_opts argument
2131      *               is non-empty.
2132      * @access public
2133      */
2134     function listSubscribed($ref, $mailbox, $status_opts=array())
2135     {
2136         return $this->_listMailboxes($ref, $mailbox, true, $status_opts, NULL);
2137     }
2138
2139     /**
2140      * IMAP LIST/LSUB command
2141      *
2142      * @param string $ref         Reference name
2143      * @param string $mailbox     Mailbox name
2144      * @param bool   $subscribed  Enables returning subscribed mailboxes only
2145      * @param array  $status_opts List of STATUS options (RFC5819: LIST-STATUS)
2146      *                            Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN
2147      * @param array  $select_opts List of selection options (RFC5258: LIST-EXTENDED)
2148      *                            Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE
2149      *
2150      * @return array List of mailboxes or hash of options if $status_ops argument
2151      *               is non-empty.
2152      * @access private
2153      */
2154     private function _listMailboxes($ref, $mailbox, $subscribed=false,
2155         $status_opts=array(), $select_opts=array())
2156     {
2157         if (!strlen($mailbox)) {
2158             $mailbox = '*';
2159         }
2160
2161         $args = array();
2162
2163         if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) {
2164             $select_opts = (array) $select_opts;
2165
2166             $args[] = '(' . implode(' ', $select_opts) . ')';
2167         }
2168
2169         $args[] = $this->escape($ref);
2170         $args[] = $this->escape($mailbox);
2171
2172         if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) {
2173             $status_opts = (array) $status_opts;
2174             $lstatus = true;
2175
2176             $args[] = 'RETURN (STATUS (' . implode(' ', $status_opts) . '))';
2177         }
2178
2179         list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args);
2180
2181         if ($code == self::ERROR_OK) {
2182             $folders = array();
2183             while ($this->tokenizeResponse($response, 1) == '*') {
2184                 $cmd = strtoupper($this->tokenizeResponse($response, 1));
2185                 // * LIST (<options>) <delimiter> <mailbox>
2186                 if (!$lstatus || $cmd == 'LIST' || $cmd == 'LSUB') {
2187                     list($opts, $delim, $mailbox) = $this->tokenizeResponse($response, 3);
2188
2189                     // Add to result array
2190                     if (!$lstatus) {
2191                         $folders[] = $mailbox;
2192                     }
2193                     else {
2194                         $folders[$mailbox] = array();
2195                     }
2196
2197                     // Add to options array
2198                     if (!empty($opts)) {
2199                         if (empty($this->data['LIST'][$mailbox]))
2200                             $this->data['LIST'][$mailbox] = $opts;
2201                         else
2202                             $this->data['LIST'][$mailbox] = array_unique(array_merge(
2203                                 $this->data['LIST'][$mailbox], $opts));
2204                     }
2205                 }
2206                 // * STATUS <mailbox> (<result>)
2207                 else if ($cmd == 'STATUS') {
2208                     list($mailbox, $status) = $this->tokenizeResponse($response, 2);
2209
2210                     for ($i=0, $len=count($status); $i<$len; $i += 2) {
2211                         list($name, $value) = $this->tokenizeResponse($status, 2);
2212                         $folders[$mailbox][$name] = $value;
2213                     }
2214                 }
2215             }
2216
2217             return $folders;
2218         }
2219
2220         return false;
2221     }
2222
2223     function fetchMIMEHeaders($mailbox, $id, $parts, $mime=true)
2224     {
2225         if (!$this->select($mailbox)) {
2226             return false;
2227         }
2228
2229         $result = false;
2230         $parts  = (array) $parts;
2231         $key    = $this->nextTag();
2232         $peeks  = '';
2233         $idx    = 0;
2234         $type   = $mime ? 'MIME' : 'HEADER';
2235
2236         // format request
2237         foreach($parts as $part) {
2238             $peeks[] = "BODY.PEEK[$part.$type]";
2239         }
2240
2241         $request = "$key FETCH $id (" . implode(' ', $peeks) . ')';
2242
2243         // send request
2244         if (!$this->putLine($request)) {
2245             $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
2246             return false;
2247         }
2248
2249         do {
2250             $line = $this->readLine(1024);
2251             $line = $this->multLine($line);
2252
2253             if (preg_match('/BODY\[([0-9\.]+)\.'.$type.'\]/', $line, $matches)) {
2254                 $idx = $matches[1];
2255                 $result[$idx] = preg_replace('/^(\* '.$id.' FETCH \()?\s*BODY\['.$idx.'\.'.$type.'\]\s+/', '', $line);
2256                 $result[$idx] = trim($result[$idx], '"');
2257                 $result[$idx] = rtrim($result[$idx], "\t\r\n\0\x0B");
2258             }
2259         } while (!$this->startsWith($line, $key, true));
2260
2261         return $result;
2262     }
2263
2264     function fetchPartHeader($mailbox, $id, $is_uid=false, $part=NULL)
2265     {
2266         $part = empty($part) ? 'HEADER' : $part.'.MIME';
2267
2268         return $this->handlePartBody($mailbox, $id, $is_uid, $part);
2269     }
2270
2271     function handlePartBody($mailbox, $id, $is_uid=false, $part='', $encoding=NULL, $print=NULL, $file=NULL)
2272     {
2273         if (!$this->select($mailbox)) {
2274             return false;
2275         }
2276
2277         switch ($encoding) {
2278         case 'base64':
2279             $mode = 1;
2280             break;
2281         case 'quoted-printable':
2282             $mode = 2;
2283             break;
2284         case 'x-uuencode':
2285         case 'x-uue':
2286         case 'uue':
2287         case 'uuencode':
2288             $mode = 3;
2289             break;
2290         default:
2291             $mode = 0;
2292         }
2293
2294         // format request
2295         $reply_key = '* ' . $id;
2296         $key       = $this->nextTag();
2297         $request   = $key . ($is_uid ? ' UID' : '') . " FETCH $id (BODY.PEEK[$part])";
2298
2299         // send request
2300         if (!$this->putLine($request)) {
2301             $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
2302             return false;
2303         }
2304
2305         // receive reply line
2306         do {
2307             $line = rtrim($this->readLine(1024));
2308             $a    = explode(' ', $line);
2309         } while (!($end = $this->startsWith($line, $key, true)) && $a[2] != 'FETCH');
2310
2311         $len    = strlen($line);
2312         $result = false;
2313
2314         // handle empty "* X FETCH ()" response
2315         if ($line[$len-1] == ')' && $line[$len-2] != '(') {
2316             // one line response, get everything between first and last quotes
2317             if (substr($line, -4, 3) == 'NIL') {
2318                 // NIL response
2319                 $result = '';
2320             } else {
2321                 $from = strpos($line, '"') + 1;
2322                 $to   = strrpos($line, '"');
2323                 $len  = $to - $from;
2324                 $result = substr($line, $from, $len);
2325             }
2326
2327             if ($mode == 1) {
2328                 $result = base64_decode($result);
2329             }
2330             else if ($mode == 2) {
2331                 $result = quoted_printable_decode($result);
2332             }
2333             else if ($mode == 3) {
2334                 $result = convert_uudecode($result);
2335             }
2336
2337         } else if ($line[$len-1] == '}') {
2338             // multi-line request, find sizes of content and receive that many bytes
2339             $from     = strpos($line, '{') + 1;
2340             $to       = strrpos($line, '}');
2341             $len      = $to - $from;
2342             $sizeStr  = substr($line, $from, $len);
2343             $bytes    = (int)$sizeStr;
2344             $prev     = '';
2345
2346             while ($bytes > 0) {
2347                 $line = $this->readLine(4096);
2348
2349                 if ($line === NULL) {
2350                     break;
2351                 }
2352
2353                 $len  = strlen($line);
2354
2355                 if ($len > $bytes) {
2356                     $line = substr($line, 0, $bytes);
2357                     $len = strlen($line);
2358                 }
2359                 $bytes -= $len;
2360
2361                 // BASE64
2362                 if ($mode == 1) {
2363                     $line = rtrim($line, "\t\r\n\0\x0B");
2364                     // create chunks with proper length for base64 decoding
2365                     $line = $prev.$line;
2366                     $length = strlen($line);
2367                     if ($length % 4) {
2368                         $length = floor($length / 4) * 4;
2369                         $prev = substr($line, $length);
2370                         $line = substr($line, 0, $length);
2371                     }
2372                     else
2373                         $prev = '';
2374                     $line = base64_decode($line);
2375                 // QUOTED-PRINTABLE
2376                 } else if ($mode == 2) {
2377                     $line = rtrim($line, "\t\r\0\x0B");
2378                     $line = quoted_printable_decode($line);
2379                     // Remove NULL characters (#1486189)
2380                     $line = str_replace("\x00", '', $line);
2381                 // UUENCODE
2382                 } else if ($mode == 3) {
2383                     $line = rtrim($line, "\t\r\n\0\x0B");
2384                     if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line))
2385                         continue;
2386                     $line = convert_uudecode($line);
2387                 // default
2388                 } else {
2389                     $line = rtrim($line, "\t\r\n\0\x0B") . "\n";
2390                 }
2391
2392                 if ($file)
2393                     fwrite($file, $line);
2394                 else if ($print)
2395                     echo $line;
2396                 else
2397                     $result .= $line;
2398             }
2399         }
2400
2401         // read in anything up until last line
2402         if (!$end)
2403             do {
2404                 $line = $this->readLine(1024);
2405             } while (!$this->startsWith($line, $key, true));
2406
2407         if ($result !== false) {
2408             if ($file) {
2409                 fwrite($file, $result);
2410             } else if ($print) {
2411                 echo $result;
2412             } else
2413                 return $result;
2414             return true;
2415         }
2416
2417         return false;
2418     }
2419
2420     function createFolder($mailbox)
2421     {
2422         $result = $this->execute('CREATE', array($this->escape($mailbox)),
2423             self::COMMAND_NORESPONSE);
2424
2425         return ($result == self::ERROR_OK);
2426     }
2427
2428     function renameFolder($from, $to)
2429     {
2430         $result = $this->execute('RENAME', array($this->escape($from), $this->escape($to)),
2431             self::COMMAND_NORESPONSE);
2432
2433         return ($result == self::ERROR_OK);
2434     }
2435
2436     function append($mailbox, &$message)
2437     {
2438         if (!$mailbox) {
2439             return false;
2440         }
2441
2442         $message = str_replace("\r", '', $message);
2443         $message = str_replace("\n", "\r\n", $message);
2444
2445         $len = strlen($message);
2446         if (!$len) {
2447             return false;
2448         }
2449
2450         $key = $this->nextTag();
2451         $request = sprintf("$key APPEND %s (\\Seen) {%d%s}", $this->escape($mailbox),
2452             $len, ($this->prefs['literal+'] ? '+' : ''));
2453
2454         if ($this->putLine($request)) {
2455             // Don't wait when LITERAL+ is supported
2456             if (!$this->prefs['literal+']) {
2457                 $line = $this->readReply();
2458
2459                 if ($line[0] != '+') {
2460                     $this->parseResult($line, 'APPEND: ');
2461                     return false;
2462                 }
2463             }
2464
2465             if (!$this->putLine($message)) {
2466                 return false;
2467             }
2468
2469             do {
2470                 $line = $this->readLine();
2471             } while (!$this->startsWith($line, $key, true, true));
2472
2473             // Clear internal status cache
2474             unset($this->data['STATUS:'.$mailbox]);
2475
2476             return ($this->parseResult($line, 'APPEND: ') == self::ERROR_OK);
2477         }
2478         else {
2479             $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
2480         }
2481
2482         return false;
2483     }
2484
2485     function appendFromFile($mailbox, $path, $headers=null)
2486     {
2487         if (!$mailbox) {
2488             return false;
2489         }
2490
2491         // open message file
2492         $in_fp = false;
2493         if (file_exists(realpath($path))) {
2494             $in_fp = fopen($path, 'r');
2495         }
2496         if (!$in_fp) {
2497             $this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading");
2498             return false;
2499         }
2500
2501         $body_separator = "\r\n\r\n";
2502         $len = filesize($path);
2503
2504         if (!$len) {
2505             return false;
2506         }
2507
2508         if ($headers) {
2509             $headers = preg_replace('/[\r\n]+$/', '', $headers);
2510             $len += strlen($headers) + strlen($body_separator);
2511         }
2512
2513         // send APPEND command
2514         $key = $this->nextTag();
2515         $request = sprintf("$key APPEND %s (\\Seen) {%d%s}", $this->escape($mailbox),
2516             $len, ($this->prefs['literal+'] ? '+' : ''));
2517
2518         if ($this->putLine($request)) {
2519             // Don't wait when LITERAL+ is supported
2520             if (!$this->prefs['literal+']) {
2521                 $line = $this->readReply();
2522
2523                 if ($line[0] != '+') {
2524                     $this->parseResult($line, 'APPEND: ');
2525                     return false;
2526                 }
2527             }
2528
2529             // send headers with body separator
2530             if ($headers) {
2531                 $this->putLine($headers . $body_separator, false);
2532             }
2533
2534             // send file
2535             while (!feof($in_fp) && $this->fp) {
2536                 $buffer = fgets($in_fp, 4096);
2537                 $this->putLine($buffer, false);
2538             }
2539             fclose($in_fp);
2540
2541             if (!$this->putLine('')) { // \r\n
2542                 return false;
2543             }
2544
2545             // read response
2546             do {
2547                 $line = $this->readLine();
2548             } while (!$this->startsWith($line, $key, true, true));
2549
2550             // Clear internal status cache
2551             unset($this->data['STATUS:'.$mailbox]);
2552
2553             return ($this->parseResult($line, 'APPEND: ') == self::ERROR_OK);
2554         }
2555         else {
2556             $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
2557         }
2558
2559         return false;
2560     }
2561
2562     function fetchStructureString($mailbox, $id, $is_uid=false)
2563     {
2564         if (!$this->select($mailbox)) {
2565             return false;
2566         }
2567
2568         $key = $this->nextTag();
2569         $result = false;
2570         $command = $key . ($is_uid ? ' UID' : '') ." FETCH $id (BODYSTRUCTURE)";
2571
2572         if ($this->putLine($command)) {
2573             do {
2574                 $line = $this->readLine(5000);
2575                 $line = $this->multLine($line, true);
2576                 if (!preg_match("/^$key /", $line))
2577                     $result .= $line;
2578             } while (!$this->startsWith($line, $key, true, true));
2579
2580             $result = trim(substr($result, strpos($result, 'BODYSTRUCTURE')+13, -1));
2581         }
2582         else {
2583             $this->setError(self::ERROR_COMMAND, "Unable to send command: $command");
2584         }
2585
2586         return $result;
2587     }
2588
2589     function getQuota()
2590     {
2591         /*
2592          * GETQUOTAROOT "INBOX"
2593          * QUOTAROOT INBOX user/rchijiiwa1
2594          * QUOTA user/rchijiiwa1 (STORAGE 654 9765)
2595          * OK Completed
2596          */
2597         $result      = false;
2598         $quota_lines = array();
2599         $key         = $this->nextTag();
2600         $command     = $key . ' GETQUOTAROOT INBOX';
2601
2602         // get line(s) containing quota info
2603         if ($this->putLine($command)) {
2604             do {
2605                 $line = rtrim($this->readLine(5000));
2606                 if (preg_match('/^\* QUOTA /', $line)) {
2607                     $quota_lines[] = $line;
2608                 }
2609             } while (!$this->startsWith($line, $key, true, true));
2610         }
2611         else {
2612             $this->setError(self::ERROR_COMMAND, "Unable to send command: $command");
2613         }
2614
2615         // return false if not found, parse if found
2616         $min_free = PHP_INT_MAX;
2617         foreach ($quota_lines as $key => $quota_line) {
2618             $quota_line   = str_replace(array('(', ')'), '', $quota_line);
2619             $parts        = explode(' ', $quota_line);
2620             $storage_part = array_search('STORAGE', $parts);
2621
2622             if (!$storage_part) {
2623                 continue;
2624             }
2625
2626             $used  = intval($parts[$storage_part+1]);
2627             $total = intval($parts[$storage_part+2]);
2628             $free  = $total - $used;
2629
2630             // return lowest available space from all quotas
2631             if ($free < $min_free) {
2632                 $min_free          = $free;
2633                 $result['used']    = $used;
2634                 $result['total']   = $total;
2635                 $result['percent'] = min(100, round(($used/max(1,$total))*100));
2636                 $result['free']    = 100 - $result['percent'];
2637             }
2638         }
2639
2640         return $result;
2641     }
2642
2643     /**
2644      * Send the SETACL command (RFC4314)
2645      *
2646      * @param string $mailbox Mailbox name
2647      * @param string $user    User name
2648      * @param mixed  $acl     ACL string or array
2649      *
2650      * @return boolean True on success, False on failure
2651      *
2652      * @access public
2653      * @since 0.5-beta
2654      */
2655     function setACL($mailbox, $user, $acl)
2656     {
2657         if (is_array($acl)) {
2658             $acl = implode('', $acl);
2659         }
2660
2661         $result = $this->execute('SETACL', array(
2662             $this->escape($mailbox), $this->escape($user), strtolower($acl)),
2663             self::COMMAND_NORESPONSE);
2664
2665         return ($result == self::ERROR_OK);
2666     }
2667
2668     /**
2669      * Send the DELETEACL command (RFC4314)
2670      *
2671      * @param string $mailbox Mailbox name
2672      * @param string $user    User name
2673      *
2674      * @return boolean True on success, False on failure
2675      *
2676      * @access public
2677      * @since 0.5-beta
2678      */
2679     function deleteACL($mailbox, $user)
2680     {
2681         $result = $this->execute('DELETEACL', array(
2682             $this->escape($mailbox), $this->escape($user)),
2683             self::COMMAND_NORESPONSE);
2684
2685         return ($result == self::ERROR_OK);
2686     }
2687
2688     /**
2689      * Send the GETACL command (RFC4314)
2690      *
2691      * @param string $mailbox Mailbox name
2692      *
2693      * @return array User-rights array on success, NULL on error
2694      * @access public
2695      * @since 0.5-beta
2696      */
2697     function getACL($mailbox)
2698     {
2699         list($code, $response) = $this->execute('GETACL', array($this->escape($mailbox)));
2700
2701         if ($code == self::ERROR_OK && preg_match('/^\* ACL /i', $response)) {
2702             // Parse server response (remove "* ACL ")
2703             $response = substr($response, 6);
2704             $ret  = $this->tokenizeResponse($response);
2705             $mbox = array_shift($ret);
2706             $size = count($ret);
2707
2708             // Create user-rights hash array
2709             // @TODO: consider implementing fixACL() method according to RFC4314.2.1.1
2710             // so we could return only standard rights defined in RFC4314,
2711             // excluding 'c' and 'd' defined in RFC2086.
2712             if ($size % 2 == 0) {
2713                 for ($i=0; $i<$size; $i++) {
2714                     $ret[$ret[$i]] = str_split($ret[++$i]);
2715                     unset($ret[$i-1]);
2716                     unset($ret[$i]);
2717                 }
2718                 return $ret;
2719             }
2720
2721             $this->setError(self::ERROR_COMMAND, "Incomplete ACL response");
2722             return NULL;
2723         }
2724
2725         return NULL;
2726     }
2727
2728     /**
2729      * Send the LISTRIGHTS command (RFC4314)
2730      *
2731      * @param string $mailbox Mailbox name
2732      * @param string $user    User name
2733      *
2734      * @return array List of user rights
2735      * @access public
2736      * @since 0.5-beta
2737      */
2738     function listRights($mailbox, $user)
2739     {
2740         list($code, $response) = $this->execute('LISTRIGHTS', array(
2741             $this->escape($mailbox), $this->escape($user)));
2742
2743         if ($code == self::ERROR_OK && preg_match('/^\* LISTRIGHTS /i', $response)) {
2744             // Parse server response (remove "* LISTRIGHTS ")
2745             $response = substr($response, 13);
2746
2747             $ret_mbox = $this->tokenizeResponse($response, 1);
2748             $ret_user = $this->tokenizeResponse($response, 1);
2749             $granted  = $this->tokenizeResponse($response, 1);
2750             $optional = trim($response);
2751
2752             return array(
2753                 'granted'  => str_split($granted),
2754                 'optional' => explode(' ', $optional),
2755             );
2756         }
2757
2758         return NULL;
2759     }
2760
2761     /**
2762      * Send the MYRIGHTS command (RFC4314)
2763      *
2764      * @param string $mailbox Mailbox name
2765      *
2766      * @return array MYRIGHTS response on success, NULL on error
2767      * @access public
2768      * @since 0.5-beta
2769      */
2770     function myRights($mailbox)
2771     {
2772         list($code, $response) = $this->execute('MYRIGHTS', array($this->escape($mailbox)));
2773
2774         if ($code == self::ERROR_OK && preg_match('/^\* MYRIGHTS /i', $response)) {
2775             // Parse server response (remove "* MYRIGHTS ")
2776             $response = substr($response, 11);
2777
2778             $ret_mbox = $this->tokenizeResponse($response, 1);
2779             $rights   = $this->tokenizeResponse($response, 1);
2780
2781             return str_split($rights);
2782         }
2783
2784         return NULL;
2785     }
2786
2787     /**
2788      * Send the SETMETADATA command (RFC5464)
2789      *
2790      * @param string $mailbox Mailbox name
2791      * @param array  $entries Entry-value array (use NULL value as NIL)
2792      *
2793      * @return boolean True on success, False on failure
2794      * @access public
2795      * @since 0.5-beta
2796      */
2797     function setMetadata($mailbox, $entries)
2798     {
2799         if (!is_array($entries) || empty($entries)) {
2800             $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
2801             return false;
2802         }
2803
2804         foreach ($entries as $name => $value) {
2805             if ($value === null) {
2806                 $value = 'NIL';
2807             }
2808             else {
2809                 $value = sprintf("{%d}\r\n%s", strlen($value), $value);
2810             }
2811             $entries[$name] = $this->escape($name) . ' ' . $value;
2812         }
2813
2814         $entries = implode(' ', $entries);
2815         $result = $this->execute('SETMETADATA', array(
2816             $this->escape($mailbox), '(' . $entries . ')'),
2817             self::COMMAND_NORESPONSE);
2818
2819         return ($result == self::ERROR_OK);
2820     }
2821
2822     /**
2823      * Send the SETMETADATA command with NIL values (RFC5464)
2824      *
2825      * @param string $mailbox Mailbox name
2826      * @param array  $entries Entry names array
2827      *
2828      * @return boolean True on success, False on failure
2829      *
2830      * @access public
2831      * @since 0.5-beta
2832      */
2833     function deleteMetadata($mailbox, $entries)
2834     {
2835         if (!is_array($entries) && !empty($entries)) {
2836             $entries = explode(' ', $entries);
2837         }
2838
2839         if (empty($entries)) {
2840             $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
2841             return false;
2842         }
2843
2844         foreach ($entries as $entry) {
2845             $data[$entry] = NULL;
2846         }
2847
2848         return $this->setMetadata($mailbox, $data);
2849     }
2850
2851     /**
2852      * Send the GETMETADATA command (RFC5464)
2853      *
2854      * @param string $mailbox Mailbox name
2855      * @param array  $entries Entries
2856      * @param array  $options Command options (with MAXSIZE and DEPTH keys)
2857      *
2858      * @return array GETMETADATA result on success, NULL on error
2859      *
2860      * @access public
2861      * @since 0.5-beta
2862      */
2863     function getMetadata($mailbox, $entries, $options=array())
2864     {
2865         if (!is_array($entries)) {
2866             $entries = array($entries);
2867         }
2868
2869         // create entries string
2870         foreach ($entries as $idx => $name) {
2871             $entries[$idx] = $this->escape($name);
2872         }
2873
2874         $optlist = '';
2875         $entlist = '(' . implode(' ', $entries) . ')';
2876
2877         // create options string
2878         if (is_array($options)) {
2879             $options = array_change_key_case($options, CASE_UPPER);
2880             $opts = array();
2881
2882             if (!empty($options['MAXSIZE'])) {
2883                 $opts[] = 'MAXSIZE '.intval($options['MAXSIZE']);
2884             }
2885             if (!empty($options['DEPTH'])) {
2886                 $opts[] = 'DEPTH '.intval($options['DEPTH']);
2887             }
2888
2889             if ($opts) {
2890                 $optlist = '(' . implode(' ', $opts) . ')';
2891             }
2892         }
2893
2894         $optlist .= ($optlist ? ' ' : '') . $entlist;
2895
2896         list($code, $response) = $this->execute('GETMETADATA', array(
2897             $this->escape($mailbox), $optlist));
2898
2899         if ($code == self::ERROR_OK) {
2900             $result = array();
2901             $data   = $this->tokenizeResponse($response);
2902
2903             // The METADATA response can contain multiple entries in a single
2904             // response or multiple responses for each entry or group of entries
2905             if (!empty($data) && ($size = count($data))) {
2906                 for ($i=0; $i<$size; $i++) {
2907                     if (isset($mbox) && is_array($data[$i])) {
2908                         $size_sub = count($data[$i]);
2909                         for ($x=0; $x<$size_sub; $x++) {
2910                             $result[$mbox][$data[$i][$x]] = $data[$i][++$x];
2911                         }
2912                         unset($data[$i]);
2913                     }
2914                     else if ($data[$i] == '*') {
2915                         if ($data[$i+1] == 'METADATA') {
2916                             $mbox = $data[$i+2];
2917                             unset($data[$i]);   // "*"
2918                             unset($data[++$i]); // "METADATA"
2919                             unset($data[++$i]); // Mailbox
2920                         }
2921                         // get rid of other untagged responses
2922                         else {
2923                             unset($mbox);
2924                             unset($data[$i]);
2925                         }
2926                     }
2927                     else if (isset($mbox)) {
2928                         $result[$mbox][$data[$i]] = $data[++$i];
2929                         unset($data[$i]);
2930                         unset($data[$i-1]);
2931                     }
2932                     else {
2933                         unset($data[$i]);
2934                     }
2935                 }
2936             }
2937
2938             return $result;
2939         }
2940
2941         return NULL;
2942     }
2943
2944     /**
2945      * Send the SETANNOTATION command (draft-daboo-imap-annotatemore)
2946      *
2947      * @param string $mailbox Mailbox name
2948      * @param array  $data    Data array where each item is an array with
2949      *                        three elements: entry name, attribute name, value
2950      *
2951      * @return boolean True on success, False on failure
2952      * @access public
2953      * @since 0.5-beta
2954      */
2955     function setAnnotation($mailbox, $data)
2956     {
2957         if (!is_array($data) || empty($data)) {
2958             $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
2959             return false;
2960         }
2961
2962         foreach ($data as $entry) {
2963             $name  = $entry[0];
2964             $attr  = $entry[1];
2965             $value = $entry[2];
2966
2967             if ($value === null) {
2968                 $value = 'NIL';
2969             }
2970             else {
2971                 $value = sprintf("{%d}\r\n%s", strlen($value), $value);
2972             }
2973
2974             // ANNOTATEMORE drafts before version 08 require quoted parameters
2975             $entries[] = sprintf('%s (%s %s)',
2976                 $this->escape($name, true), $this->escape($attr, true), $value);
2977         }
2978
2979         $entries = implode(' ', $entries);
2980         $result  = $this->execute('SETANNOTATION', array(
2981             $this->escape($mailbox), $entries), self::COMMAND_NORESPONSE);
2982
2983         return ($result == self::ERROR_OK);
2984     }
2985
2986     /**
2987      * Send the SETANNOTATION command with NIL values (draft-daboo-imap-annotatemore)
2988      *
2989      * @param string $mailbox Mailbox name
2990      * @param array  $data    Data array where each item is an array with
2991      *                        two elements: entry name and attribute name
2992      *
2993      * @return boolean True on success, False on failure
2994      *
2995      * @access public
2996      * @since 0.5-beta
2997      */
2998     function deleteAnnotation($mailbox, $data)
2999     {
3000         if (!is_array($data) || empty($data)) {
3001             $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
3002             return false;
3003         }
3004
3005         return $this->setAnnotation($mailbox, $data);
3006     }
3007
3008     /**
3009      * Send the GETANNOTATION command (draft-daboo-imap-annotatemore)
3010      *
3011      * @param string $mailbox Mailbox name
3012      * @param array  $entries Entries names
3013      * @param array  $attribs Attribs names
3014      *
3015      * @return array Annotations result on success, NULL on error
3016      *
3017      * @access public
3018      * @since 0.5-beta
3019      */
3020     function getAnnotation($mailbox, $entries, $attribs)
3021     {
3022         if (!is_array($entries)) {
3023             $entries = array($entries);
3024         }
3025         // create entries string
3026         // ANNOTATEMORE drafts before version 08 require quoted parameters
3027         foreach ($entries as $idx => $name) {
3028             $entries[$idx] = $this->escape($name, true);
3029         }
3030         $entries = '(' . implode(' ', $entries) . ')';
3031
3032         if (!is_array($attribs)) {
3033             $attribs = array($attribs);
3034         }
3035         // create entries string
3036         foreach ($attribs as $idx => $name) {
3037             $attribs[$idx] = $this->escape($name, true);
3038         }
3039         $attribs = '(' . implode(' ', $attribs) . ')';
3040
3041         list($code, $response) = $this->execute('GETANNOTATION', array(
3042             $this->escape($mailbox), $entries, $attribs));
3043
3044         if ($code == self::ERROR_OK) {
3045             $result = array();
3046             $data   = $this->tokenizeResponse($response);
3047
3048             // Here we returns only data compatible with METADATA result format
3049             if (!empty($data) && ($size = count($data))) {
3050                 for ($i=0; $i<$size; $i++) {
3051                     $entry = $data[$i];
3052                     if (isset($mbox) && is_array($entry)) {
3053                         $attribs = $entry;
3054                         $entry   = $last_entry;
3055                     }
3056                     else if ($entry == '*') {
3057                         if ($data[$i+1] == 'ANNOTATION') {
3058                             $mbox = $data[$i+2];
3059                             unset($data[$i]);   // "*"
3060                             unset($data[++$i]); // "ANNOTATION"
3061                             unset($data[++$i]); // Mailbox
3062                         }
3063                         // get rid of other untagged responses
3064                         else {
3065                             unset($mbox);
3066                             unset($data[$i]);
3067                         }
3068                         continue;
3069                     }
3070                     else if (isset($mbox)) {
3071                         $attribs = $data[++$i];
3072                     }
3073                     else {
3074                         unset($data[$i]);
3075                         continue;
3076                     }
3077
3078                     if (!empty($attribs)) {
3079                         for ($x=0, $len=count($attribs); $x<$len;) {
3080                             $attr  = $attribs[$x++];
3081                             $value = $attribs[$x++];
3082                             if ($attr == 'value.priv') {
3083                                 $result[$mbox]['/private' . $entry] = $value;
3084                             }
3085                             else if ($attr == 'value.shared') {
3086                                 $result[$mbox]['/shared' . $entry] = $value;
3087                             }
3088                         }
3089                     }
3090                     $last_entry = $entry;
3091                     unset($data[$i]);
3092                 }
3093             }
3094
3095             return $result;
3096         }
3097
3098         return NULL;
3099     }
3100
3101     /**
3102      * Creates next command identifier (tag)
3103      *
3104      * @return string Command identifier
3105      * @access public
3106      * @since 0.5-beta
3107      */
3108     function nextTag()
3109     {
3110         $this->cmd_num++;
3111         $this->cmd_tag = sprintf('A%04d', $this->cmd_num);
3112
3113         return $this->cmd_tag;
3114     }
3115
3116     /**
3117      * Sends IMAP command and parses result
3118      *
3119      * @param string $command   IMAP command
3120      * @param array  $arguments Command arguments
3121      * @param int    $options   Execution options
3122      *
3123      * @return mixed Response code or list of response code and data
3124      * @access public
3125      * @since 0.5-beta
3126      */
3127     function execute($command, $arguments=array(), $options=0)
3128     {
3129         $tag      = $this->nextTag();
3130         $query    = $tag . ' ' . $command;
3131         $noresp   = ($options & self::COMMAND_NORESPONSE);
3132         $response = $noresp ? null : '';
3133
3134         if (!empty($arguments)) {
3135             $query .= ' ' . implode(' ', $arguments);
3136         }
3137
3138         // Send command
3139         if (!$this->putLineC($query)) {
3140             $this->setError(self::ERROR_COMMAND, "Unable to send command: $query");
3141             return $noresp ? self::ERROR_COMMAND : array(self::ERROR_COMMAND, '');
3142         }
3143
3144         // Parse response
3145         do {
3146             $line = $this->readLine(4096);
3147             if ($response !== null) {
3148                 $response .= $line;
3149             }
3150         } while (!$this->startsWith($line, $tag . ' ', true, true));
3151
3152         $code = $this->parseResult($line, $command . ': ');
3153
3154         // Remove last line from response
3155         if ($response) {
3156             $line_len = min(strlen($response), strlen($line) + 2);
3157             $response = substr($response, 0, -$line_len);
3158         }
3159
3160         // optional CAPABILITY response
3161         if (($options & self::COMMAND_CAPABILITY) && $code == self::ERROR_OK
3162             && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)
3163         ) {
3164             $this->parseCapability($matches[1], true);
3165         }
3166
3167         // return last line only (without command tag, result and response code)
3168         if ($line && ($options & self::COMMAND_LASTLINE)) {
3169             $response = preg_replace("/^$tag (OK|NO|BAD|BYE|PREAUTH)?\s*(\[[a-z-]+\])?\s*/i", '', trim($line));
3170         }
3171
3172         return $noresp ? $code : array($code, $response);
3173     }
3174
3175     /**
3176      * Splits IMAP response into string tokens
3177      *
3178      * @param string &$str The IMAP's server response
3179      * @param int    $num  Number of tokens to return
3180      *
3181      * @return mixed Tokens array or string if $num=1
3182      * @access public
3183      * @since 0.5-beta
3184      */
3185     static function tokenizeResponse(&$str, $num=0)
3186     {
3187         $result = array();
3188
3189         while (!$num || count($result) < $num) {
3190             // remove spaces from the beginning of the string
3191             $str = ltrim($str);
3192
3193             switch ($str[0]) {
3194
3195             // String literal
3196             case '{':
3197                 if (($epos = strpos($str, "}\r\n", 1)) == false) {
3198                     // error
3199                 }
3200                 if (!is_numeric(($bytes = substr($str, 1, $epos - 1)))) {
3201                     // error
3202                 }
3203                 $result[] = substr($str, $epos + 3, $bytes);
3204                 // Advance the string
3205                 $str = substr($str, $epos + 3 + $bytes);
3206                 break;
3207
3208             // Quoted string
3209             case '"':
3210                 $len = strlen($str);
3211
3212                 for ($pos=1; $pos<$len; $pos++) {
3213                     if ($str[$pos] == '"') {
3214                         break;
3215                     }
3216                     if ($str[$pos] == "\\") {
3217                         if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") {
3218                             $pos++;
3219                         }
3220                     }
3221                 }
3222                 if ($str[$pos] != '"') {
3223                     // error
3224                 }
3225                 // we need to strip slashes for a quoted string
3226                 $result[] = stripslashes(substr($str, 1, $pos - 1));
3227                 $str      = substr($str, $pos + 1);
3228                 break;
3229
3230             // Parenthesized list
3231             case '(':
3232                 $str = substr($str, 1);
3233                 $result[] = self::tokenizeResponse($str);
3234                 break;
3235             case ')':
3236                 $str = substr($str, 1);
3237                 return $result;
3238                 break;
3239
3240             // String atom, number, NIL, *, %
3241             default:
3242                 // empty or one character
3243                 if ($str === '') {
3244                     break 2;
3245                 }
3246                 if (strlen($str) < 2) {
3247                     $result[] = $str;
3248                     $str = '';
3249                     break;
3250                 }
3251
3252                 // excluded chars: SP, CTL, (, ), {, ", ], %
3253                 if (preg_match('/^([\x21\x23\x24\x26\x27\x2A-\x5C\x5E-\x7A\x7C-\x7E]+)/', $str, $m)) {
3254                     $result[] = $m[1] == 'NIL' ? NULL : $m[1];
3255                     $str = substr($str, strlen($m[1]));
3256                 }
3257                 break;
3258             }
3259         }
3260
3261         return $num == 1 ? $result[0] : $result;
3262     }
3263
3264     private function _xor($string, $string2)
3265     {
3266         $result = '';
3267         $size   = strlen($string);
3268
3269         for ($i=0; $i<$size; $i++) {
3270             $result .= chr(ord($string[$i]) ^ ord($string2[$i]));
3271         }
3272
3273         return $result;
3274     }
3275
3276     /**
3277      * Converts datetime string into unix timestamp
3278      *
3279      * @param string $date Date string
3280      *
3281      * @return int Unix timestamp
3282      */
3283     private function strToTime($date)
3284     {
3285         // support non-standard "GMTXXXX" literal
3286         $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date);
3287
3288         // if date parsing fails, we have a date in non-rfc format.
3289         // remove token from the end and try again
3290         while (($ts = intval(@strtotime($date))) <= 0) {
3291             $d = explode(' ', $date);
3292             array_pop($d);
3293             if (!$d) {
3294                 break;
3295             }
3296             $date = implode(' ', $d);
3297         }
3298
3299         $ts = (int) $ts;
3300
3301         return $ts < 0 ? 0 : $ts;
3302     }
3303
3304     private function parseCapability($str, $trusted=false)
3305     {
3306         $str = preg_replace('/^\* CAPABILITY /i', '', $str);
3307
3308         $this->capability = explode(' ', strtoupper($str));
3309
3310         if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) {
3311             $this->prefs['literal+'] = true;
3312         }
3313
3314         if ($trusted) {
3315             $this->capability_readed = true;
3316         }
3317     }
3318
3319     /**
3320      * Escapes a string when it contains special characters (RFC3501)
3321      *
3322      * @param string  $string       IMAP string
3323      * @param boolean $force_quotes Forces string quoting
3324      *
3325      * @return string Escaped string
3326      * @todo String literals, lists
3327      */
3328     static function escape($string, $force_quotes=false)
3329     {
3330         if ($string === null) {
3331             return 'NIL';
3332         }
3333         else if ($string === '') {
3334             return '""';
3335         }
3336         // need quoted-string? find special chars: SP, CTL, (, ), {, %, *, ", \, ]
3337         // plus [ character as a workaround for DBMail's bug (#1487766)
3338         else if ($force_quotes ||
3339             preg_match('/([\x00-\x20\x28-\x29\x7B\x25\x2A\x22\x5B\x5C\x5D\x7F]+)/', $string)
3340         ) {
3341             return '"' . strtr($string, array('"'=>'\\"', '\\' => '\\\\')) . '"';
3342         }
3343
3344         // atom
3345         return $string;
3346     }
3347
3348     static function unEscape($string)
3349     {
3350         return strtr($string, array('\\"'=>'"', '\\\\' => '\\'));
3351     }
3352
3353     /**
3354      * Set the value of the debugging flag.
3355      *
3356      * @param   boolean $debug      New value for the debugging flag.
3357      *
3358      * @access  public
3359      * @since   0.5-stable
3360      */
3361     function setDebug($debug, $handler = null)
3362     {
3363         $this->_debug = $debug;
3364         $this->_debug_handler = $handler;
3365     }
3366
3367     /**
3368      * Write the given debug text to the current debug output handler.
3369      *
3370      * @param   string  $message    Debug mesage text.
3371      *
3372      * @access  private
3373      * @since   0.5-stable
3374      */
3375     private function debug($message)
3376     {
3377         if ($this->_debug_handler) {
3378             call_user_func_array($this->_debug_handler, array(&$this, $message));
3379         } else {
3380             echo "DEBUG: $message\n";
3381         }
3382     }
3383
3384 }