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