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

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