source: subversion/trunk/roundcubemail/program/include/rcube_imap_generic.php @ 5839

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