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