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