source: github/program/include/rcube_imap_generic.php @ 4cb6675

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