source: subversion/branches/devel-framework/roundcubemail/program/include/rcube_imap_generic.php @ 5827

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