source: github/program/include/rcube_imap_generic.php @ 3978cbf4

HEADcourier-fixdev-browser-capabilitiespdorelease-0.6release-0.7release-0.8
Last change on this file since 3978cbf4 was 3978cbf4, checked in by alecpl <alec@…>, 3 years ago
  • use @ operator for fclose() on connection handle
  • Property mode set to 100644
File size: 58.7 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_imap_generic.php                                |
6 |                                                                       |
7 | This file is part of the RoundCube Webmail client                     |
8 | Copyright (C) 2005-2010, RoundCube Dev. - Switzerland                 |
9 | Licensed under the GNU GPL                                            |
10 |                                                                       |
11 | PURPOSE:                                                              |
12 |   Provide alternative IMAP library that doesn't rely on the standard  |
13 |   C-Client based version. This allows to function regardless          |
14 |   of whether or not the PHP build it's running on has IMAP            |
15 |   functionality built-in.                                             |
16 |                                                                       |
17 |   Based on Iloha IMAP Library. See http://ilohamail.org/ for details  |
18 |                                                                       |
19 +-----------------------------------------------------------------------+
20 | Author: Aleksander Machniak <alec@alec.pl>                            |
21 | Author: Ryo Chijiiwa <Ryo@IlohaMail.org>                              |
22 +-----------------------------------------------------------------------+
23
24 $Id$
25
26*/
27
28
29/**
30 * Struct representing an e-mail message header
31 *
32 * @package    Mail
33 * @author     Aleksander Machniak <alec@alec.pl>
34 */
35class rcube_mail_header
36{
37        public $id;
38        public $uid;
39        public $subject;
40        public $from;
41        public $to;
42        public $cc;
43        public $replyto;
44        public $in_reply_to;
45        public $date;
46        public $messageID;
47        public $size;
48        public $encoding;
49        public $charset;
50        public $ctype;
51        public $flags;
52        public $timestamp;
53        public $f;
54        public $body_structure;
55        public $internaldate;
56        public $references;
57        public $priority;
58        public $mdn_to;
59        public $mdn_sent = false;
60        public $is_draft = false;
61        public $seen = false;
62        public $deleted = false;
63        public $recent = false;
64        public $answered = false;
65        public $forwarded = false;
66        public $junk = false;
67        public $flagged = false;
68        public $has_children = false;
69        public $depth = 0;
70        public $unread_children = 0;
71        public $others = array();
72}
73
74// For backward compatibility with cached messages (#1486602)
75class iilBasicHeader extends rcube_mail_header
76{
77}
78
79/**
80 * PHP based wrapper class to connect to an IMAP server
81 *
82 * @package    Mail
83 * @author     Aleksander Machniak <alec@alec.pl>
84 */
85class rcube_imap_generic
86{
87    public $error;
88    public $errornum;
89        public $message;
90        public $rootdir;
91        public $delimiter;
92        public $permanentflags = array();
93    public $flags = array(
94        'SEEN'     => '\\Seen',
95        'DELETED'  => '\\Deleted',
96        'RECENT'   => '\\Recent',
97        'ANSWERED' => '\\Answered',
98        'DRAFT'    => '\\Draft',
99        'FLAGGED'  => '\\Flagged',
100        'FORWARDED' => '$Forwarded',
101        'MDNSENT'  => '$MDNSent',
102        '*'        => '\\*',
103    );
104
105        private $exists;
106        private $recent;
107    private $selected;
108        private $fp;
109        private $host;
110        private $logged = false;
111        private $capability = array();
112        private $capability_readed = false;
113    private $prefs;
114
115    /**
116     * Object constructor
117     */
118    function __construct()
119    {
120    }
121             
122    private function putLine($string, $endln=true)
123    {
124        if (!$this->fp)
125            return false;
126
127                if (!empty($this->prefs['debug_mode'])) {
128                write_log('imap', 'C: '. rtrim($string));
129            }
130       
131        return fputs($this->fp, $string . ($endln ? "\r\n" : ''));
132    }
133
134    // $this->putLine replacement with Command Continuation Requests (RFC3501 7.5) support
135    private function putLineC($string, $endln=true)
136    {
137        if (!$this->fp)
138            return NULL;
139
140            if ($endln)
141                    $string .= "\r\n";
142
143            $res = 0;
144            if ($parts = preg_split('/(\{[0-9]+\}\r\n)/m', $string, -1, PREG_SPLIT_DELIM_CAPTURE)) {
145                    for ($i=0, $cnt=count($parts); $i<$cnt; $i++) {
146                            if (preg_match('/^\{[0-9]+\}\r\n$/', $parts[$i+1])) {
147                                    $bytes = $this->putLine($parts[$i].$parts[$i+1], false);
148                    if ($bytes === false)
149                        return false;
150                    $res += $bytes;
151                                    $line = $this->readLine(1000);
152                                    // handle error in command
153                                    if ($line[0] != '+')
154                                            return false;
155                                    $i++;
156                            }
157                            else {
158                                    $bytes = $this->putLine($parts[$i], false);
159                    if ($bytes === false)
160                        return false;
161                    $res += $bytes;
162                }
163                    }
164            }
165
166            return $res;
167    }
168
169    private function readLine($size=1024)
170    {
171                $line = '';
172
173            if (!$this->fp) {
174                return NULL;
175            }
176   
177            if (!$size) {
178                    $size = 1024;
179            }
180   
181            do {
182                    if (feof($this->fp)) {
183                            return $line ? $line : NULL;
184                    }
185               
186                $buffer = fgets($this->fp, $size);
187
188                if ($buffer === false) {
189                @fclose($this->fp);
190                $this->fp = null;
191                        break;
192                }
193                    if (!empty($this->prefs['debug_mode'])) {
194                            write_log('imap', 'S: '. chop($buffer));
195                }
196            $line .= $buffer;
197            } while ($buffer[strlen($buffer)-1] != "\n");
198
199            return $line;
200    }
201
202    private function multLine($line, $escape=false)
203    {
204            $line = chop($line);
205            if (preg_match('/\{[0-9]+\}$/', $line)) {
206                    $out = '';
207       
208                    preg_match_all('/(.*)\{([0-9]+)\}$/', $line, $a);
209                    $bytes = $a[2][0];
210                    while (strlen($out) < $bytes) {
211                            $line = $this->readBytes($bytes); 
212                            if ($line === NULL)
213                                    break;
214                            $out .= $line;
215                    }
216
217                    $line = $a[1][0] . '"' . ($escape ? $this->Escape($out) : $out) . '"';
218            }
219       
220        return $line;
221    }
222
223    private function readBytes($bytes)
224    {
225            $data = '';
226            $len  = 0;
227            while ($len < $bytes && !feof($this->fp))
228            {
229                    $d = fread($this->fp, $bytes-$len);
230                    if (!empty($this->prefs['debug_mode'])) {
231                            write_log('imap', 'S: '. $d);
232            }
233            $data .= $d;
234                    $data_len = strlen($data);
235                    if ($len == $data_len) {
236                    break; // nothing was read -> exit to avoid apache lockups
237                }
238                $len = $data_len;
239            }
240       
241            return $data;
242    }
243
244    // don't use it in loops, until you exactly know what you're doing
245    private function readReply()
246    {
247            do {
248                    $line = trim($this->readLine(1024));
249            } while ($line[0] == '*');
250
251            return $line;
252    }
253
254    private function parseResult($string)
255    {
256            $a = explode(' ', trim($string));
257            if (count($a) >= 2) {
258                    $res = strtoupper($a[1]);
259                    if ($res == 'OK') {
260                            return 0;
261                    } else if ($res == 'NO') {
262                            return -1;
263                    } else if ($res == 'BAD') {
264                            return -2;
265                    } else if ($res == 'BYE') {
266                @fclose($this->fp);
267                $this->fp = null;
268                            return -3;
269                    }
270            }
271            return -4;
272    }
273
274    // check if $string starts with $match (or * BYE/BAD)
275    private function startsWith($string, $match, $error=false, $nonempty=false)
276    {
277            $len = strlen($match);
278            if ($len == 0) {
279                    return false;
280            }
281        if (!$this->fp) {
282            return true;
283        }
284            if (strncmp($string, $match, $len) == 0) {
285                    return true;
286            }
287            if ($error && preg_match('/^\* (BYE|BAD) /i', $string, $m)) {
288            if (strtoupper($m[1]) == 'BYE') {
289                @fclose($this->fp);
290                $this->fp = null;
291            }
292                    return true;
293            }
294        if ($nonempty && !strlen($string)) {
295            return true;
296        }
297            return false;
298    }
299
300    private function startsWithI($string, $match, $error=false, $nonempty=false)
301    {
302            $len = strlen($match);
303            if ($len == 0) {
304                    return false;
305            }
306        if (!$this->fp) {
307            return true;
308        }
309            if (strncasecmp($string, $match, $len) == 0) {
310                    return true;
311            }
312            if ($error && preg_match('/^\* (BYE|BAD) /i', $string, $m)) {
313            if (strtoupper($m[1]) == 'BYE') {
314                @fclose($this->fp);
315                $this->fp = null;
316            }
317                    return true;
318            }
319        if ($nonempty && !strlen($string)) {
320            return true;
321        }
322            return false;
323    }
324
325    function getCapability($name)
326    {
327            if (in_array($name, $this->capability)) {
328                    return true;
329            }
330            else if ($this->capability_readed) {
331                    return false;
332            }
333
334            // get capabilities (only once) because initial
335            // optional CAPABILITY response may differ
336            $this->capability = array();
337
338            if (!$this->putLine("cp01 CAPABILITY")) {
339            return false;
340        }
341            do {
342                    $line = trim($this->readLine(1024));
343                    $a = explode(' ', $line);
344                    if ($line[0] == '*') {
345                            while (list($k, $w) = each($a)) {
346                                    if ($w != '*' && $w != 'CAPABILITY')
347                                        $this->capability[] = strtoupper($w);
348                            }
349                    }
350            } while ($a[0] != 'cp01');
351       
352            $this->capability_readed = true;
353
354            if (in_array($name, $this->capability)) {
355                    return true;
356            }
357
358            return false;
359    }
360
361    function clearCapability()
362    {
363            $this->capability = array();
364            $this->capability_readed = false;
365    }
366
367    function authenticate($user, $pass, $encChallenge)
368    {
369        $ipad = '';
370        $opad = '';
371   
372        // initialize ipad, opad
373        for ($i=0; $i<64; $i++) {
374            $ipad .= chr(0x36);
375            $opad .= chr(0x5C);
376        }
377
378        // pad $pass so it's 64 bytes
379        $padLen = 64 - strlen($pass);
380        for ($i=0; $i<$padLen; $i++) {
381            $pass .= chr(0);
382        }
383   
384        // generate hash
385        $hash  = md5($this->_xor($pass,$opad) . pack("H*", md5($this->_xor($pass, $ipad) . base64_decode($encChallenge))));
386   
387        // generate reply
388        $reply = base64_encode($user . ' ' . $hash);
389   
390        // send result, get reply
391        $this->putLine($reply);
392        $line = $this->readLine(1024);
393   
394        // process result
395        $result = $this->parseResult($line);
396        if ($result == 0) {
397            $this->errornum = 0;
398            return $this->fp;
399        }
400
401        $this->error    = "Authentication for $user failed (AUTH): $line";
402        $this->errornum = $result;
403
404        return $result;
405    }
406
407    function login($user, $password)
408    {
409        $this->putLine('a001 LOGIN "'.$this->escape($user).'" "'.$this->escape($password).'"');
410
411        $line = $this->readReply();
412
413        // process result
414        $result = $this->parseResult($line);
415
416        if ($result == 0) {
417            $this->errornum = 0;
418            return $this->fp;
419        }
420
421        @fclose($this->fp);
422        $this->fp = false;
423   
424        $this->error    = "Authentication for $user failed (LOGIN): $line";
425        $this->errornum = $result;
426
427        return $result;
428    }
429
430    function getNamespace()
431    {
432            if (isset($this->prefs['rootdir']) && is_string($this->prefs['rootdir'])) {
433                $this->rootdir = $this->prefs['rootdir'];
434                    return true;
435            }
436       
437        if (!$this->getCapability('NAMESPACE')) {
438                return false;
439            }
440   
441            if (!$this->putLine("ns1 NAMESPACE")) {
442            return false;
443        }
444            do {
445                    $line = $this->readLine(1024);
446                    if ($this->startsWith($line, '* NAMESPACE')) {
447                            $i    = 0;
448                            $line = $this->unEscape($line);
449                            $data = $this->parseNamespace(substr($line,11), $i, 0, 0);
450                    }
451            } while (!$this->startsWith($line, 'ns1', true, true));
452
453            if (!is_array($data)) {
454                return false;
455            }
456
457            $user_space_data = $data[0];
458            if (!is_array($user_space_data)) {
459                return false;
460            }
461
462            $first_userspace = $user_space_data[0];
463            if (count($first_userspace)!=2) {
464                return false;
465            }
466   
467            $this->rootdir            = $first_userspace[0];
468            $this->delimiter          = $first_userspace[1];
469            $this->prefs['rootdir']   = substr($this->rootdir, 0, -1);
470            $this->prefs['delimiter'] = $this->delimiter;
471       
472            return true;
473    }
474
475
476    /**
477     * Gets the delimiter, for example:
478     * INBOX.foo -> .
479     * INBOX/foo -> /
480     * INBOX\foo -> \
481     *
482     * @return mixed A delimiter (string), or false.
483     * @see connect()
484     */
485    function getHierarchyDelimiter()
486    {
487            if ($this->delimiter) {
488                return $this->delimiter;
489            }
490            if (!empty($this->prefs['delimiter'])) {
491            return ($this->delimiter = $this->prefs['delimiter']);
492            }
493
494            $delimiter = false;
495
496            // try (LIST "" ""), should return delimiter (RFC2060 Sec 6.3.8)
497            if (!$this->putLine('ghd LIST "" ""')) {
498                return false;
499            }
500   
501            do {
502                    $line = $this->readLine(500);
503                    if ($line[0] == '*') {
504                            $line = rtrim($line);
505                            $a = rcube_explode_quoted_string(' ', $this->unEscape($line));
506                            if ($a[0] == '*') {
507                                $delimiter = str_replace('"', '', $a[count($a)-2]);
508                        }
509                    }
510            } while (!$this->startsWith($line, 'ghd', true, true));
511
512            if (strlen($delimiter)>0) {
513                return $delimiter;
514            }
515
516            // if that fails, try namespace extension
517            // try to fetch namespace data
518            if (!$this->putLine("ns1 NAMESPACE")) {
519            return false;
520        }
521
522            do {
523                    $line = $this->readLine(1024);
524                    if ($this->startsWith($line, '* NAMESPACE')) {
525                            $i = 0;
526                            $line = $this->unEscape($line);
527                            $data = $this->parseNamespace(substr($line,11), $i, 0, 0);
528                    }
529            } while (!$this->startsWith($line, 'ns1', true, true));
530
531            if (!is_array($data)) {
532                return false;
533            }
534   
535            // extract user space data (opposed to global/shared space)
536            $user_space_data = $data[0];
537            if (!is_array($user_space_data)) {
538                return false;
539            }
540
541            // get first element
542            $first_userspace = $user_space_data[0];
543            if (!is_array($first_userspace)) {
544                return false;
545            }
546
547            // extract delimiter
548            $delimiter = $first_userspace[1];   
549
550            return $delimiter;
551    }
552
553    function connect($host, $user, $password, $options=null)
554    {
555            // set options
556            if (is_array($options)) {
557            $this->prefs = $options;
558        }
559        // set auth method
560        if (!empty($this->prefs['auth_method'])) {
561            $auth_method = strtoupper($this->prefs['auth_method']);
562            } else {
563                $auth_method = 'CHECK';
564        }
565
566            $message = "INITIAL: $auth_method\n";
567
568            $result = false;
569       
570            // initialize connection
571            $this->error    = '';
572            $this->errornum = 0;
573            $this->selected = '';
574            $this->user     = $user;
575            $this->host     = $host;
576        $this->logged   = false;
577
578            // check input
579            if (empty($host)) {
580                    $this->error    = "Empty host";
581                    $this->errornum = -2;
582                    return false;
583            }
584        if (empty($user)) {
585                    $this->error    = "Empty user";
586                $this->errornum = -1;
587                return false;
588            }
589            if (empty($password)) {
590                $this->error    = "Empty password";
591                $this->errornum = -1;
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            $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, 10);
604            if (!$this->fp) {
605                $this->error    = sprintf("Could not connect to %s:%d: %s", $host, $this->prefs['port'], $errstr);
606                $this->errornum = -2;
607                    return false;
608            }
609
610            stream_set_timeout($this->fp, 10);
611            $line = trim(fgets($this->fp, 8192));
612
613            if ($this->prefs['debug_mode'] && $line) {
614                    write_log('imap', 'S: '. $line);
615        }
616
617            // Connected to wrong port or connection error?
618            if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) {
619                    if ($line)
620                            $this->error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line);
621                    else
622                            $this->error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']);
623                $this->errornum = -2;
624                return false;
625            }
626
627            // RFC3501 [7.1] optional CAPABILITY response
628            if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
629                    $this->capability = explode(' ', strtoupper($matches[1]));
630            }
631
632            $this->message .= $line;
633
634            // TLS connection
635            if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
636                if (version_compare(PHP_VERSION, '5.1.0', '>=')) {
637                $this->putLine("tls0 STARTTLS");
638
639                            $line = $this->readLine(4096);
640                if (!$this->startsWith($line, "tls0 OK")) {
641                                    $this->error    = "Server responded to STARTTLS with: $line";
642                                    $this->errornum = -2;
643                    return false;
644                }
645
646                            if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
647                                    $this->error    = "Unable to negotiate TLS";
648                                    $this->errornum = -2;
649                                    return false;
650                            }
651                       
652                            // Now we're authenticated, capabilities need to be reread
653                            $this->clearCapability();
654                }
655            }
656
657            $orig_method = $auth_method;
658
659            if ($auth_method == 'CHECK') {
660                    // check for supported auth methods
661                    if ($this->getCapability('AUTH=CRAM-MD5') || $this->getCapability('AUTH=CRAM_MD5')) {
662                            $auth_method = 'AUTH';
663                    }
664                    else {
665                            // default to plain text auth
666                            $auth_method = 'PLAIN';
667                    }
668            }
669
670            if ($auth_method == 'AUTH') {
671                    // do CRAM-MD5 authentication
672                    $this->putLine("a000 AUTHENTICATE CRAM-MD5");
673                    $line = trim($this->readLine(1024));
674
675                    if ($line[0] == '+') {
676                            // got a challenge string, try CRAM-MD5
677                            $result = $this->authenticate($user, $password, substr($line,2));
678                       
679                            // stop if server sent BYE response
680                            if ($result == -3) {
681                                    return false;
682                            }
683                    }
684               
685                    if (!is_resource($result) && $orig_method == 'CHECK') {
686                            $auth_method = 'PLAIN';
687                    }
688            }
689               
690            if ($auth_method == 'PLAIN') {
691                    // do plain text auth
692                    $result = $this->login($user, $password);
693            }
694
695            if (is_resource($result)) {
696            if ($this->prefs['force_caps']) {
697                            $this->clearCapability();
698            }
699                    $this->getNamespace();
700            $this->logged = true;
701                    return true;
702            } else {
703                    return false;
704            }
705    }
706
707    function connected()
708    {
709                return ($this->fp && $this->logged) ? true : false;
710    }
711
712    function close()
713    {
714            if ($this->putLine("I LOGOUT")) {
715                    if (!feof($this->fp))
716                            fgets($this->fp, 1024);
717            }
718                @fclose($this->fp);
719                $this->fp = false;
720    }
721
722    function select($mailbox)
723    {
724            if (empty($mailbox)) {
725                    return false;
726            }
727            if ($this->selected == $mailbox) {
728                    return true;
729            }
730   
731            if ($this->putLine("sel1 SELECT \"".$this->escape($mailbox).'"')) {
732                    do {
733                            $line = chop($this->readLine(300));
734                            $a = explode(' ', $line);
735                            if (count($a) == 3) {
736                                    $token = strtoupper($a[2]);
737                                    if ($token == 'EXISTS') {
738                                            $this->exists = (int) $a[1];
739                                    }
740                                    else if ($token == 'RECENT') {
741                                            $this->recent = (int) $a[1];
742                                    }
743                            }
744                            else if (preg_match('/\[?PERMANENTFLAGS\s+\(([^\)]+)\)\]/U', $line, $match)) {
745                                    $this->permanentflags = explode(' ', $match[1]);
746                            }
747                    } while (!$this->startsWith($line, 'sel1', true, true));
748
749                    if (strcasecmp($a[1], 'OK') == 0) {
750                            $this->selected = $mailbox;
751                            return true;
752                    }
753            else {
754                $this->error = "Couldn't select $mailbox";
755            }
756            }
757
758        return false;
759    }
760
761    function checkForRecent($mailbox)
762    {
763            if (empty($mailbox)) {
764                    $mailbox = 'INBOX';
765            }
766   
767            $this->select($mailbox);
768            if ($this->selected == $mailbox) {
769                    return $this->recent;
770            }
771
772            return false;
773    }
774
775    function countMessages($mailbox, $refresh = false)
776    {
777            if ($refresh) {
778                    $this->selected = '';
779            }
780       
781            $this->select($mailbox);
782            if ($this->selected == $mailbox) {
783                    return $this->exists;
784            }
785
786        return false;
787    }
788
789    function sort($mailbox, $field, $add='', $is_uid=FALSE, $encoding = 'US-ASCII')
790    {
791            $field = strtoupper($field);
792            if ($field == 'INTERNALDATE') {
793                $field = 'ARRIVAL';
794            }
795       
796            $fields = array('ARRIVAL' => 1,'CC' => 1,'DATE' => 1,
797            'FROM' => 1, 'SIZE' => 1, 'SUBJECT' => 1, 'TO' => 1);
798
799            if (!$fields[$field]) {
800                return false;
801            }
802
803            /*  Do "SELECT" command */
804            if (!$this->select($mailbox)) {
805                return false;
806            }
807   
808            $is_uid = $is_uid ? 'UID ' : '';
809       
810            // message IDs
811            if (is_array($add))
812                    $add = $this->compressMessageSet(join(',', $add));
813
814            $command  = "s ".$is_uid."SORT ($field) $encoding ALL";
815            $line     = $data = '';
816
817            if (!empty($add))
818                $command .= ' '.$add;
819
820            if (!$this->putLineC($command)) {
821                return false;
822            }
823            do {
824                    $line = chop($this->readLine());
825                    if ($this->startsWith($line, '* SORT')) {
826                            $data .= substr($line, 7);
827                } else if (preg_match('/^[0-9 ]+$/', $line)) {
828                            $data .= $line;
829                    }
830            } while (!$this->startsWith($line, 's ', true, true));
831       
832            $result_code = $this->parseResult($line);
833       
834            if ($result_code != 0) {
835            $this->error = "Sort: $line";
836            return false;
837            }
838       
839            return preg_split('/\s+/', $data, -1, PREG_SPLIT_NO_EMPTY);
840    }
841
842    function fetchHeaderIndex($mailbox, $message_set, $index_field='', $skip_deleted=true, $uidfetch=false)
843    {
844            if (is_array($message_set)) {
845                    if (!($message_set = $this->compressMessageSet(join(',', $message_set))))
846                            return false;
847            } else {
848                    list($from_idx, $to_idx) = explode(':', $message_set);
849                    if (empty($message_set) ||
850                            (isset($to_idx) && $to_idx != '*' && (int)$from_idx > (int)$to_idx)) {
851                            return false;
852                    }
853            }
854       
855            $index_field = empty($index_field) ? 'DATE' : strtoupper($index_field);
856       
857        $fields_a['DATE']         = 1;
858            $fields_a['INTERNALDATE'] = 4;
859        $fields_a['ARRIVAL']      = 4;
860            $fields_a['FROM']         = 1;
861        $fields_a['REPLY-TO']     = 1;
862            $fields_a['SENDER']       = 1;
863        $fields_a['TO']           = 1;
864            $fields_a['CC']           = 1;
865        $fields_a['SUBJECT']      = 1;
866            $fields_a['UID']          = 2;
867        $fields_a['SIZE']         = 2;
868            $fields_a['SEEN']         = 3;
869        $fields_a['RECENT']       = 3;
870            $fields_a['DELETED']      = 3;
871
872        if (!($mode = $fields_a[$index_field])) {
873                return false;
874            }
875
876        /*  Do "SELECT" command */
877            if (!$this->select($mailbox)) {
878                    return false;
879            }
880       
881        // build FETCH command string
882            $key     = 'fhi0';
883            $cmd     = $uidfetch ? 'UID FETCH' : 'FETCH';
884            $deleted = $skip_deleted ? ' FLAGS' : '';
885
886            if ($mode == 1 && $index_field == 'DATE')
887                    $request = " $cmd $message_set (INTERNALDATE BODY.PEEK[HEADER.FIELDS (DATE)]$deleted)";
888            else if ($mode == 1)
889                    $request = " $cmd $message_set (BODY.PEEK[HEADER.FIELDS ($index_field)]$deleted)";
890            else if ($mode == 2) {
891                    if ($index_field == 'SIZE')
892                            $request = " $cmd $message_set (RFC822.SIZE$deleted)";
893                    else
894                            $request = " $cmd $message_set ($index_field$deleted)";
895            } else if ($mode == 3)
896                    $request = " $cmd $message_set (FLAGS)";
897            else // 4
898                    $request = " $cmd $message_set (INTERNALDATE$deleted)";
899
900            $request = $key . $request;
901
902            if (!$this->putLine($request)) {
903                    return false;
904        }
905
906            $result = array();
907
908            do {
909                    $line = chop($this->readLine(200));
910                    $line = $this->multLine($line);
911
912                    if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
913                $id     = $m[1];
914                            $flags  = NULL;
915                                       
916                            if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
917                                    $flags = explode(' ', strtoupper($matches[1]));
918                                    if (in_array('\\DELETED', $flags)) {
919                                            $deleted[$id] = $id;
920                                            continue;
921                                    }
922                            }
923
924                            if ($mode == 1 && $index_field == 'DATE') {
925                                    if (preg_match('/BODY\[HEADER\.FIELDS \("*DATE"*\)\] (.*)/', $line, $matches)) {
926                                            $value = preg_replace(array('/^"*[a-z]+:/i'), '', $matches[1]);
927                                            $value = trim($value);
928                                            $result[$id] = $this->strToTime($value);
929                                    }
930                                    // non-existent/empty Date: header, use INTERNALDATE
931                                    if (empty($result[$id])) {
932                                            if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches))
933                                                    $result[$id] = $this->strToTime($matches[1]);
934                                            else
935                                                    $result[$id] = 0;
936                                    }
937                            } else if ($mode == 1) {
938                                    if (preg_match('/BODY\[HEADER\.FIELDS \("?(FROM|REPLY-TO|SENDER|TO|SUBJECT)"?\)\] (.*)/', $line, $matches)) {
939                                            $value = preg_replace(array('/^"*[a-z]+:/i', '/\s+$/sm'), array('', ''), $matches[2]);
940                                            $result[$id] = trim($value);
941                                    } else {
942                                            $result[$id] = '';
943                                    }
944                            } else if ($mode == 2) {
945                                    if (preg_match('/\((UID|RFC822\.SIZE) ([0-9]+)/', $line, $matches)) {
946                                            $result[$id] = trim($matches[2]);
947                                    } else {
948                                            $result[$id] = 0;
949                                    }
950                            } else if ($mode == 3) {
951                                    if (!$flags && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
952                                            $flags = explode(' ', $matches[1]);
953                                    }
954                                    $result[$id] = in_array('\\'.$index_field, $flags) ? 1 : 0;
955                            } else if ($mode == 4) {
956                                    if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) {
957                                            $result[$id] = $this->strToTime($matches[1]);
958                                    } else {
959                                            $result[$id] = 0;
960                                    }
961                            }
962                    }
963            } while (!$this->startsWith($line, $key, true, true));
964
965            return $result;     
966    }
967
968    private function compressMessageSet($message_set)
969    {
970            // given a comma delimited list of independent mid's,
971            // compresses by grouping sequences together
972       
973            // if less than 255 bytes long, let's not bother
974            if (strlen($message_set)<255) {
975                return $message_set;
976            }
977   
978            // see if it's already been compress
979            if (strpos($message_set, ':') !== false) {
980                return $message_set;
981            }
982   
983            // separate, then sort
984            $ids = explode(',', $message_set);
985            sort($ids);
986       
987            $result = array();
988            $start  = $prev = $ids[0];
989
990            foreach ($ids as $id) {
991                    $incr = $id - $prev;
992                    if ($incr > 1) {                    //found a gap
993                            if ($start == $prev) {
994                                $result[] = $prev;      //push single id
995                            } else {
996                                $result[] = $start . ':' . $prev;   //push sequence as start_id:end_id
997                            }
998                        $start = $id;                   //start of new sequence
999                    }
1000                    $prev = $id;
1001            }
1002
1003            // handle the last sequence/id
1004            if ($start==$prev) {
1005                $result[] = $prev;
1006            } else {
1007            $result[] = $start.':'.$prev;
1008            }
1009   
1010            // return as comma separated string
1011            return implode(',', $result);
1012    }
1013
1014    function UID2ID($folder, $uid)
1015    {
1016            if ($uid > 0) {
1017                $id_a = $this->search($folder, "UID $uid");
1018                if (is_array($id_a) && count($id_a) == 1) {
1019                        return $id_a[0];
1020                    }
1021            }
1022            return false;
1023    }
1024
1025    function ID2UID($folder, $id)
1026    {
1027            if (empty($id)) {
1028                return  -1;
1029            }
1030
1031        if (!$this->select($folder)) {
1032            return -1;
1033        }
1034
1035            $result = -1;
1036                if ($this->putLine("fuid FETCH $id (UID)")) {
1037                    do {
1038                            $line = chop($this->readLine(1024));
1039                                if (preg_match("/^\* $id FETCH \(UID (.*)\)/i", $line, $r)) {
1040                                        $result = $r[1];
1041                                }
1042                        } while (!$this->startsWith($line, 'fuid', true, true));
1043                }
1044
1045        return $result;
1046    }
1047
1048    function fetchUIDs($mailbox, $message_set=null)
1049    {
1050            if (is_array($message_set))
1051                    $message_set = join(',', $message_set);
1052        else if (empty($message_set))
1053                    $message_set = '1:*';
1054       
1055            return $this->fetchHeaderIndex($mailbox, $message_set, 'UID', false);
1056    }
1057
1058    function fetchHeaders($mailbox, $message_set, $uidfetch=false, $bodystr=false, $add='')
1059    {
1060            $result = array();
1061       
1062            if (!$this->select($mailbox)) {
1063                    return false;
1064            }
1065
1066            if (is_array($message_set))
1067                    $message_set = join(',', $message_set);
1068
1069            $message_set = $this->compressMessageSet($message_set);
1070
1071            if ($add)
1072                    $add = ' '.strtoupper(trim($add));
1073
1074            /* FETCH uid, size, flags and headers */
1075            $key          = 'FH12';
1076            $request  = $key . ($uidfetch ? ' UID' : '') . " FETCH $message_set ";
1077            $request .= "(UID RFC822.SIZE FLAGS INTERNALDATE ";
1078            if ($bodystr)
1079                    $request .= "BODYSTRUCTURE ";
1080            $request .= "BODY.PEEK[HEADER.FIELDS ";
1081            $request .= "(DATE FROM TO SUBJECT REPLY-TO IN-REPLY-TO CC BCC ";
1082            $request .= "CONTENT-TRANSFER-ENCODING CONTENT-TYPE MESSAGE-ID ";
1083            $request .= "REFERENCES DISPOSITION-NOTIFICATION-TO X-PRIORITY ";
1084            $request .= "X-DRAFT-INFO".$add.")])";
1085
1086            if (!$this->putLine($request)) {
1087                    return false;
1088            }
1089            do {
1090                    $line = $this->readLine(1024);
1091                    $line = $this->multLine($line);
1092           
1093            if (!$line)
1094                break;
1095           
1096                    $a    = explode(' ', $line);
1097
1098                    if (($line[0] == '*') && ($a[2] == 'FETCH')) {
1099                            $id = $a[1];
1100           
1101                            $result[$id]            = new rcube_mail_header;
1102                            $result[$id]->id        = $id;
1103                            $result[$id]->subject   = '';
1104                            $result[$id]->messageID = 'mid:' . $id;
1105
1106                            $lines = array();
1107                            $ln = 0;
1108
1109                            // Sample reply line:
1110                            // * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen)
1111                            // INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...)
1112                            // BODY[HEADER.FIELDS ...
1113
1114                            if (preg_match('/^\* [0-9]+ FETCH \((.*) BODY/s', $line, $matches)) {
1115                                    $str = $matches[1];
1116
1117                                    // swap parents with quotes, then explode
1118                                    $str = preg_replace('/[()]/', '"', $str);
1119                                    $a = rcube_explode_quoted_string(' ', $str);
1120
1121                                    // did we get the right number of replies?
1122                                    $parts_count = count($a);
1123                                    if ($parts_count>=6) {
1124                                            for ($i=0; $i<$parts_count; $i=$i+2) {
1125                                                    if ($a[$i] == 'UID')
1126                                                            $result[$id]->uid = $a[$i+1];
1127                                                    else if ($a[$i] == 'RFC822.SIZE')
1128                                                            $result[$id]->size = $a[$i+1];
1129                                                else if ($a[$i] == 'INTERNALDATE')
1130                                                        $time_str = $a[$i+1];
1131                                                else if ($a[$i] == 'FLAGS')
1132                                                        $flags_str = $a[$i+1];
1133                                        }
1134
1135                                            $time_str = str_replace('"', '', $time_str);
1136                                       
1137                                            // if time is gmt...
1138                                $time_str = str_replace('GMT','+0000',$time_str);
1139                                       
1140                                            $result[$id]->internaldate = $time_str;
1141                                        $result[$id]->timestamp = $this->StrToTime($time_str);
1142                                        $result[$id]->date = $time_str;
1143                                }
1144
1145                                // BODYSTRUCTURE
1146                                    if($bodystr) {
1147                                            while (!preg_match('/ BODYSTRUCTURE (.*) BODY\[HEADER.FIELDS/s', $line, $m)) {
1148                                                    $line2 = $this->readLine(1024);
1149                                                $line .= $this->multLine($line2, true);
1150                                        }
1151                                        $result[$id]->body_structure = $m[1];
1152                                }
1153
1154                                // the rest of the result
1155                                preg_match('/ BODY\[HEADER.FIELDS \(.*?\)\]\s*(.*)$/s', $line, $m);
1156                                $reslines = explode("\n", trim($m[1], '"'));
1157                                // re-parse (see below)
1158                                    foreach ($reslines as $resln) {
1159                                        if (ord($resln[0])<=32) {
1160                                                $lines[$ln] .= (empty($lines[$ln])?'':"\n").trim($resln);
1161                                        } else {
1162                                                $lines[++$ln] = trim($resln);
1163                                            }
1164                                }
1165                        }
1166
1167                                // Start parsing headers.  The problem is, some header "lines" take up multiple lines.
1168                                // So, we'll read ahead, and if the one we're reading now is a valid header, we'll
1169                                // process the previous line.  Otherwise, we'll keep adding the strings until we come
1170                                // to the next valid header line.
1171       
1172                            do {
1173                                    $line = chop($this->readLine(300), "\r\n");
1174
1175                                // The preg_match below works around communigate imap, which outputs " UID <number>)".
1176                                // Without this, the while statement continues on and gets the "FH0 OK completed" message.
1177                                // If this loop gets the ending message, then the outer loop does not receive it from radline on line 1249. 
1178                                // This in causes the if statement on line 1278 to never be true, which causes the headers to end up missing
1179                                    // If the if statement was changed to pick up the fh0 from this loop, then it causes the outer loop to spin
1180                                // An alternative might be:
1181                                // if (!preg_match("/:/",$line) && preg_match("/\)$/",$line)) break;
1182                                // however, unsure how well this would work with all imap clients.
1183                                if (preg_match("/^\s*UID [0-9]+\)$/", $line)) {
1184                                        break;
1185                                    }
1186
1187                                // handle FLAGS reply after headers (AOL, Zimbra?)
1188                                if (preg_match('/\s+FLAGS \((.*)\)\)$/', $line, $matches)) {
1189                                        $flags_str = $matches[1];
1190                                        break;
1191                                    }
1192
1193                                if (ord($line[0])<=32) {
1194                                        $lines[$ln] .= (empty($lines[$ln])?'':"\n").trim($line);
1195                                } else {
1196                                        $lines[++$ln] = trim($line);
1197                                    }
1198                        // patch from "Maksim Rubis" <siburny@hotmail.com>
1199                        } while ($line[0] != ')' && !$this->startsWith($line, $key, true));
1200
1201                        if (strncmp($line, $key, strlen($key))) { 
1202                                // process header, fill rcube_mail_header obj.
1203                                // initialize
1204                                if (is_array($headers)) {
1205                                        reset($headers);
1206                                            while (list($k, $bar) = each($headers)) {
1207                                                $headers[$k] = '';
1208                                        }
1209                                }
1210
1211                                // create array with header field:data
1212                                while ( list($lines_key, $str) = each($lines) ) {
1213                                        list($field, $string) = $this->splitHeaderLine($str);
1214                                       
1215                                        $field  = strtolower($field);
1216                                        $string = preg_replace('/\n\s*/', ' ', $string);
1217                                           
1218                                        switch ($field) {
1219                                        case 'date';
1220                                                $result[$id]->date = $string;
1221                                                $result[$id]->timestamp = $this->strToTime($string);
1222                                        break;
1223                                        case 'from':
1224                                                $result[$id]->from = $string;
1225                                                break;
1226                                        case 'to':
1227                                                $result[$id]->to = preg_replace('/undisclosed-recipients:[;,]*/', '', $string);
1228                                                break;
1229                                        case 'subject':
1230                                                $result[$id]->subject = $string;
1231                                                break;
1232                                        case 'reply-to':
1233                                                $result[$id]->replyto = $string;
1234                                                break;
1235                                        case 'cc':
1236                                                $result[$id]->cc = $string;
1237                                                break;
1238                                        case 'bcc':
1239                                                $result[$id]->bcc = $string;
1240                                                break;
1241                                        case 'content-transfer-encoding':
1242                                                $result[$id]->encoding = $string;
1243                                                break;
1244                                        case 'content-type':
1245                                                $ctype_parts = preg_split('/[; ]/', $string);
1246                                                $result[$id]->ctype = array_shift($ctype_parts);
1247                                                if (preg_match('/charset\s*=\s*"?([a-z0-9\-\.\_]+)"?/i', $string, $regs)) {
1248                                                        $result[$id]->charset = $regs[1];
1249                                                }
1250                                                break;
1251                                            case 'in-reply-to':
1252                                                $result[$id]->in_reply_to = preg_replace('/[\n<>]/', '', $string);
1253                                                break;
1254                                        case 'references':
1255                                                $result[$id]->references = $string;
1256                                                break;
1257                                        case 'return-receipt-to':
1258                                        case 'disposition-notification-to':
1259                                            case 'x-confirm-reading-to':
1260                                                $result[$id]->mdn_to = $string;
1261                                                break;
1262                                        case 'message-id':
1263                                                $result[$id]->messageID = $string;
1264                                                break;
1265                                            case 'x-priority':
1266                                                if (preg_match('/^(\d+)/', $string, $matches))
1267                                                        $result[$id]->priority = intval($matches[1]);
1268                                                break;
1269                                        default:
1270                                                if (strlen($field) > 2)
1271                                                        $result[$id]->others[$field] = $string;
1272                                                    break;
1273                                        } // end switch ()
1274                                } // end while ()
1275                        } else {
1276                                $a = explode(' ', $line);
1277                            }
1278
1279                        // process flags
1280                        if (!empty($flags_str)) {
1281                                $flags_str = preg_replace('/[\\\"]/', '', $flags_str);
1282                                $flags_a   = explode(' ', $flags_str);
1283                                       
1284                                    if (is_array($flags_a)) {
1285                                //      reset($flags_a);
1286                                        foreach($flags_a as $flag) {
1287                                                $flag = strtoupper($flag);
1288                                                if ($flag == 'SEEN') {
1289                                                    $result[$id]->seen = true;
1290                                                } else if ($flag == 'DELETED') {
1291                                                    $result[$id]->deleted = true;
1292                                                } else if ($flag == 'RECENT') {
1293                                                    $result[$id]->recent = true;
1294                                                } else if ($flag == 'ANSWERED') {
1295                                                        $result[$id]->answered = true;
1296                                                } else if ($flag == '$FORWARDED') {
1297                                                        $result[$id]->forwarded = true;
1298                                                } else if ($flag == 'DRAFT') {
1299                                                        $result[$id]->is_draft = true;
1300                                                } else if ($flag == '$MDNSENT') {
1301                                                        $result[$id]->mdn_sent = true;
1302                                                } else if ($flag == 'FLAGGED') {
1303                                                         $result[$id]->flagged = true;
1304                                                    }
1305                                        }
1306                                        $result[$id]->flags = $flags_a;
1307                                }
1308                            }
1309                }
1310            } while (!$this->startsWith($line, $key, true));
1311
1312        return $result;
1313    }
1314
1315    function fetchHeader($mailbox, $id, $uidfetch=false, $bodystr=false, $add='')
1316    {
1317            $a  = $this->fetchHeaders($mailbox, $id, $uidfetch, $bodystr, $add);
1318            if (is_array($a)) {
1319                    return array_shift($a);
1320            }
1321            return false;
1322    }
1323
1324    function sortHeaders($a, $field, $flag)
1325    {
1326            if (empty($field)) {
1327                $field = 'uid';
1328            }
1329        else {
1330            $field = strtolower($field);
1331        }
1332
1333            if ($field == 'date' || $field == 'internaldate') {
1334                $field = 'timestamp';
1335            }
1336        if (empty($flag)) {
1337                $flag = 'ASC';
1338            } else {
1339                $flag = strtoupper($flag);
1340        }
1341
1342            $stripArr = ($field=='subject') ? array('Re: ','Fwd: ','Fw: ','"') : array('"');
1343
1344            $c = count($a);
1345            if ($c > 0) {
1346                   
1347                        // Strategy:
1348                        // First, we'll create an "index" array.
1349                        // Then, we'll use sort() on that array,
1350                        // and use that to sort the main array.
1351               
1352                    // create "index" array
1353                    $index = array();
1354                    reset($a);
1355                    while (list($key, $val) = each($a)) {
1356                            if ($field == 'timestamp') {
1357                                    $data = $this->strToTime($val->date);
1358                                    if (!$data) {
1359                                            $data = $val->timestamp;
1360                        }
1361                            } else {
1362                                    $data = $val->$field;
1363                                    if (is_string($data)) {
1364                                            $data = strtoupper(str_replace($stripArr, '', $data));
1365                        }
1366                            }
1367                        $index[$key]=$data;
1368                }
1369               
1370                    // sort index
1371                $i = 0;
1372                if ($flag == 'ASC') {
1373                        asort($index);
1374                } else {
1375                    arsort($index);
1376                    }
1377
1378                // form new array based on index
1379                $result = array();
1380                    reset($index);
1381                while (list($key, $val) = each($index)) {
1382                        $result[$key]=$a[$key];
1383                        $i++;
1384                    }
1385            }
1386       
1387            return $result;
1388    }
1389
1390    function expunge($mailbox, $messages=NULL)
1391    {
1392            if (!$this->select($mailbox)) {
1393            return -1;
1394        }
1395               
1396        $c = 0;
1397                $command = $messages ? "UID EXPUNGE $messages" : "EXPUNGE";
1398
1399                if (!$this->putLine("exp1 $command")) {
1400            return -1;
1401        }
1402
1403                do {
1404                        $line = $this->readLine(100);
1405                        if ($line[0] == '*') {
1406                $c++;
1407                }
1408                } while (!$this->startsWith($line, 'exp1', true, true));
1409               
1410                if ($this->parseResult($line) == 0) {
1411                        $this->selected = ''; // state has changed, need to reselect
1412                        return $c;
1413                }
1414                $this->error = $line;
1415            return -1;
1416    }
1417
1418    function modFlag($mailbox, $messages, $flag, $mod)
1419    {
1420            if ($mod != '+' && $mod != '-') {
1421                return -1;
1422            }
1423   
1424            $flag = $this->flags[strtoupper($flag)];
1425   
1426            if (!$this->select($mailbox)) {
1427                return -1;
1428            }
1429   
1430        $c = 0;
1431            if (!$this->putLine("flg UID STORE $messages {$mod}FLAGS ($flag)")) {
1432            return false;
1433        }
1434
1435            do {
1436                    $line = $this->readLine(1000);
1437                    if ($line[0] == '*') {
1438                        $c++;
1439            }
1440            } while (!$this->startsWith($line, 'flg', true, true));
1441
1442            if ($this->parseResult($line) == 0) {
1443                    return $c;
1444            }
1445
1446            $this->error = $line;
1447            return -1;
1448    }
1449
1450    function flag($mailbox, $messages, $flag) {
1451            return $this->modFlag($mailbox, $messages, $flag, '+');
1452    }
1453
1454    function unflag($mailbox, $messages, $flag) {
1455            return $this->modFlag($mailbox, $messages, $flag, '-');
1456    }
1457
1458    function delete($mailbox, $messages) {
1459            return $this->modFlag($mailbox, $messages, 'DELETED', '+');
1460    }
1461
1462    function copy($messages, $from, $to)
1463    {
1464            if (empty($from) || empty($to)) {
1465                return -1;
1466            }
1467   
1468            if (!$this->select($from)) {
1469            return -1;
1470            }
1471       
1472        $this->putLine("cpy1 UID COPY $messages \"".$this->escape($to)."\"");
1473            $line = $this->readReply();
1474            return $this->parseResult($line);
1475    }
1476
1477    function countUnseen($folder)
1478    {
1479        $index = $this->search($folder, 'ALL UNSEEN');
1480        if (is_array($index))
1481            return count($index);
1482        return false;
1483    }
1484
1485    // Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about
1486    // 7 times instead :-) See comments on http://uk2.php.net/references and this article:
1487    // http://derickrethans.nl/files/phparch-php-variables-article.pdf
1488    private function parseThread($str, $begin, $end, $root, $parent, $depth, &$depthmap, &$haschildren)
1489    {
1490            $node = array();
1491            if ($str[$begin] != '(') {
1492                    $stop = $begin + strspn($str, "1234567890", $begin, $end - $begin);
1493                    $msg = substr($str, $begin, $stop - $begin);
1494                    if ($msg == 0)
1495                        return $node;
1496                    if (is_null($root))
1497                            $root = $msg;
1498                    $depthmap[$msg] = $depth;
1499                    $haschildren[$msg] = false;
1500                    if (!is_null($parent))
1501                            $haschildren[$parent] = true;
1502                    if ($stop + 1 < $end)
1503                            $node[$msg] = $this->parseThread($str, $stop + 1, $end, $root, $msg, $depth + 1, $depthmap, $haschildren);
1504                    else
1505                            $node[$msg] = array();
1506            } else {
1507                    $off = $begin;
1508                    while ($off < $end) {
1509                            $start = $off;
1510                        $off++;
1511                        $n = 1;
1512                        while ($n > 0) {
1513                                $p = strpos($str, ')', $off);
1514                                    if ($p === false) {
1515                                            error_log('Mismatched brackets parsing IMAP THREAD response:');
1516                                        error_log(substr($str, ($begin < 10) ? 0 : ($begin - 10), $end - $begin + 20));
1517                                        error_log(str_repeat(' ', $off - (($begin < 10) ? 0 : ($begin - 10))));
1518                                        return $node;
1519                                }
1520                                    $p1 = strpos($str, '(', $off);
1521                                if ($p1 !== false && $p1 < $p) {
1522                                        $off = $p1 + 1;
1523                                        $n++;
1524                                } else {
1525                                        $off = $p + 1;
1526                                            $n--;
1527                                }
1528                        }
1529                        $node += $this->parseThread($str, $start + 1, $off - 1, $root, $parent, $depth, $depthmap, $haschildren);
1530                    }
1531            }
1532       
1533            return $node;
1534    }
1535
1536    function thread($folder, $algorithm='REFERENCES', $criteria='', $encoding='US-ASCII')
1537    {
1538            if (!$this->select($folder)) {
1539                    return false;
1540            }
1541
1542        $encoding  = $encoding ? trim($encoding) : 'US-ASCII';
1543            $algorithm = $algorithm ? trim($algorithm) : 'REFERENCES';
1544            $criteria  = $criteria ? 'ALL '.trim($criteria) : 'ALL';
1545               
1546            if (!$this->putLineC("thrd1 THREAD $algorithm $encoding $criteria")) {
1547                    return false;
1548            }
1549            do {
1550                    $line = trim($this->readLine(10000));
1551                    if (preg_match('/^\* THREAD/', $line)) {
1552                            $str         = trim(substr($line, 8));
1553                        $depthmap    = array();
1554                        $haschildren = array();
1555                            $tree = $this->parseThread($str, 0, strlen($str), null, null, 0, $depthmap, $haschildren);
1556                    }
1557            } while (!$this->startsWith($line, 'thrd1', true, true));
1558
1559        $result_code = $this->parseResult($line);
1560            if ($result_code == 0) {
1561            return array($tree, $depthmap, $haschildren);
1562            }
1563
1564        $this->error = "Thread: $line";
1565            return false;       
1566    }
1567
1568    function search($folder, $criteria, $return_uid=false)
1569    {
1570            if (!$this->select($folder)) {
1571                return false;
1572            }
1573
1574        $data = '';
1575            $query = 'srch1 ' . ($return_uid ? 'UID ' : '') . 'SEARCH ' . chop($criteria);
1576
1577            if (!$this->putLineC($query)) {
1578                    return false;
1579            }
1580
1581        do {
1582                $line = trim($this->readLine());
1583                    if ($this->startsWith($line, '* SEARCH')) {
1584                        $data .= substr($line, 8);
1585                } else if (preg_match('/^[0-9 ]+$/', $line)) {
1586                        $data .= $line;
1587                    }
1588        } while (!$this->startsWith($line, 'srch1', true, true));
1589
1590            $result_code = $this->parseResult($line);
1591            if ($result_code == 0) {
1592                    return preg_split('/\s+/', $data, -1, PREG_SPLIT_NO_EMPTY);
1593            }
1594
1595        $this->error = "Search: $line";
1596            return false;       
1597    }
1598
1599    function move($messages, $from, $to)
1600    {
1601        if (!$from || !$to) {
1602            return -1;
1603        }
1604   
1605        $r = $this->copy($messages, $from, $to);
1606
1607        if ($r==0) {
1608            return $this->delete($from, $messages);
1609        }
1610        return $r;
1611    }
1612
1613    function listMailboxes($ref, $mailbox)
1614    {
1615        return $this->_listMailboxes($ref, $mailbox, false);
1616    }
1617
1618    function listSubscribed($ref, $mailbox)
1619    {
1620        return $this->_listMailboxes($ref, $mailbox, true);
1621    }
1622
1623    private function _listMailboxes($ref, $mailbox, $subscribed=false)
1624    {
1625                if (empty($mailbox)) {
1626                $mailbox = '*';
1627            }
1628       
1629            if (empty($ref) && $this->rootdir) {
1630                $ref = $this->rootdir;
1631            }
1632   
1633        if ($subscribed) {
1634            $key     = 'lsb';
1635            $command = 'LSUB';
1636        }
1637        else {
1638            $key     = 'lmb';
1639            $command = 'LIST';
1640        }
1641
1642        // send command
1643            if (!$this->putLine($key." ".$command." \"". $this->escape($ref) ."\" \"". $this->escape($mailbox) ."\"")) {
1644                    $this->error = "Couldn't send $command command";
1645                return false;
1646            }
1647   
1648            // get folder list
1649            do {
1650                    $line = $this->readLine(500);
1651                    $line = $this->multLine($line, true);
1652                    $a    = explode(' ', $line);
1653
1654                    if (($line[0] == '*') && ($a[1] == $command)) {
1655                            $line = rtrim($line);
1656                        // split one line
1657                            $a = rcube_explode_quoted_string(' ', $line);
1658                        // last string is folder name
1659                            $folders[] = preg_replace(array('/^"/', '/"$/'), '', $this->unEscape($a[count($a)-1]));
1660                        // second from last is delimiter
1661                        $delim = trim($a[count($a)-2], '"');
1662                    }
1663            } while (!$this->startsWith($line, $key, true));
1664
1665            if (is_array($folders)) {
1666            return $folders;
1667            } else if ($this->parseResult($line) == 0) {
1668            return array();
1669        }
1670
1671            $this->error = $line;
1672        return false;
1673    }
1674
1675    function fetchMIMEHeaders($mailbox, $id, $parts, $mime=true)
1676    {
1677            if (!$this->select($mailbox)) {
1678                    return false;
1679            }
1680       
1681        $result = false;
1682            $parts  = (array) $parts;
1683        $key    = 'fmh0';
1684            $peeks  = '';
1685        $idx    = 0;
1686        $type   = $mime ? 'MIME' : 'HEADER';
1687
1688            // format request
1689            foreach($parts as $part)
1690                    $peeks[] = "BODY.PEEK[$part.$type]";
1691       
1692            $request = "$key FETCH $id (" . implode(' ', $peeks) . ')';
1693
1694            // send request
1695            if (!$this->putLine($request)) {
1696                return false;
1697            }
1698       
1699            do {
1700                $line = $this->readLine(1000);
1701                $line = $this->multLine($line);
1702
1703                    if (preg_match('/BODY\[([0-9\.]+)\.'.$type.'\]/', $line, $matches)) {
1704                            $idx = $matches[1];
1705                        $result[$idx] = preg_replace('/^(\* '.$id.' FETCH \()?\s*BODY\['.$idx.'\.'.$type.'\]\s+/', '', $line);
1706                        $result[$idx] = trim($result[$idx], '"');
1707                        $result[$idx] = rtrim($result[$idx], "\t\r\n\0\x0B");
1708                }
1709            } while (!$this->startsWith($line, $key, true));
1710
1711            return $result;
1712    }
1713
1714    function fetchPartHeader($mailbox, $id, $is_uid=false, $part=NULL)
1715    {
1716            $part = empty($part) ? 'HEADER' : $part.'.MIME';
1717
1718        return $this->handlePartBody($mailbox, $id, $is_uid, $part);
1719    }
1720
1721    function handlePartBody($mailbox, $id, $is_uid=false, $part='', $encoding=NULL, $print=NULL, $file=NULL)
1722    {
1723        if (!$this->select($mailbox)) {
1724            return false;
1725        }
1726
1727            switch ($encoding) {
1728                    case 'base64':
1729                            $mode = 1;
1730                break;
1731                case 'quoted-printable':
1732                        $mode = 2;
1733                break;
1734                case 'x-uuencode':
1735                    case 'x-uue':
1736                case 'uue':
1737                case 'uuencode':
1738                        $mode = 3;
1739                break;
1740                default:
1741                        $mode = 0;
1742            }
1743       
1744                $reply_key = '* ' . $id;
1745        $result = false;
1746
1747        // format request
1748                $key     = 'ftch0';
1749                $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id (BODY.PEEK[$part])";
1750        // send request
1751                if (!$this->putLine($request)) {
1752                    return false;
1753                }
1754
1755                // receive reply line
1756                do {
1757                $line = chop($this->readLine(1000));
1758                $a    = explode(' ', $line);
1759                } while (!($end = $this->startsWith($line, $key, true)) && $a[2] != 'FETCH');
1760
1761                $len = strlen($line);
1762
1763                // handle empty "* X FETCH ()" response
1764        if ($line[$len-1] == ')' && $line[$len-2] != '(') {
1765                // one line response, get everything between first and last quotes
1766                        if (substr($line, -4, 3) == 'NIL') {
1767                                // NIL response
1768                                $result = '';
1769                        } else {
1770                            $from = strpos($line, '"') + 1;
1771                        $to   = strrpos($line, '"');
1772                        $len  = $to - $from;
1773                                $result = substr($line, $from, $len);
1774                        }
1775
1776                if ($mode == 1)
1777                                $result = base64_decode($result);
1778                        else if ($mode == 2)
1779                                $result = quoted_printable_decode($result);
1780                        else if ($mode == 3)
1781                                $result = convert_uudecode($result);
1782
1783        } else if ($line[$len-1] == '}') {
1784                // multi-line request, find sizes of content and receive that many bytes
1785                $from     = strpos($line, '{') + 1;
1786                $to       = strrpos($line, '}');
1787                $len      = $to - $from;
1788                $sizeStr  = substr($line, $from, $len);
1789                $bytes    = (int)$sizeStr;
1790                        $prev     = '';
1791                       
1792                while ($bytes > 0) {
1793                    $line = $this->readLine(1024);
1794                $len  = strlen($line);
1795               
1796                        if ($len > $bytes) {
1797                        $line = substr($line, 0, $bytes);
1798                                        $len = strlen($line);
1799                        }
1800                $bytes -= $len;
1801
1802                        if ($mode == 1) {
1803                                        $line = rtrim($line, "\t\r\n\0\x0B");
1804                                        // create chunks with proper length for base64 decoding
1805                                        $line = $prev.$line;
1806                                        $length = strlen($line);
1807                                        if ($length % 4) {
1808                                                $length = floor($length / 4) * 4;
1809                                                $prev = substr($line, $length);
1810                                                $line = substr($line, 0, $length);
1811                                        }
1812                                        else
1813                                                $prev = '';
1814                                               
1815                                        if ($file)
1816                                                fwrite($file, base64_decode($line));
1817                        else if ($print)
1818                                                echo base64_decode($line);
1819                                        else
1820                                                $result .= base64_decode($line);
1821                                } else if ($mode == 2) {
1822                                        $line = rtrim($line, "\t\r\0\x0B");
1823                                        if ($file)
1824                                                fwrite($file, quoted_printable_decode($line));
1825                        else if ($print)
1826                                                echo quoted_printable_decode($line);
1827                                        else
1828                                                $result .= quoted_printable_decode($line);
1829                                } else if ($mode == 3) {
1830                                        $line = rtrim($line, "\t\r\n\0\x0B");
1831                                        if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line))
1832                                                continue;
1833                                        if ($file)
1834                                                fwrite($file, convert_uudecode($line));
1835                        else if ($print)
1836                                                echo convert_uudecode($line);
1837                                        else
1838                                                $result .= convert_uudecode($line);
1839                                } else {
1840                                        $line = rtrim($line, "\t\r\n\0\x0B");
1841                                        if ($file)
1842                                                fwrite($file, $line . "\n");
1843                        else if ($print)
1844                                                echo $line . "\n";
1845                                        else
1846                                                $result .= $line . "\n";
1847                                }
1848                }
1849        }
1850           
1851        // read in anything up until last line
1852                if (!$end)
1853                        do {
1854                                $line = $this->readLine(1024);
1855                        } while (!$this->startsWith($line, $key, true));
1856
1857                if ($result) {
1858                if ($file) {
1859                        fwrite($file, $result);
1860                        } else if ($print) {
1861                        echo $result;
1862                } else
1863                        return $result;
1864                        return true;
1865                }
1866
1867            return false;
1868    }
1869
1870    function createFolder($folder)
1871    {
1872            if ($this->putLine('c CREATE "' . $this->escape($folder) . '"')) {
1873                    do {
1874                            $line = $this->readLine(300);
1875                    } while (!$this->startsWith($line, 'c ', true, true));
1876                    return ($this->parseResult($line) == 0);
1877            }
1878            return false;
1879    }
1880
1881    function renameFolder($from, $to)
1882    {
1883            if ($this->putLine('r RENAME "' . $this->escape($from) . '" "' . $this->escape($to) . '"')) {
1884                    do {
1885                            $line = $this->readLine(300);
1886                    } while (!$this->startsWith($line, 'r ', true, true));
1887                    return ($this->parseResult($line) == 0);
1888            }
1889            return false;
1890    }
1891
1892    function deleteFolder($folder)
1893    {
1894            if ($this->putLine('d DELETE "' . $this->escape($folder). '"')) {
1895                    do {
1896                            $line = $this->readLine(300);
1897                    } while (!$this->startsWith($line, 'd ', true, true));
1898                    return ($this->parseResult($line) == 0);
1899            }
1900            return false;
1901    }
1902
1903    function clearFolder($folder)
1904    {
1905            $num_in_trash = $this->countMessages($folder);
1906            if ($num_in_trash > 0) {
1907                    $this->delete($folder, '1:*');
1908            }
1909            return ($this->expunge($folder) >= 0);
1910    }
1911
1912    function subscribe($folder)
1913    {
1914            $query = 'sub1 SUBSCRIBE "' . $this->escape($folder). '"';
1915            $this->putLine($query);
1916
1917        $line = trim($this->readLine(512));
1918            return ($this->parseResult($line) == 0);
1919    }
1920
1921    function unsubscribe($folder)
1922    {
1923        $query = 'usub1 UNSUBSCRIBE "' . $this->escape($folder) . '"';
1924            $this->putLine($query);
1925   
1926            $line = trim($this->readLine(512));
1927            return ($this->parseResult($line) == 0);
1928    }
1929
1930    function append($folder, &$message)
1931    {
1932            if (!$folder) {
1933                    return false;
1934            }
1935
1936        $message = str_replace("\r", '', $message);
1937            $message = str_replace("\n", "\r\n", $message);
1938
1939        $len = strlen($message);
1940            if (!$len) {
1941                    return false;
1942            }
1943
1944            $request = 'a APPEND "' . $this->escape($folder) .'" (\\Seen) {' . $len . '}';
1945
1946            if ($this->putLine($request)) {
1947                    $line = $this->readLine(512);
1948
1949                if ($line[0] != '+') {
1950                        // $errornum = $this->parseResult($line);
1951                        $this->error = "Cannot write to folder: $line";
1952                            return false;
1953                }
1954
1955                if (!$this->putLine($message)) {
1956                return false;
1957            }
1958
1959                    do {
1960                            $line = $this->readLine();
1961                } while (!$this->startsWith($line, 'a ', true, true));
1962       
1963                $result = ($this->parseResult($line) == 0);
1964                    if (!$result) {
1965                        $this->error = $line;
1966                    }
1967                return $result;
1968            }
1969
1970            $this->error = "Couldn't send command \"$request\"";
1971            return false;
1972    }
1973
1974    function appendFromFile($folder, $path, $headers=null, $separator="\n\n")
1975    {
1976            if (!$folder) {
1977                return false;
1978            }
1979   
1980            // open message file
1981            $in_fp = false;
1982            if (file_exists(realpath($path))) {
1983                    $in_fp = fopen($path, 'r');
1984            }
1985            if (!$in_fp) { 
1986                    $this->error = "Couldn't open $path for reading";
1987                    return false;
1988            }
1989       
1990            $len = filesize($path);
1991            if (!$len) {
1992                    return false;
1993            }
1994
1995        if ($headers) {
1996            $headers = preg_replace('/[\r\n]+$/', '', $headers);
1997            $len += strlen($headers) + strlen($separator);
1998        }
1999
2000        // send APPEND command
2001            $request    = 'a APPEND "' . $this->escape($folder) . '" (\\Seen) {' . $len . '}';
2002            if ($this->putLine($request)) {
2003                    $line = $this->readLine(512);
2004
2005                    if ($line[0] != '+') {
2006                            //$errornum = $this->parseResult($line);
2007                            $this->error = "Cannot write to folder: $line";
2008                            return false;
2009                    }
2010
2011            // send headers with body separator
2012            if ($headers) {
2013                            $this->putLine($headers . $separator, false);
2014            }
2015
2016                    // send file
2017                    while (!feof($in_fp) && $this->fp) {
2018                            $buffer = fgets($in_fp, 4096);
2019                            $this->putLine($buffer, false);
2020                    }
2021                    fclose($in_fp);
2022
2023                    if (!$this->putLine('')) { // \r\n
2024                return false;
2025            }
2026
2027                    // read response
2028                    do {
2029                            $line = $this->readLine();
2030                    } while (!$this->startsWith($line, 'a ', true, true));
2031
2032                    $result = ($this->parseResult($line) == 0);
2033                    if (!$result) {
2034                        $this->error = $line;
2035                    }
2036
2037                    return $result;
2038            }
2039       
2040        $this->error = "Couldn't send command \"$request\"";
2041            return false;
2042    }
2043
2044    function fetchStructureString($folder, $id, $is_uid=false)
2045    {
2046            if (!$this->select($folder)) {
2047            return false;
2048        }
2049
2050                $key = 'F1247';
2051        $result = false;
2052
2053                if ($this->putLine($key . ($is_uid ? ' UID' : '') ." FETCH $id (BODYSTRUCTURE)")) {
2054                        do {
2055                                $line = $this->readLine(5000);
2056                                $line = $this->multLine($line, true);
2057                                if (!preg_match("/^$key/", $line))
2058                                        $result .= $line;
2059                        } while (!$this->startsWith($line, $key, true, true));
2060
2061                        $result = trim(substr($result, strpos($result, 'BODYSTRUCTURE')+13, -1));
2062                }
2063
2064        return $result;
2065    }
2066
2067    function getQuota()
2068    {
2069        /*
2070         * GETQUOTAROOT "INBOX"
2071         * QUOTAROOT INBOX user/rchijiiwa1
2072         * QUOTA user/rchijiiwa1 (STORAGE 654 9765)
2073         * OK Completed
2074         */
2075            $result      = false;
2076            $quota_lines = array();
2077       
2078            // get line(s) containing quota info
2079            if ($this->putLine('QUOT1 GETQUOTAROOT "INBOX"')) {
2080                    do {
2081                            $line = chop($this->readLine(5000));
2082                            if ($this->startsWith($line, '* QUOTA ')) {
2083                                    $quota_lines[] = $line;
2084                        }
2085                    } while (!$this->startsWith($line, 'QUOT1', true, true));
2086            }
2087       
2088            // return false if not found, parse if found
2089            $min_free = PHP_INT_MAX;
2090            foreach ($quota_lines as $key => $quota_line) {
2091                    $quota_line   = preg_replace('/[()]/', '', $quota_line);
2092                    $parts        = explode(' ', $quota_line);
2093                    $storage_part = array_search('STORAGE', $parts);
2094               
2095                    if (!$storage_part)
2096                continue;
2097       
2098                    $used  = intval($parts[$storage_part+1]);
2099                    $total = intval($parts[$storage_part+2]);
2100                    $free  = $total - $used; 
2101       
2102                    // return lowest available space from all quotas
2103                    if ($free < $min_free) { 
2104                        $min_free          = $free; 
2105                            $result['used']    = $used;
2106                            $result['total']   = $total;
2107                            $result['percent'] = min(100, round(($used/max(1,$total))*100));
2108                            $result['free']    = 100 - $result['percent'];
2109                    }
2110            }
2111
2112            return $result;
2113    }
2114
2115    private function _xor($string, $string2)
2116    {
2117            $result = '';
2118            $size = strlen($string);
2119            for ($i=0; $i<$size; $i++) {
2120                $result .= chr(ord($string[$i]) ^ ord($string2[$i]));
2121            }
2122            return $result;
2123    }
2124
2125    private function strToTime($date)
2126    {
2127            // support non-standard "GMTXXXX" literal
2128            $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date);
2129        // if date parsing fails, we have a date in non-rfc format.
2130            // remove token from the end and try again
2131            while ((($ts = @strtotime($date))===false) || ($ts < 0)) {
2132                $d = explode(' ', $date);
2133                    array_pop($d);
2134                    if (!$d) break;
2135                    $date = implode(' ', $d);
2136            }
2137
2138            $ts = (int) $ts;
2139
2140            return $ts < 0 ? 0 : $ts;   
2141    }
2142
2143    private function SplitHeaderLine($string)
2144    {
2145            $pos = strpos($string, ':');
2146            if ($pos>0) {
2147                    $res[0] = substr($string, 0, $pos);
2148                    $res[1] = trim(substr($string, $pos+1));
2149                    return $res;
2150            }
2151        return $string;
2152    }
2153
2154    private function parseNamespace($str, &$i, $len=0, $l)
2155    {
2156            if (!$l) {
2157                $str = str_replace('NIL', '()', $str);
2158            }
2159            if (!$len) {
2160                $len = strlen($str);
2161            }
2162            $data      = array();
2163            $in_quotes = false;
2164            $elem      = 0;
2165       
2166        for ($i;$i<$len;$i++) {
2167                    $c = (string)$str[$i];
2168                    if ($c == '(' && !$in_quotes) {
2169                            $i++;
2170                            $data[$elem] = $this->parseNamespace($str, $i, $len, $l++);
2171                            $elem++;
2172                    } else if ($c == ')' && !$in_quotes) {
2173                            return $data;
2174                } else if ($c == '\\') {
2175                            $i++;
2176                            if ($in_quotes) {
2177                                    $data[$elem] .= $c.$str[$i];
2178                        }
2179                    } else if ($c == '"') {
2180                            $in_quotes = !$in_quotes;
2181                            if (!$in_quotes) {
2182                                    $elem++;
2183                        }
2184                    } else if ($in_quotes) {
2185                            $data[$elem].=$c;
2186                    }
2187            }
2188           
2189        return $data;
2190    }
2191
2192    private function escape($string)
2193    {
2194            return strtr($string, array('"'=>'\\"', '\\' => '\\\\')); 
2195    }
2196
2197    private function unEscape($string)
2198    {
2199            return strtr($string, array('\\"'=>'"', '\\\\' => '\\')); 
2200    }
2201
2202}
2203
2204?>
Note: See TracBrowser for help on using the repository browser.