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