source: github/program/include/rcube_imap_generic.php @ 8578831

release-0.8
Last change on this file since 8578831 was 8578831, checked in by Aleksander Machniak <alec@…>, 12 months ago

Fix Call to undefined method rcube_mail_header::get() in show_additional_headers plugin (#1488489)

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