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