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

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