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