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