PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 10.1.4
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v10.1.4
11.1.1 11.1.0 11.0.5 11.0.4 11.0.3 11.0.2 11.0.1 11.0.0 10.11.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 trunk 10.0.0 10.0.1 10.1.0 10.1.1 10.1.2 10.1.3 10.1.4 10.2.0 10.2.1 10.2.2 All 62 releases
acymailing / vendor / javanile / php-imap2 / src / Roundcube / ImapClient.php

ImapClient.php in AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress 10.1.4, at vendor/javanile/php-imap2/src/Roundcube/ImapClient.php

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