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