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