0
|
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 }
|