Mercurial > hg > rc2
comparison program/lib/Roundcube/rcube_imap.php @ 0:4681f974d28b
vanilla 1.3.3 distro, I hope
author | Charlie Root |
---|---|
date | Thu, 04 Jan 2018 15:52:31 -0500 |
parents | |
children | 3a5f959af5ae |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4681f974d28b |
---|---|
1 <?php | |
2 | |
3 /** | |
4 +-----------------------------------------------------------------------+ | |
5 | This file is part of the Roundcube Webmail client | | |
6 | Copyright (C) 2005-2012, 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 | IMAP Storage Engine | | |
15 +-----------------------------------------------------------------------+ | |
16 | Author: Thomas Bruederli <roundcube@gmail.com> | | |
17 | Author: Aleksander Machniak <alec@alec.pl> | | |
18 +-----------------------------------------------------------------------+ | |
19 */ | |
20 | |
21 /** | |
22 * Interface class for accessing an IMAP server | |
23 * | |
24 * @package Framework | |
25 * @subpackage Storage | |
26 * @author Thomas Bruederli <roundcube@gmail.com> | |
27 * @author Aleksander Machniak <alec@alec.pl> | |
28 */ | |
29 class rcube_imap extends rcube_storage | |
30 { | |
31 /** | |
32 * Instance of rcube_imap_generic | |
33 * | |
34 * @var rcube_imap_generic | |
35 */ | |
36 public $conn; | |
37 | |
38 /** | |
39 * Instance of rcube_imap_cache | |
40 * | |
41 * @var rcube_imap_cache | |
42 */ | |
43 protected $mcache; | |
44 | |
45 /** | |
46 * Instance of rcube_cache | |
47 * | |
48 * @var rcube_cache | |
49 */ | |
50 protected $cache; | |
51 | |
52 /** | |
53 * Internal (in-memory) cache | |
54 * | |
55 * @var array | |
56 */ | |
57 protected $icache = array(); | |
58 | |
59 protected $plugins; | |
60 protected $delimiter; | |
61 protected $namespace; | |
62 protected $sort_field = ''; | |
63 protected $sort_order = 'DESC'; | |
64 protected $struct_charset; | |
65 protected $search_set; | |
66 protected $search_string = ''; | |
67 protected $search_charset = ''; | |
68 protected $search_sort_field = ''; | |
69 protected $search_threads = false; | |
70 protected $search_sorted = false; | |
71 protected $options = array('auth_type' => 'check'); | |
72 protected $caching = false; | |
73 protected $messages_caching = false; | |
74 protected $threading = false; | |
75 | |
76 | |
77 /** | |
78 * Object constructor. | |
79 */ | |
80 public function __construct() | |
81 { | |
82 $this->conn = new rcube_imap_generic(); | |
83 $this->plugins = rcube::get_instance()->plugins; | |
84 | |
85 // Set namespace and delimiter from session, | |
86 // so some methods would work before connection | |
87 if (isset($_SESSION['imap_namespace'])) { | |
88 $this->namespace = $_SESSION['imap_namespace']; | |
89 } | |
90 if (isset($_SESSION['imap_delimiter'])) { | |
91 $this->delimiter = $_SESSION['imap_delimiter']; | |
92 } | |
93 } | |
94 | |
95 /** | |
96 * Magic getter for backward compat. | |
97 * | |
98 * @deprecated. | |
99 */ | |
100 public function __get($name) | |
101 { | |
102 if (isset($this->{$name})) { | |
103 return $this->{$name}; | |
104 } | |
105 } | |
106 | |
107 /** | |
108 * Connect to an IMAP server | |
109 * | |
110 * @param string $host Host to connect | |
111 * @param string $user Username for IMAP account | |
112 * @param string $pass Password for IMAP account | |
113 * @param integer $port Port to connect to | |
114 * @param string $use_ssl SSL schema (either ssl or tls) or null if plain connection | |
115 * | |
116 * @return boolean True on success, False on failure | |
117 */ | |
118 public function connect($host, $user, $pass, $port=143, $use_ssl=null) | |
119 { | |
120 // check for OpenSSL support in PHP build | |
121 if ($use_ssl && extension_loaded('openssl')) { | |
122 $this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl; | |
123 } | |
124 else if ($use_ssl) { | |
125 rcube::raise_error(array('code' => 403, 'type' => 'imap', | |
126 'file' => __FILE__, 'line' => __LINE__, | |
127 'message' => "OpenSSL not available"), true, false); | |
128 $port = 143; | |
129 } | |
130 | |
131 $this->options['port'] = $port; | |
132 | |
133 if ($this->options['debug']) { | |
134 $this->set_debug(true); | |
135 | |
136 $this->options['ident'] = array( | |
137 'name' => 'Roundcube', | |
138 'version' => RCUBE_VERSION, | |
139 'php' => PHP_VERSION, | |
140 'os' => PHP_OS, | |
141 'command' => $_SERVER['REQUEST_URI'], | |
142 ); | |
143 } | |
144 | |
145 $attempt = 0; | |
146 do { | |
147 $data = $this->plugins->exec_hook('storage_connect', | |
148 array_merge($this->options, array('host' => $host, 'user' => $user, | |
149 'attempt' => ++$attempt))); | |
150 | |
151 if (!empty($data['pass'])) { | |
152 $pass = $data['pass']; | |
153 } | |
154 | |
155 // Handle per-host socket options | |
156 rcube_utils::parse_socket_options($data['socket_options'], $data['host']); | |
157 | |
158 $this->conn->connect($data['host'], $data['user'], $pass, $data); | |
159 } while(!$this->conn->connected() && $data['retry']); | |
160 | |
161 $config = array( | |
162 'host' => $data['host'], | |
163 'user' => $data['user'], | |
164 'password' => $pass, | |
165 'port' => $port, | |
166 'ssl' => $use_ssl, | |
167 ); | |
168 | |
169 $this->options = array_merge($this->options, $config); | |
170 $this->connect_done = true; | |
171 | |
172 if ($this->conn->connected()) { | |
173 // check for session identifier | |
174 $session = null; | |
175 if (preg_match('/\s+SESSIONID=([^=\s]+)/', $this->conn->result, $m)) { | |
176 $session = $m[1]; | |
177 } | |
178 | |
179 // get namespace and delimiter | |
180 $this->set_env(); | |
181 | |
182 // trigger post-connect hook | |
183 $this->plugins->exec_hook('storage_connected', array( | |
184 'host' => $host, 'user' => $user, 'session' => $session | |
185 )); | |
186 | |
187 return true; | |
188 } | |
189 // write error log | |
190 else if ($this->conn->error) { | |
191 if ($pass && $user) { | |
192 $message = sprintf("Login failed for %s from %s. %s", | |
193 $user, rcube_utils::remote_ip(), $this->conn->error); | |
194 | |
195 rcube::raise_error(array('code' => 403, 'type' => 'imap', | |
196 'file' => __FILE__, 'line' => __LINE__, | |
197 'message' => $message), true, false); | |
198 } | |
199 } | |
200 | |
201 return false; | |
202 } | |
203 | |
204 /** | |
205 * Close IMAP connection. | |
206 * Usually done on script shutdown | |
207 */ | |
208 public function close() | |
209 { | |
210 $this->connect_done = false; | |
211 $this->conn->closeConnection(); | |
212 | |
213 if ($this->mcache) { | |
214 $this->mcache->close(); | |
215 } | |
216 } | |
217 | |
218 /** | |
219 * Check connection state, connect if not connected. | |
220 * | |
221 * @return bool Connection state. | |
222 */ | |
223 public function check_connection() | |
224 { | |
225 // Establish connection if it wasn't done yet | |
226 if (!$this->connect_done && !empty($this->options['user'])) { | |
227 return $this->connect( | |
228 $this->options['host'], | |
229 $this->options['user'], | |
230 $this->options['password'], | |
231 $this->options['port'], | |
232 $this->options['ssl'] | |
233 ); | |
234 } | |
235 | |
236 return $this->is_connected(); | |
237 } | |
238 | |
239 /** | |
240 * Checks IMAP connection. | |
241 * | |
242 * @return boolean TRUE on success, FALSE on failure | |
243 */ | |
244 public function is_connected() | |
245 { | |
246 return $this->conn->connected(); | |
247 } | |
248 | |
249 /** | |
250 * Returns code of last error | |
251 * | |
252 * @return int Error code | |
253 */ | |
254 public function get_error_code() | |
255 { | |
256 return $this->conn->errornum; | |
257 } | |
258 | |
259 /** | |
260 * Returns text of last error | |
261 * | |
262 * @return string Error string | |
263 */ | |
264 public function get_error_str() | |
265 { | |
266 return $this->conn->error; | |
267 } | |
268 | |
269 /** | |
270 * Returns code of last command response | |
271 * | |
272 * @return int Response code | |
273 */ | |
274 public function get_response_code() | |
275 { | |
276 switch ($this->conn->resultcode) { | |
277 case 'NOPERM': | |
278 return self::NOPERM; | |
279 case 'READ-ONLY': | |
280 return self::READONLY; | |
281 case 'TRYCREATE': | |
282 return self::TRYCREATE; | |
283 case 'INUSE': | |
284 return self::INUSE; | |
285 case 'OVERQUOTA': | |
286 return self::OVERQUOTA; | |
287 case 'ALREADYEXISTS': | |
288 return self::ALREADYEXISTS; | |
289 case 'NONEXISTENT': | |
290 return self::NONEXISTENT; | |
291 case 'CONTACTADMIN': | |
292 return self::CONTACTADMIN; | |
293 default: | |
294 return self::UNKNOWN; | |
295 } | |
296 } | |
297 | |
298 /** | |
299 * Activate/deactivate debug mode | |
300 * | |
301 * @param boolean $dbg True if IMAP conversation should be logged | |
302 */ | |
303 public function set_debug($dbg = true) | |
304 { | |
305 $this->options['debug'] = $dbg; | |
306 $this->conn->setDebug($dbg, array($this, 'debug_handler')); | |
307 } | |
308 | |
309 /** | |
310 * Set internal folder reference. | |
311 * All operations will be performed on this folder. | |
312 * | |
313 * @param string $folder Folder name | |
314 */ | |
315 public function set_folder($folder) | |
316 { | |
317 $this->folder = $folder; | |
318 } | |
319 | |
320 /** | |
321 * Save a search result for future message listing methods | |
322 * | |
323 * @param array $set Search set, result from rcube_imap::get_search_set(): | |
324 * 0 - searching criteria, string | |
325 * 1 - search result, rcube_result_index|rcube_result_thread | |
326 * 2 - searching character set, string | |
327 * 3 - sorting field, string | |
328 * 4 - true if sorted, bool | |
329 */ | |
330 public function set_search_set($set) | |
331 { | |
332 $set = (array)$set; | |
333 | |
334 $this->search_string = $set[0]; | |
335 $this->search_set = $set[1]; | |
336 $this->search_charset = $set[2]; | |
337 $this->search_sort_field = $set[3]; | |
338 $this->search_sorted = $set[4]; | |
339 $this->search_threads = is_a($this->search_set, 'rcube_result_thread'); | |
340 | |
341 if (is_a($this->search_set, 'rcube_result_multifolder')) { | |
342 $this->set_threading(false); | |
343 } | |
344 } | |
345 | |
346 /** | |
347 * Return the saved search set as hash array | |
348 * | |
349 * @return array Search set | |
350 */ | |
351 public function get_search_set() | |
352 { | |
353 if (empty($this->search_set)) { | |
354 return null; | |
355 } | |
356 | |
357 return array( | |
358 $this->search_string, | |
359 $this->search_set, | |
360 $this->search_charset, | |
361 $this->search_sort_field, | |
362 $this->search_sorted, | |
363 ); | |
364 } | |
365 | |
366 /** | |
367 * Returns the IMAP server's capability. | |
368 * | |
369 * @param string $cap Capability name | |
370 * | |
371 * @return mixed Capability value or TRUE if supported, FALSE if not | |
372 */ | |
373 public function get_capability($cap) | |
374 { | |
375 $cap = strtoupper($cap); | |
376 $sess_key = "STORAGE_$cap"; | |
377 | |
378 if (!isset($_SESSION[$sess_key])) { | |
379 if (!$this->check_connection()) { | |
380 return false; | |
381 } | |
382 | |
383 $_SESSION[$sess_key] = $this->conn->getCapability($cap); | |
384 } | |
385 | |
386 return $_SESSION[$sess_key]; | |
387 } | |
388 | |
389 /** | |
390 * Checks the PERMANENTFLAGS capability of the current folder | |
391 * and returns true if the given flag is supported by the IMAP server | |
392 * | |
393 * @param string $flag Permanentflag name | |
394 * | |
395 * @return boolean True if this flag is supported | |
396 */ | |
397 public function check_permflag($flag) | |
398 { | |
399 $flag = strtoupper($flag); | |
400 $perm_flags = $this->get_permflags($this->folder); | |
401 $imap_flag = $this->conn->flags[$flag]; | |
402 | |
403 return $imap_flag && !empty($perm_flags) && in_array_nocase($imap_flag, $perm_flags); | |
404 } | |
405 | |
406 /** | |
407 * Returns PERMANENTFLAGS of the specified folder | |
408 * | |
409 * @param string $folder Folder name | |
410 * | |
411 * @return array Flags | |
412 */ | |
413 public function get_permflags($folder) | |
414 { | |
415 if (!strlen($folder)) { | |
416 return array(); | |
417 } | |
418 | |
419 if (!$this->check_connection()) { | |
420 return array(); | |
421 } | |
422 | |
423 if ($this->conn->select($folder)) { | |
424 $permflags = $this->conn->data['PERMANENTFLAGS']; | |
425 } | |
426 else { | |
427 return array(); | |
428 } | |
429 | |
430 if (!is_array($permflags)) { | |
431 $permflags = array(); | |
432 } | |
433 | |
434 return $permflags; | |
435 } | |
436 | |
437 /** | |
438 * Returns the delimiter that is used by the IMAP server for folder separation | |
439 * | |
440 * @return string Delimiter string | |
441 */ | |
442 public function get_hierarchy_delimiter() | |
443 { | |
444 return $this->delimiter; | |
445 } | |
446 | |
447 /** | |
448 * Get namespace | |
449 * | |
450 * @param string $name Namespace array index: personal, other, shared, prefix | |
451 * | |
452 * @return array Namespace data | |
453 */ | |
454 public function get_namespace($name = null) | |
455 { | |
456 $ns = $this->namespace; | |
457 | |
458 if ($name) { | |
459 // an alias for BC | |
460 if ($name == 'prefix') { | |
461 $name = 'prefix_in'; | |
462 } | |
463 | |
464 return isset($ns[$name]) ? $ns[$name] : null; | |
465 } | |
466 | |
467 unset($ns['prefix_in'], $ns['prefix_out']); | |
468 | |
469 return $ns; | |
470 } | |
471 | |
472 /** | |
473 * Sets delimiter and namespaces | |
474 */ | |
475 protected function set_env() | |
476 { | |
477 if ($this->delimiter !== null && $this->namespace !== null) { | |
478 return; | |
479 } | |
480 | |
481 $config = rcube::get_instance()->config; | |
482 $imap_personal = $config->get('imap_ns_personal'); | |
483 $imap_other = $config->get('imap_ns_other'); | |
484 $imap_shared = $config->get('imap_ns_shared'); | |
485 $imap_delimiter = $config->get('imap_delimiter'); | |
486 | |
487 if (!$this->check_connection()) { | |
488 return; | |
489 } | |
490 | |
491 $ns = $this->conn->getNamespace(); | |
492 | |
493 // Set namespaces (NAMESPACE supported) | |
494 if (is_array($ns)) { | |
495 $this->namespace = $ns; | |
496 } | |
497 else { | |
498 $this->namespace = array( | |
499 'personal' => NULL, | |
500 'other' => NULL, | |
501 'shared' => NULL, | |
502 ); | |
503 } | |
504 | |
505 if ($imap_delimiter) { | |
506 $this->delimiter = $imap_delimiter; | |
507 } | |
508 if (empty($this->delimiter)) { | |
509 $this->delimiter = $this->namespace['personal'][0][1]; | |
510 } | |
511 if (empty($this->delimiter)) { | |
512 $this->delimiter = $this->conn->getHierarchyDelimiter(); | |
513 } | |
514 if (empty($this->delimiter)) { | |
515 $this->delimiter = '/'; | |
516 } | |
517 | |
518 // Overwrite namespaces | |
519 if ($imap_personal !== null) { | |
520 $this->namespace['personal'] = NULL; | |
521 foreach ((array)$imap_personal as $dir) { | |
522 $this->namespace['personal'][] = array($dir, $this->delimiter); | |
523 } | |
524 } | |
525 if ($imap_other !== null) { | |
526 $this->namespace['other'] = NULL; | |
527 foreach ((array)$imap_other as $dir) { | |
528 if ($dir) { | |
529 $this->namespace['other'][] = array($dir, $this->delimiter); | |
530 } | |
531 } | |
532 } | |
533 if ($imap_shared !== null) { | |
534 $this->namespace['shared'] = NULL; | |
535 foreach ((array)$imap_shared as $dir) { | |
536 if ($dir) { | |
537 $this->namespace['shared'][] = array($dir, $this->delimiter); | |
538 } | |
539 } | |
540 } | |
541 | |
542 // Find personal namespace prefix(es) for self::mod_folder() | |
543 if (is_array($this->namespace['personal']) && !empty($this->namespace['personal'])) { | |
544 // There can be more than one namespace root, | |
545 // - for prefix_out get the first one but only | |
546 // if there is only one root | |
547 // - for prefix_in get the first one but only | |
548 // if there is no non-prefixed namespace root (#5403) | |
549 $roots = array(); | |
550 foreach ($this->namespace['personal'] as $ns) { | |
551 $roots[] = $ns[0]; | |
552 } | |
553 | |
554 if (!in_array('', $roots)) { | |
555 $this->namespace['prefix_in'] = $roots[0]; | |
556 } | |
557 if (count($roots) == 1) { | |
558 $this->namespace['prefix_out'] = $roots[0]; | |
559 } | |
560 } | |
561 | |
562 $_SESSION['imap_namespace'] = $this->namespace; | |
563 $_SESSION['imap_delimiter'] = $this->delimiter; | |
564 } | |
565 | |
566 /** | |
567 * Returns IMAP server vendor name | |
568 * | |
569 * @return string Vendor name | |
570 * @since 1.2 | |
571 */ | |
572 public function get_vendor() | |
573 { | |
574 if ($_SESSION['imap_vendor'] !== null) { | |
575 return $_SESSION['imap_vendor']; | |
576 } | |
577 | |
578 $config = rcube::get_instance()->config; | |
579 $imap_vendor = $config->get('imap_vendor'); | |
580 | |
581 if ($imap_vendor) { | |
582 return $imap_vendor; | |
583 } | |
584 | |
585 if (!$this->check_connection()) { | |
586 return; | |
587 } | |
588 | |
589 if (($ident = $this->conn->data['ID']) === null) { | |
590 $ident = $this->conn->id(array( | |
591 'name' => 'Roundcube', | |
592 'version' => RCUBE_VERSION, | |
593 'php' => PHP_VERSION, | |
594 'os' => PHP_OS, | |
595 )); | |
596 } | |
597 | |
598 $vendor = (string) (!empty($ident) ? $ident['name'] : ''); | |
599 $ident = strtolower($vendor . ' ' . $this->conn->data['GREETING']); | |
600 $vendors = array('cyrus', 'dovecot', 'uw-imap', 'gmail', 'hmail'); | |
601 | |
602 foreach ($vendors as $v) { | |
603 if (strpos($ident, $v) !== false) { | |
604 $vendor = $v; | |
605 break; | |
606 } | |
607 } | |
608 | |
609 return $_SESSION['imap_vendor'] = $vendor; | |
610 } | |
611 | |
612 /** | |
613 * Get message count for a specific folder | |
614 * | |
615 * @param string $folder Folder name | |
616 * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS] | |
617 * @param boolean $force Force reading from server and update cache | |
618 * @param boolean $status Enables storing folder status info (max UID/count), | |
619 * required for folder_status() | |
620 * | |
621 * @return int Number of messages | |
622 */ | |
623 public function count($folder='', $mode='ALL', $force=false, $status=true) | |
624 { | |
625 if (!strlen($folder)) { | |
626 $folder = $this->folder; | |
627 } | |
628 | |
629 return $this->countmessages($folder, $mode, $force, $status); | |
630 } | |
631 | |
632 /** | |
633 * Protected method for getting number of messages | |
634 * | |
635 * @param string $folder Folder name | |
636 * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS] | |
637 * @param boolean $force Force reading from server and update cache | |
638 * @param boolean $status Enables storing folder status info (max UID/count), | |
639 * required for folder_status() | |
640 * @param boolean $no_search Ignore current search result | |
641 * | |
642 * @return int Number of messages | |
643 * @see rcube_imap::count() | |
644 */ | |
645 protected function countmessages($folder, $mode = 'ALL', $force = false, $status = true, $no_search = false) | |
646 { | |
647 $mode = strtoupper($mode); | |
648 | |
649 // Count search set, assume search set is always up-to-date (don't check $force flag) | |
650 // @TODO: this could be handled in more reliable way, e.g. a separate method | |
651 // maybe in rcube_imap_search | |
652 if (!$no_search && $this->search_string && $folder == $this->folder) { | |
653 if ($mode == 'ALL') { | |
654 return $this->search_set->count_messages(); | |
655 } | |
656 else if ($mode == 'THREADS') { | |
657 return $this->search_set->count(); | |
658 } | |
659 } | |
660 | |
661 // EXISTS is a special alias for ALL, it allows to get the number | |
662 // of all messages in a folder also when search is active and with | |
663 // any skip_deleted setting | |
664 | |
665 $a_folder_cache = $this->get_cache('messagecount'); | |
666 | |
667 // return cached value | |
668 if (!$force && is_array($a_folder_cache[$folder]) && isset($a_folder_cache[$folder][$mode])) { | |
669 return $a_folder_cache[$folder][$mode]; | |
670 } | |
671 | |
672 if (!is_array($a_folder_cache[$folder])) { | |
673 $a_folder_cache[$folder] = array(); | |
674 } | |
675 | |
676 if ($mode == 'THREADS') { | |
677 $res = $this->threads($folder); | |
678 $count = $res->count(); | |
679 | |
680 if ($status) { | |
681 $msg_count = $res->count_messages(); | |
682 $this->set_folder_stats($folder, 'cnt', $msg_count); | |
683 $this->set_folder_stats($folder, 'maxuid', $msg_count ? $this->id2uid($msg_count, $folder) : 0); | |
684 } | |
685 } | |
686 // Need connection here | |
687 else if (!$this->check_connection()) { | |
688 return 0; | |
689 } | |
690 // RECENT count is fetched a bit different | |
691 else if ($mode == 'RECENT') { | |
692 $count = $this->conn->countRecent($folder); | |
693 } | |
694 // use SEARCH for message counting | |
695 else if ($mode != 'EXISTS' && !empty($this->options['skip_deleted'])) { | |
696 $search_str = "ALL UNDELETED"; | |
697 $keys = array('COUNT'); | |
698 | |
699 if ($mode == 'UNSEEN') { | |
700 $search_str .= " UNSEEN"; | |
701 } | |
702 else { | |
703 if ($this->messages_caching) { | |
704 $keys[] = 'ALL'; | |
705 } | |
706 if ($status) { | |
707 $keys[] = 'MAX'; | |
708 } | |
709 } | |
710 | |
711 // @TODO: if $mode == 'ALL' we could try to use cache index here | |
712 | |
713 // get message count using (E)SEARCH | |
714 // not very performant but more precise (using UNDELETED) | |
715 $index = $this->conn->search($folder, $search_str, true, $keys); | |
716 $count = $index->count(); | |
717 | |
718 if ($mode == 'ALL') { | |
719 // Cache index data, will be used in index_direct() | |
720 $this->icache['undeleted_idx'] = $index; | |
721 | |
722 if ($status) { | |
723 $this->set_folder_stats($folder, 'cnt', $count); | |
724 $this->set_folder_stats($folder, 'maxuid', $index->max()); | |
725 } | |
726 } | |
727 } | |
728 else { | |
729 if ($mode == 'UNSEEN') { | |
730 $count = $this->conn->countUnseen($folder); | |
731 } | |
732 else { | |
733 $count = $this->conn->countMessages($folder); | |
734 if ($status && $mode == 'ALL') { | |
735 $this->set_folder_stats($folder, 'cnt', $count); | |
736 $this->set_folder_stats($folder, 'maxuid', $count ? $this->id2uid($count, $folder) : 0); | |
737 } | |
738 } | |
739 } | |
740 | |
741 $a_folder_cache[$folder][$mode] = (int)$count; | |
742 | |
743 // write back to cache | |
744 $this->update_cache('messagecount', $a_folder_cache); | |
745 | |
746 return (int)$count; | |
747 } | |
748 | |
749 /** | |
750 * Public method for listing message flags | |
751 * | |
752 * @param string $folder Folder name | |
753 * @param array $uids Message UIDs | |
754 * @param int $mod_seq Optional MODSEQ value (of last flag update) | |
755 * | |
756 * @return array Indexed array with message flags | |
757 */ | |
758 public function list_flags($folder, $uids, $mod_seq = null) | |
759 { | |
760 if (!strlen($folder)) { | |
761 $folder = $this->folder; | |
762 } | |
763 | |
764 if (!$this->check_connection()) { | |
765 return array(); | |
766 } | |
767 | |
768 // @TODO: when cache was synchronized in this request | |
769 // we might already have asked for flag updates, use it. | |
770 | |
771 $flags = $this->conn->fetch($folder, $uids, true, array('FLAGS'), $mod_seq); | |
772 $result = array(); | |
773 | |
774 if (!empty($flags)) { | |
775 foreach ($flags as $message) { | |
776 $result[$message->uid] = $message->flags; | |
777 } | |
778 } | |
779 | |
780 return $result; | |
781 } | |
782 | |
783 /** | |
784 * Public method for listing headers | |
785 * | |
786 * @param string $folder Folder name | |
787 * @param int $page Current page to list | |
788 * @param string $sort_field Header field to sort by | |
789 * @param string $sort_order Sort order [ASC|DESC] | |
790 * @param int $slice Number of slice items to extract from result array | |
791 * | |
792 * @return array Indexed array with message header objects | |
793 */ | |
794 public function list_messages($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0) | |
795 { | |
796 if (!strlen($folder)) { | |
797 $folder = $this->folder; | |
798 } | |
799 | |
800 return $this->_list_messages($folder, $page, $sort_field, $sort_order, $slice); | |
801 } | |
802 | |
803 /** | |
804 * protected method for listing message headers | |
805 * | |
806 * @param string $folder Folder name | |
807 * @param int $page Current page to list | |
808 * @param string $sort_field Header field to sort by | |
809 * @param string $sort_order Sort order [ASC|DESC] | |
810 * @param int $slice Number of slice items to extract from result array | |
811 * | |
812 * @return array Indexed array with message header objects | |
813 * @see rcube_imap::list_messages | |
814 */ | |
815 protected function _list_messages($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0) | |
816 { | |
817 if (!strlen($folder)) { | |
818 return array(); | |
819 } | |
820 | |
821 $this->set_sort_order($sort_field, $sort_order); | |
822 $page = $page ? $page : $this->list_page; | |
823 | |
824 // use saved message set | |
825 if ($this->search_string) { | |
826 return $this->list_search_messages($folder, $page, $slice); | |
827 } | |
828 | |
829 if ($this->threading) { | |
830 return $this->list_thread_messages($folder, $page, $slice); | |
831 } | |
832 | |
833 // get UIDs of all messages in the folder, sorted | |
834 $index = $this->index($folder, $this->sort_field, $this->sort_order); | |
835 | |
836 if ($index->is_empty()) { | |
837 return array(); | |
838 } | |
839 | |
840 $from = ($page-1) * $this->page_size; | |
841 $to = $from + $this->page_size; | |
842 | |
843 $index->slice($from, $to - $from); | |
844 | |
845 if ($slice) { | |
846 $index->slice(-$slice, $slice); | |
847 } | |
848 | |
849 // fetch reqested messages headers | |
850 $a_index = $index->get(); | |
851 $a_msg_headers = $this->fetch_headers($folder, $a_index); | |
852 | |
853 return array_values($a_msg_headers); | |
854 } | |
855 | |
856 /** | |
857 * protected method for listing message headers using threads | |
858 * | |
859 * @param string $folder Folder name | |
860 * @param int $page Current page to list | |
861 * @param int $slice Number of slice items to extract from result array | |
862 * | |
863 * @return array Indexed array with message header objects | |
864 * @see rcube_imap::list_messages | |
865 */ | |
866 protected function list_thread_messages($folder, $page, $slice=0) | |
867 { | |
868 // get all threads (not sorted) | |
869 if ($mcache = $this->get_mcache_engine()) { | |
870 $threads = $mcache->get_thread($folder); | |
871 } | |
872 else { | |
873 $threads = $this->threads($folder); | |
874 } | |
875 | |
876 return $this->fetch_thread_headers($folder, $threads, $page, $slice); | |
877 } | |
878 | |
879 /** | |
880 * Method for fetching threads data | |
881 * | |
882 * @param string $folder Folder name | |
883 * | |
884 * @return rcube_imap_thread Thread data object | |
885 */ | |
886 function threads($folder) | |
887 { | |
888 if ($mcache = $this->get_mcache_engine()) { | |
889 // don't store in self's internal cache, cache has it's own internal cache | |
890 return $mcache->get_thread($folder); | |
891 } | |
892 | |
893 if (!empty($this->icache['threads'])) { | |
894 if ($this->icache['threads']->get_parameters('MAILBOX') == $folder) { | |
895 return $this->icache['threads']; | |
896 } | |
897 } | |
898 | |
899 // get all threads | |
900 $result = $this->threads_direct($folder); | |
901 | |
902 // add to internal (fast) cache | |
903 return $this->icache['threads'] = $result; | |
904 } | |
905 | |
906 /** | |
907 * Method for direct fetching of threads data | |
908 * | |
909 * @param string $folder Folder name | |
910 * | |
911 * @return rcube_imap_thread Thread data object | |
912 */ | |
913 function threads_direct($folder) | |
914 { | |
915 if (!$this->check_connection()) { | |
916 return new rcube_result_thread(); | |
917 } | |
918 | |
919 // get all threads | |
920 return $this->conn->thread($folder, $this->threading, | |
921 $this->options['skip_deleted'] ? 'UNDELETED' : '', true); | |
922 } | |
923 | |
924 /** | |
925 * protected method for fetching threaded messages headers | |
926 * | |
927 * @param string $folder Folder name | |
928 * @param rcube_result_thread $threads Threads data object | |
929 * @param int $page List page number | |
930 * @param int $slice Number of threads to slice | |
931 * | |
932 * @return array Messages headers | |
933 */ | |
934 protected function fetch_thread_headers($folder, $threads, $page, $slice=0) | |
935 { | |
936 // Sort thread structure | |
937 $this->sort_threads($threads); | |
938 | |
939 $from = ($page-1) * $this->page_size; | |
940 $to = $from + $this->page_size; | |
941 | |
942 $threads->slice($from, $to - $from); | |
943 | |
944 if ($slice) { | |
945 $threads->slice(-$slice, $slice); | |
946 } | |
947 | |
948 // Get UIDs of all messages in all threads | |
949 $a_index = $threads->get(); | |
950 | |
951 // fetch reqested headers from server | |
952 $a_msg_headers = $this->fetch_headers($folder, $a_index); | |
953 | |
954 unset($a_index); | |
955 | |
956 // Set depth, has_children and unread_children fields in headers | |
957 $this->set_thread_flags($a_msg_headers, $threads); | |
958 | |
959 return array_values($a_msg_headers); | |
960 } | |
961 | |
962 /** | |
963 * protected method for setting threaded messages flags: | |
964 * depth, has_children, unread_children, flagged_children | |
965 * | |
966 * @param array $headers Reference to headers array indexed by message UID | |
967 * @param rcube_result_thread $threads Threads data object | |
968 * | |
969 * @return array Message headers array indexed by message UID | |
970 */ | |
971 protected function set_thread_flags(&$headers, $threads) | |
972 { | |
973 $parents = array(); | |
974 | |
975 list ($msg_depth, $msg_children) = $threads->get_thread_data(); | |
976 | |
977 foreach ($headers as $uid => $header) { | |
978 $depth = $msg_depth[$uid]; | |
979 $parents = array_slice($parents, 0, $depth); | |
980 | |
981 if (!empty($parents)) { | |
982 $headers[$uid]->parent_uid = end($parents); | |
983 if (empty($header->flags['SEEN'])) { | |
984 $headers[$parents[0]]->unread_children++; | |
985 } | |
986 if (!empty($header->flags['FLAGGED'])) { | |
987 $headers[$parents[0]]->flagged_children++; | |
988 } | |
989 } | |
990 array_push($parents, $uid); | |
991 | |
992 $headers[$uid]->depth = $depth; | |
993 $headers[$uid]->has_children = $msg_children[$uid]; | |
994 } | |
995 } | |
996 | |
997 /** | |
998 * protected method for listing a set of message headers (search results) | |
999 * | |
1000 * @param string $folder Folder name | |
1001 * @param int $page Current page to list | |
1002 * @param int $slice Number of slice items to extract from result array | |
1003 * | |
1004 * @return array Indexed array with message header objects | |
1005 */ | |
1006 protected function list_search_messages($folder, $page, $slice=0) | |
1007 { | |
1008 if (!strlen($folder) || empty($this->search_set) || $this->search_set->is_empty()) { | |
1009 return array(); | |
1010 } | |
1011 | |
1012 // gather messages from a multi-folder search | |
1013 if ($this->search_set->multi) { | |
1014 $page_size = $this->page_size; | |
1015 $sort_field = $this->sort_field; | |
1016 $search_set = $this->search_set; | |
1017 | |
1018 // prepare paging | |
1019 $cnt = $search_set->count(); | |
1020 $from = ($page-1) * $page_size; | |
1021 $to = $from + $page_size; | |
1022 $slice_length = min($page_size, $cnt - $from); | |
1023 | |
1024 // fetch resultset headers, sort and slice them | |
1025 if (!empty($sort_field) && $search_set->get_parameters('SORT') != $sort_field) { | |
1026 $this->sort_field = null; | |
1027 $this->page_size = 1000; // fetch up to 1000 matching messages per folder | |
1028 $this->threading = false; | |
1029 | |
1030 $a_msg_headers = array(); | |
1031 foreach ($search_set->sets as $resultset) { | |
1032 if (!$resultset->is_empty()) { | |
1033 $this->search_set = $resultset; | |
1034 $this->search_threads = $resultset instanceof rcube_result_thread; | |
1035 | |
1036 $a_headers = $this->list_search_messages($resultset->get_parameters('MAILBOX'), 1); | |
1037 $a_msg_headers = array_merge($a_msg_headers, $a_headers); | |
1038 unset($a_headers); | |
1039 } | |
1040 } | |
1041 | |
1042 // sort headers | |
1043 if (!empty($a_msg_headers)) { | |
1044 $a_msg_headers = rcube_imap_generic::sortHeaders($a_msg_headers, $sort_field, $this->sort_order); | |
1045 } | |
1046 | |
1047 // store (sorted) message index | |
1048 $search_set->set_message_index($a_msg_headers, $sort_field, $this->sort_order); | |
1049 | |
1050 // only return the requested part of the set | |
1051 $a_msg_headers = array_slice(array_values($a_msg_headers), $from, $slice_length); | |
1052 } | |
1053 else { | |
1054 if ($this->sort_order != $search_set->get_parameters('ORDER')) { | |
1055 $search_set->revert(); | |
1056 } | |
1057 | |
1058 // slice resultset first... | |
1059 $fetch = array(); | |
1060 foreach (array_slice($search_set->get(), $from, $slice_length) as $msg_id) { | |
1061 list($uid, $folder) = explode('-', $msg_id, 2); | |
1062 $fetch[$folder][] = $uid; | |
1063 } | |
1064 | |
1065 // ... and fetch the requested set of headers | |
1066 $a_msg_headers = array(); | |
1067 foreach ($fetch as $folder => $a_index) { | |
1068 $a_msg_headers = array_merge($a_msg_headers, array_values($this->fetch_headers($folder, $a_index))); | |
1069 } | |
1070 } | |
1071 | |
1072 if ($slice) { | |
1073 $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice); | |
1074 } | |
1075 | |
1076 // restore members | |
1077 $this->sort_field = $sort_field; | |
1078 $this->page_size = $page_size; | |
1079 $this->search_set = $search_set; | |
1080 | |
1081 return $a_msg_headers; | |
1082 } | |
1083 | |
1084 // use saved messages from searching | |
1085 if ($this->threading) { | |
1086 return $this->list_search_thread_messages($folder, $page, $slice); | |
1087 } | |
1088 | |
1089 // search set is threaded, we need a new one | |
1090 if ($this->search_threads) { | |
1091 $this->search('', $this->search_string, $this->search_charset, $this->sort_field); | |
1092 } | |
1093 | |
1094 $index = clone $this->search_set; | |
1095 $from = ($page-1) * $this->page_size; | |
1096 $to = $from + $this->page_size; | |
1097 | |
1098 // return empty array if no messages found | |
1099 if ($index->is_empty()) { | |
1100 return array(); | |
1101 } | |
1102 | |
1103 // quickest method (default sorting) | |
1104 if (!$this->search_sort_field && !$this->sort_field) { | |
1105 $got_index = true; | |
1106 } | |
1107 // sorted messages, so we can first slice array and then fetch only wanted headers | |
1108 else if ($this->search_sorted) { // SORT searching result | |
1109 $got_index = true; | |
1110 // reset search set if sorting field has been changed | |
1111 if ($this->sort_field && $this->search_sort_field != $this->sort_field) { | |
1112 $this->search('', $this->search_string, $this->search_charset, $this->sort_field); | |
1113 | |
1114 $index = clone $this->search_set; | |
1115 | |
1116 // return empty array if no messages found | |
1117 if ($index->is_empty()) { | |
1118 return array(); | |
1119 } | |
1120 } | |
1121 } | |
1122 | |
1123 if ($got_index) { | |
1124 if ($this->sort_order != $index->get_parameters('ORDER')) { | |
1125 $index->revert(); | |
1126 } | |
1127 | |
1128 // get messages uids for one page | |
1129 $index->slice($from, $to-$from); | |
1130 | |
1131 if ($slice) { | |
1132 $index->slice(-$slice, $slice); | |
1133 } | |
1134 | |
1135 // fetch headers | |
1136 $a_index = $index->get(); | |
1137 $a_msg_headers = $this->fetch_headers($folder, $a_index); | |
1138 | |
1139 return array_values($a_msg_headers); | |
1140 } | |
1141 | |
1142 // SEARCH result, need sorting | |
1143 $cnt = $index->count(); | |
1144 | |
1145 // 300: experimantal value for best result | |
1146 if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) { | |
1147 // use memory less expensive (and quick) method for big result set | |
1148 $index = clone $this->index('', $this->sort_field, $this->sort_order); | |
1149 // get messages uids for one page... | |
1150 $index->slice($from, min($cnt-$from, $this->page_size)); | |
1151 | |
1152 if ($slice) { | |
1153 $index->slice(-$slice, $slice); | |
1154 } | |
1155 | |
1156 // ...and fetch headers | |
1157 $a_index = $index->get(); | |
1158 $a_msg_headers = $this->fetch_headers($folder, $a_index); | |
1159 | |
1160 return array_values($a_msg_headers); | |
1161 } | |
1162 else { | |
1163 // for small result set we can fetch all messages headers | |
1164 $a_index = $index->get(); | |
1165 $a_msg_headers = $this->fetch_headers($folder, $a_index, false); | |
1166 | |
1167 // return empty array if no messages found | |
1168 if (!is_array($a_msg_headers) || empty($a_msg_headers)) { | |
1169 return array(); | |
1170 } | |
1171 | |
1172 // if not already sorted | |
1173 $a_msg_headers = rcube_imap_generic::sortHeaders( | |
1174 $a_msg_headers, $this->sort_field, $this->sort_order); | |
1175 | |
1176 // only return the requested part of the set | |
1177 $slice_length = min($this->page_size, $cnt - ($to > $cnt ? $from : $to)); | |
1178 $a_msg_headers = array_slice(array_values($a_msg_headers), $from, $slice_length); | |
1179 | |
1180 if ($slice) { | |
1181 $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice); | |
1182 } | |
1183 | |
1184 return $a_msg_headers; | |
1185 } | |
1186 } | |
1187 | |
1188 /** | |
1189 * protected method for listing a set of threaded message headers (search results) | |
1190 * | |
1191 * @param string $folder Folder name | |
1192 * @param int $page Current page to list | |
1193 * @param int $slice Number of slice items to extract from result array | |
1194 * | |
1195 * @return array Indexed array with message header objects | |
1196 * @see rcube_imap::list_search_messages() | |
1197 */ | |
1198 protected function list_search_thread_messages($folder, $page, $slice=0) | |
1199 { | |
1200 // update search_set if previous data was fetched with disabled threading | |
1201 if (!$this->search_threads) { | |
1202 if ($this->search_set->is_empty()) { | |
1203 return array(); | |
1204 } | |
1205 $this->search('', $this->search_string, $this->search_charset, $this->sort_field); | |
1206 } | |
1207 | |
1208 return $this->fetch_thread_headers($folder, clone $this->search_set, $page, $slice); | |
1209 } | |
1210 | |
1211 /** | |
1212 * Fetches messages headers (by UID) | |
1213 * | |
1214 * @param string $folder Folder name | |
1215 * @param array $msgs Message UIDs | |
1216 * @param bool $sort Enables result sorting by $msgs | |
1217 * @param bool $force Disables cache use | |
1218 * | |
1219 * @return array Messages headers indexed by UID | |
1220 */ | |
1221 function fetch_headers($folder, $msgs, $sort = true, $force = false) | |
1222 { | |
1223 if (empty($msgs)) { | |
1224 return array(); | |
1225 } | |
1226 | |
1227 if (!$force && ($mcache = $this->get_mcache_engine())) { | |
1228 $headers = $mcache->get_messages($folder, $msgs); | |
1229 } | |
1230 else if (!$this->check_connection()) { | |
1231 return array(); | |
1232 } | |
1233 else { | |
1234 // fetch reqested headers from server | |
1235 $headers = $this->conn->fetchHeaders( | |
1236 $folder, $msgs, true, false, $this->get_fetch_headers()); | |
1237 } | |
1238 | |
1239 if (empty($headers)) { | |
1240 return array(); | |
1241 } | |
1242 | |
1243 foreach ($headers as $h) { | |
1244 $h->folder = $folder; | |
1245 $a_msg_headers[$h->uid] = $h; | |
1246 } | |
1247 | |
1248 if ($sort) { | |
1249 // use this class for message sorting | |
1250 $sorter = new rcube_message_header_sorter(); | |
1251 $sorter->set_index($msgs); | |
1252 $sorter->sort_headers($a_msg_headers); | |
1253 } | |
1254 | |
1255 return $a_msg_headers; | |
1256 } | |
1257 | |
1258 /** | |
1259 * Returns current status of a folder (compared to the last time use) | |
1260 * | |
1261 * We compare the maximum UID to determine the number of | |
1262 * new messages because the RECENT flag is not reliable. | |
1263 * | |
1264 * @param string $folder Folder name | |
1265 * @param array $diff Difference data | |
1266 * | |
1267 * @return int Folder status | |
1268 */ | |
1269 public function folder_status($folder = null, &$diff = array()) | |
1270 { | |
1271 if (!strlen($folder)) { | |
1272 $folder = $this->folder; | |
1273 } | |
1274 $old = $this->get_folder_stats($folder); | |
1275 | |
1276 // refresh message count -> will update | |
1277 $this->countmessages($folder, 'ALL', true, true, true); | |
1278 | |
1279 $result = 0; | |
1280 | |
1281 if (empty($old)) { | |
1282 return $result; | |
1283 } | |
1284 | |
1285 $new = $this->get_folder_stats($folder); | |
1286 | |
1287 // got new messages | |
1288 if ($new['maxuid'] > $old['maxuid']) { | |
1289 $result += 1; | |
1290 // get new message UIDs range, that can be used for example | |
1291 // to get the data of these messages | |
1292 $diff['new'] = ($old['maxuid'] + 1 < $new['maxuid'] ? ($old['maxuid']+1).':' : '') . $new['maxuid']; | |
1293 } | |
1294 // some messages has been deleted | |
1295 if ($new['cnt'] < $old['cnt']) { | |
1296 $result += 2; | |
1297 } | |
1298 | |
1299 // @TODO: optional checking for messages flags changes (?) | |
1300 // @TODO: UIDVALIDITY checking | |
1301 | |
1302 return $result; | |
1303 } | |
1304 | |
1305 /** | |
1306 * Stores folder statistic data in session | |
1307 * @TODO: move to separate DB table (cache?) | |
1308 * | |
1309 * @param string $folder Folder name | |
1310 * @param string $name Data name | |
1311 * @param mixed $data Data value | |
1312 */ | |
1313 protected function set_folder_stats($folder, $name, $data) | |
1314 { | |
1315 $_SESSION['folders'][$folder][$name] = $data; | |
1316 } | |
1317 | |
1318 /** | |
1319 * Gets folder statistic data | |
1320 * | |
1321 * @param string $folder Folder name | |
1322 * | |
1323 * @return array Stats data | |
1324 */ | |
1325 protected function get_folder_stats($folder) | |
1326 { | |
1327 if ($_SESSION['folders'][$folder]) { | |
1328 return (array) $_SESSION['folders'][$folder]; | |
1329 } | |
1330 | |
1331 return array(); | |
1332 } | |
1333 | |
1334 /** | |
1335 * Return sorted list of message UIDs | |
1336 * | |
1337 * @param string $folder Folder to get index from | |
1338 * @param string $sort_field Sort column | |
1339 * @param string $sort_order Sort order [ASC, DESC] | |
1340 * @param bool $no_threads Get not threaded index | |
1341 * @param bool $no_search Get index not limited to search result (optionally) | |
1342 * | |
1343 * @return rcube_result_index|rcube_result_thread List of messages (UIDs) | |
1344 */ | |
1345 public function index($folder = '', $sort_field = NULL, $sort_order = NULL, | |
1346 $no_threads = false, $no_search = false | |
1347 ) { | |
1348 if (!$no_threads && $this->threading) { | |
1349 return $this->thread_index($folder, $sort_field, $sort_order); | |
1350 } | |
1351 | |
1352 $this->set_sort_order($sort_field, $sort_order); | |
1353 | |
1354 if (!strlen($folder)) { | |
1355 $folder = $this->folder; | |
1356 } | |
1357 | |
1358 // we have a saved search result, get index from there | |
1359 if ($this->search_string) { | |
1360 if ($this->search_set->is_empty()) { | |
1361 return new rcube_result_index($folder, '* SORT'); | |
1362 } | |
1363 | |
1364 if ($this->search_set instanceof rcube_result_multifolder) { | |
1365 $index = $this->search_set; | |
1366 $index->folder = $folder; | |
1367 // TODO: handle changed sorting | |
1368 } | |
1369 // search result is an index with the same sorting? | |
1370 else if (($this->search_set instanceof rcube_result_index) | |
1371 && ((!$this->sort_field && !$this->search_sorted) || | |
1372 ($this->search_sorted && $this->search_sort_field == $this->sort_field)) | |
1373 ) { | |
1374 $index = $this->search_set; | |
1375 } | |
1376 // $no_search is enabled when we are not interested in | |
1377 // fetching index for search result, e.g. to sort | |
1378 // threaded search result we can use full mailbox index. | |
1379 // This makes possible to use index from cache | |
1380 else if (!$no_search) { | |
1381 if (!$this->sort_field) { | |
1382 // No sorting needed, just build index from the search result | |
1383 // @TODO: do we need to sort by UID here? | |
1384 $search = $this->search_set->get_compressed(); | |
1385 $index = new rcube_result_index($folder, '* ESEARCH ALL ' . $search); | |
1386 } | |
1387 else { | |
1388 $index = $this->index_direct($folder, $this->sort_field, $this->sort_order, $this->search_set); | |
1389 } | |
1390 } | |
1391 | |
1392 if (isset($index)) { | |
1393 if ($this->sort_order != $index->get_parameters('ORDER')) { | |
1394 $index->revert(); | |
1395 } | |
1396 | |
1397 return $index; | |
1398 } | |
1399 } | |
1400 | |
1401 // check local cache | |
1402 if ($mcache = $this->get_mcache_engine()) { | |
1403 return $mcache->get_index($folder, $this->sort_field, $this->sort_order); | |
1404 } | |
1405 | |
1406 // fetch from IMAP server | |
1407 return $this->index_direct($folder, $this->sort_field, $this->sort_order); | |
1408 } | |
1409 | |
1410 /** | |
1411 * Return sorted list of message UIDs ignoring current search settings. | |
1412 * Doesn't uses cache by default. | |
1413 * | |
1414 * @param string $folder Folder to get index from | |
1415 * @param string $sort_field Sort column | |
1416 * @param string $sort_order Sort order [ASC, DESC] | |
1417 * @param rcube_result_* $search Optional messages set to limit the result | |
1418 * | |
1419 * @return rcube_result_index Sorted list of message UIDs | |
1420 */ | |
1421 public function index_direct($folder, $sort_field = null, $sort_order = null, $search = null) | |
1422 { | |
1423 if (!empty($search)) { | |
1424 $search = $search->get_compressed(); | |
1425 } | |
1426 | |
1427 // use message index sort as default sorting | |
1428 if (!$sort_field) { | |
1429 // use search result from count() if possible | |
1430 if (empty($search) && $this->options['skip_deleted'] | |
1431 && !empty($this->icache['undeleted_idx']) | |
1432 && $this->icache['undeleted_idx']->get_parameters('ALL') !== null | |
1433 && $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder | |
1434 ) { | |
1435 $index = $this->icache['undeleted_idx']; | |
1436 } | |
1437 else if (!$this->check_connection()) { | |
1438 return new rcube_result_index(); | |
1439 } | |
1440 else { | |
1441 $query = $this->options['skip_deleted'] ? 'UNDELETED' : ''; | |
1442 if ($search) { | |
1443 $query = trim($query . ' UID ' . $search); | |
1444 } | |
1445 | |
1446 $index = $this->conn->search($folder, $query, true); | |
1447 } | |
1448 } | |
1449 else if (!$this->check_connection()) { | |
1450 return new rcube_result_index(); | |
1451 } | |
1452 // fetch complete message index | |
1453 else { | |
1454 if ($this->get_capability('SORT')) { | |
1455 $query = $this->options['skip_deleted'] ? 'UNDELETED' : ''; | |
1456 if ($search) { | |
1457 $query = trim($query . ' UID ' . $search); | |
1458 } | |
1459 | |
1460 $index = $this->conn->sort($folder, $sort_field, $query, true); | |
1461 } | |
1462 | |
1463 if (empty($index) || $index->is_error()) { | |
1464 $index = $this->conn->index($folder, $search ? $search : "1:*", | |
1465 $sort_field, $this->options['skip_deleted'], | |
1466 $search ? true : false, true); | |
1467 } | |
1468 } | |
1469 | |
1470 if ($sort_order != $index->get_parameters('ORDER')) { | |
1471 $index->revert(); | |
1472 } | |
1473 | |
1474 return $index; | |
1475 } | |
1476 | |
1477 /** | |
1478 * Return index of threaded message UIDs | |
1479 * | |
1480 * @param string $folder Folder to get index from | |
1481 * @param string $sort_field Sort column | |
1482 * @param string $sort_order Sort order [ASC, DESC] | |
1483 * | |
1484 * @return rcube_result_thread Message UIDs | |
1485 */ | |
1486 public function thread_index($folder='', $sort_field=NULL, $sort_order=NULL) | |
1487 { | |
1488 if (!strlen($folder)) { | |
1489 $folder = $this->folder; | |
1490 } | |
1491 | |
1492 // we have a saved search result, get index from there | |
1493 if ($this->search_string && $this->search_threads && $folder == $this->folder) { | |
1494 $threads = $this->search_set; | |
1495 } | |
1496 else { | |
1497 // get all threads (default sort order) | |
1498 $threads = $this->threads($folder); | |
1499 } | |
1500 | |
1501 $this->set_sort_order($sort_field, $sort_order); | |
1502 $this->sort_threads($threads); | |
1503 | |
1504 return $threads; | |
1505 } | |
1506 | |
1507 /** | |
1508 * Sort threaded result, using THREAD=REFS method if available. | |
1509 * If not, use any method and re-sort the result in THREAD=REFS way. | |
1510 * | |
1511 * @param rcube_result_thread $threads Threads result set | |
1512 */ | |
1513 protected function sort_threads($threads) | |
1514 { | |
1515 if ($threads->is_empty()) { | |
1516 return; | |
1517 } | |
1518 | |
1519 // THREAD=ORDEREDSUBJECT: sorting by sent date of root message | |
1520 // THREAD=REFERENCES: sorting by sent date of root message | |
1521 // THREAD=REFS: sorting by the most recent date in each thread | |
1522 | |
1523 if ($this->threading != 'REFS' || ($this->sort_field && $this->sort_field != 'date')) { | |
1524 $sortby = $this->sort_field ? $this->sort_field : 'date'; | |
1525 $index = $this->index($this->folder, $sortby, $this->sort_order, true, true); | |
1526 | |
1527 if (!$index->is_empty()) { | |
1528 $threads->sort($index); | |
1529 } | |
1530 } | |
1531 else if ($this->sort_order != $threads->get_parameters('ORDER')) { | |
1532 $threads->revert(); | |
1533 } | |
1534 } | |
1535 | |
1536 /** | |
1537 * Invoke search request to IMAP server | |
1538 * | |
1539 * @param string $folder Folder name to search in | |
1540 * @param string $search Search criteria | |
1541 * @param string $charset Search charset | |
1542 * @param string $sort_field Header field to sort by | |
1543 * | |
1544 * @return rcube_result_index Search result object | |
1545 * @todo: Search criteria should be provided in non-IMAP format, eg. array | |
1546 */ | |
1547 public function search($folder = '', $search = 'ALL', $charset = null, $sort_field = null) | |
1548 { | |
1549 if (!$search) { | |
1550 $search = 'ALL'; | |
1551 } | |
1552 | |
1553 if ((is_array($folder) && empty($folder)) || (!is_array($folder) && !strlen($folder))) { | |
1554 $folder = $this->folder; | |
1555 } | |
1556 | |
1557 $plugin = $this->plugins->exec_hook('imap_search_before', array( | |
1558 'folder' => $folder, | |
1559 'search' => $search, | |
1560 'charset' => $charset, | |
1561 'sort_field' => $sort_field, | |
1562 'threading' => $this->threading, | |
1563 )); | |
1564 | |
1565 $folder = $plugin['folder']; | |
1566 $search = $plugin['search']; | |
1567 $charset = $plugin['charset']; | |
1568 $sort_field = $plugin['sort_field']; | |
1569 $results = $plugin['result']; | |
1570 | |
1571 // multi-folder search | |
1572 if (!$results && is_array($folder) && count($folder) > 1 && $search != 'ALL') { | |
1573 // connect IMAP to have all the required classes and settings loaded | |
1574 $this->check_connection(); | |
1575 | |
1576 // disable threading | |
1577 $this->threading = false; | |
1578 | |
1579 $searcher = new rcube_imap_search($this->options, $this->conn); | |
1580 | |
1581 // set limit to not exceed the client's request timeout | |
1582 $searcher->set_timelimit(60); | |
1583 | |
1584 // continue existing incomplete search | |
1585 if (!empty($this->search_set) && $this->search_set->incomplete && $search == $this->search_string) { | |
1586 $searcher->set_results($this->search_set); | |
1587 } | |
1588 | |
1589 // execute the search | |
1590 $results = $searcher->exec( | |
1591 $folder, | |
1592 $search, | |
1593 $charset ? $charset : $this->default_charset, | |
1594 $sort_field && $this->get_capability('SORT') ? $sort_field : null, | |
1595 $this->threading | |
1596 ); | |
1597 } | |
1598 else if (!$results) { | |
1599 $folder = is_array($folder) ? $folder[0] : $folder; | |
1600 $search = is_array($search) ? $search[$folder] : $search; | |
1601 $results = $this->search_index($folder, $search, $charset, $sort_field); | |
1602 } | |
1603 | |
1604 $sorted = $this->threading || $this->search_sorted || $plugin['search_sorted'] ? true : false; | |
1605 | |
1606 $this->set_search_set(array($search, $results, $charset, $sort_field, $sorted)); | |
1607 | |
1608 return $results; | |
1609 } | |
1610 | |
1611 /** | |
1612 * Direct (real and simple) SEARCH request (without result sorting and caching). | |
1613 * | |
1614 * @param string $mailbox Mailbox name to search in | |
1615 * @param string $str Search string | |
1616 * | |
1617 * @return rcube_result_index Search result (UIDs) | |
1618 */ | |
1619 public function search_once($folder = null, $str = 'ALL') | |
1620 { | |
1621 if (!$this->check_connection()) { | |
1622 return new rcube_result_index(); | |
1623 } | |
1624 | |
1625 if (!$str) { | |
1626 $str = 'ALL'; | |
1627 } | |
1628 | |
1629 // multi-folder search | |
1630 if (is_array($folder) && count($folder) > 1) { | |
1631 $searcher = new rcube_imap_search($this->options, $this->conn); | |
1632 $index = $searcher->exec($folder, $str, $this->default_charset); | |
1633 } | |
1634 else { | |
1635 $folder = is_array($folder) ? $folder[0] : $folder; | |
1636 if (!strlen($folder)) { | |
1637 $folder = $this->folder; | |
1638 } | |
1639 $index = $this->conn->search($folder, $str, true); | |
1640 } | |
1641 | |
1642 return $index; | |
1643 } | |
1644 | |
1645 /** | |
1646 * protected search method | |
1647 * | |
1648 * @param string $folder Folder name | |
1649 * @param string $criteria Search criteria | |
1650 * @param string $charset Charset | |
1651 * @param string $sort_field Sorting field | |
1652 * | |
1653 * @return rcube_result_index|rcube_result_thread Search results (UIDs) | |
1654 * @see rcube_imap::search() | |
1655 */ | |
1656 protected function search_index($folder, $criteria='ALL', $charset=NULL, $sort_field=NULL) | |
1657 { | |
1658 if (!$this->check_connection()) { | |
1659 if ($this->threading) { | |
1660 return new rcube_result_thread(); | |
1661 } | |
1662 else { | |
1663 return new rcube_result_index(); | |
1664 } | |
1665 } | |
1666 | |
1667 if ($this->options['skip_deleted'] && !preg_match('/UNDELETED/', $criteria)) { | |
1668 $criteria = 'UNDELETED '.$criteria; | |
1669 } | |
1670 | |
1671 // unset CHARSET if criteria string is ASCII, this way | |
1672 // SEARCH won't be re-sent after "unsupported charset" response | |
1673 if ($charset && $charset != 'US-ASCII' && is_ascii($criteria)) { | |
1674 $charset = 'US-ASCII'; | |
1675 } | |
1676 | |
1677 if ($this->threading) { | |
1678 $threads = $this->conn->thread($folder, $this->threading, $criteria, true, $charset); | |
1679 | |
1680 // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8, | |
1681 // but I've seen that Courier doesn't support UTF-8) | |
1682 if ($threads->is_error() && $charset && $charset != 'US-ASCII') { | |
1683 $threads = $this->conn->thread($folder, $this->threading, | |
1684 self::convert_criteria($criteria, $charset), true, 'US-ASCII'); | |
1685 } | |
1686 | |
1687 return $threads; | |
1688 } | |
1689 | |
1690 if ($sort_field && $this->get_capability('SORT')) { | |
1691 $charset = $charset ? $charset : $this->default_charset; | |
1692 $messages = $this->conn->sort($folder, $sort_field, $criteria, true, $charset); | |
1693 | |
1694 // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8, | |
1695 // but I've seen Courier with disabled UTF-8 support) | |
1696 if ($messages->is_error() && $charset && $charset != 'US-ASCII') { | |
1697 $messages = $this->conn->sort($folder, $sort_field, | |
1698 self::convert_criteria($criteria, $charset), true, 'US-ASCII'); | |
1699 } | |
1700 | |
1701 if (!$messages->is_error()) { | |
1702 $this->search_sorted = true; | |
1703 return $messages; | |
1704 } | |
1705 } | |
1706 | |
1707 $messages = $this->conn->search($folder, | |
1708 ($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true); | |
1709 | |
1710 // Error, try with US-ASCII (some servers may support only US-ASCII) | |
1711 if ($messages->is_error() && $charset && $charset != 'US-ASCII') { | |
1712 $messages = $this->conn->search($folder, | |
1713 self::convert_criteria($criteria, $charset), true); | |
1714 } | |
1715 | |
1716 $this->search_sorted = false; | |
1717 | |
1718 return $messages; | |
1719 } | |
1720 | |
1721 /** | |
1722 * Converts charset of search criteria string | |
1723 * | |
1724 * @param string $str Search string | |
1725 * @param string $charset Original charset | |
1726 * @param string $dest_charset Destination charset (default US-ASCII) | |
1727 * | |
1728 * @return string Search string | |
1729 */ | |
1730 public static function convert_criteria($str, $charset, $dest_charset='US-ASCII') | |
1731 { | |
1732 // convert strings to US_ASCII | |
1733 if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) { | |
1734 $last = 0; $res = ''; | |
1735 foreach ($matches[1] as $m) { | |
1736 $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n | |
1737 $string = substr($str, $string_offset - 1, $m[0]); | |
1738 $string = rcube_charset::convert($string, $charset, $dest_charset); | |
1739 | |
1740 if ($string === false || !strlen($string)) { | |
1741 continue; | |
1742 } | |
1743 | |
1744 $res .= substr($str, $last, $m[1] - $last - 1) . rcube_imap_generic::escape($string); | |
1745 $last = $m[0] + $string_offset - 1; | |
1746 } | |
1747 | |
1748 if ($last < strlen($str)) { | |
1749 $res .= substr($str, $last, strlen($str)-$last); | |
1750 } | |
1751 } | |
1752 // strings for conversion not found | |
1753 else { | |
1754 $res = $str; | |
1755 } | |
1756 | |
1757 return $res; | |
1758 } | |
1759 | |
1760 /** | |
1761 * Refresh saved search set | |
1762 * | |
1763 * @return array Current search set | |
1764 */ | |
1765 public function refresh_search() | |
1766 { | |
1767 if (!empty($this->search_string)) { | |
1768 $this->search( | |
1769 is_object($this->search_set) ? $this->search_set->get_parameters('MAILBOX') : '', | |
1770 $this->search_string, | |
1771 $this->search_charset, | |
1772 $this->search_sort_field | |
1773 ); | |
1774 } | |
1775 | |
1776 return $this->get_search_set(); | |
1777 } | |
1778 | |
1779 /** | |
1780 * Flag certain result subsets as 'incomplete'. | |
1781 * For subsequent refresh_search() calls to only refresh the updated parts. | |
1782 */ | |
1783 protected function set_search_dirty($folder) | |
1784 { | |
1785 if ($this->search_set && is_a($this->search_set, 'rcube_result_multifolder')) { | |
1786 if ($subset = $this->search_set->get_set($folder)) { | |
1787 $subset->incomplete = $this->search_set->incomplete = true; | |
1788 } | |
1789 } | |
1790 } | |
1791 | |
1792 /** | |
1793 * Return message headers object of a specific message | |
1794 * | |
1795 * @param int $id Message UID | |
1796 * @param string $folder Folder to read from | |
1797 * @param bool $force True to skip cache | |
1798 * | |
1799 * @return rcube_message_header Message headers | |
1800 */ | |
1801 public function get_message_headers($uid, $folder = null, $force = false) | |
1802 { | |
1803 // decode combined UID-folder identifier | |
1804 if (preg_match('/^\d+-.+/', $uid)) { | |
1805 list($uid, $folder) = explode('-', $uid, 2); | |
1806 } | |
1807 | |
1808 if (!strlen($folder)) { | |
1809 $folder = $this->folder; | |
1810 } | |
1811 | |
1812 // get cached headers | |
1813 if (!$force && $uid && ($mcache = $this->get_mcache_engine())) { | |
1814 $headers = $mcache->get_message($folder, $uid); | |
1815 } | |
1816 else if (!$this->check_connection()) { | |
1817 $headers = false; | |
1818 } | |
1819 else { | |
1820 $headers = $this->conn->fetchHeader( | |
1821 $folder, $uid, true, true, $this->get_fetch_headers()); | |
1822 | |
1823 if (is_object($headers)) | |
1824 $headers->folder = $folder; | |
1825 } | |
1826 | |
1827 return $headers; | |
1828 } | |
1829 | |
1830 /** | |
1831 * Fetch message headers and body structure from the IMAP server and build | |
1832 * an object structure. | |
1833 * | |
1834 * @param int $uid Message UID to fetch | |
1835 * @param string $folder Folder to read from | |
1836 * | |
1837 * @return object rcube_message_header Message data | |
1838 */ | |
1839 public function get_message($uid, $folder = null) | |
1840 { | |
1841 if (!strlen($folder)) { | |
1842 $folder = $this->folder; | |
1843 } | |
1844 | |
1845 // decode combined UID-folder identifier | |
1846 if (preg_match('/^\d+-.+/', $uid)) { | |
1847 list($uid, $folder) = explode('-', $uid, 2); | |
1848 } | |
1849 | |
1850 // Check internal cache | |
1851 if (!empty($this->icache['message'])) { | |
1852 if (($headers = $this->icache['message']) && $headers->uid == $uid) { | |
1853 return $headers; | |
1854 } | |
1855 } | |
1856 | |
1857 $headers = $this->get_message_headers($uid, $folder); | |
1858 | |
1859 // message doesn't exist? | |
1860 if (empty($headers)) { | |
1861 return null; | |
1862 } | |
1863 | |
1864 // structure might be cached | |
1865 if (!empty($headers->structure)) { | |
1866 return $headers; | |
1867 } | |
1868 | |
1869 $this->msg_uid = $uid; | |
1870 | |
1871 if (!$this->check_connection()) { | |
1872 return $headers; | |
1873 } | |
1874 | |
1875 if (empty($headers->bodystructure)) { | |
1876 $headers->bodystructure = $this->conn->getStructure($folder, $uid, true); | |
1877 } | |
1878 | |
1879 $structure = $headers->bodystructure; | |
1880 | |
1881 if (empty($structure)) { | |
1882 return $headers; | |
1883 } | |
1884 | |
1885 // set message charset from message headers | |
1886 if ($headers->charset) { | |
1887 $this->struct_charset = $headers->charset; | |
1888 } | |
1889 else { | |
1890 $this->struct_charset = $this->structure_charset($structure); | |
1891 } | |
1892 | |
1893 $headers->ctype = @strtolower($headers->ctype); | |
1894 | |
1895 // Here we can recognize malformed BODYSTRUCTURE and | |
1896 // 1. [@TODO] parse the message in other way to create our own message structure | |
1897 // 2. or just show the raw message body. | |
1898 // Example of structure for malformed MIME message: | |
1899 // ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL) | |
1900 if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain' | |
1901 && strtolower($structure[0].'/'.$structure[1]) == 'text/plain' | |
1902 ) { | |
1903 // A special known case "Content-type: text" (#1488968) | |
1904 if ($headers->ctype == 'text') { | |
1905 $structure[1] = 'plain'; | |
1906 $headers->ctype = 'text/plain'; | |
1907 } | |
1908 // we can handle single-part messages, by simple fix in structure (#1486898) | |
1909 else if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) { | |
1910 $structure[0] = $m[1]; | |
1911 $structure[1] = $m[2]; | |
1912 } | |
1913 else { | |
1914 // Try to parse the message using rcube_mime_decode. | |
1915 // We need a better solution, it parses message | |
1916 // in memory, which wouldn't work for very big messages, | |
1917 // (it uses up to 10x more memory than the message size) | |
1918 // it's also buggy and not actively developed | |
1919 if ($headers->size && rcube_utils::mem_check($headers->size * 10)) { | |
1920 $raw_msg = $this->get_raw_body($uid); | |
1921 $struct = rcube_mime::parse_message($raw_msg); | |
1922 } | |
1923 else { | |
1924 return $headers; | |
1925 } | |
1926 } | |
1927 } | |
1928 | |
1929 if (empty($struct)) { | |
1930 $struct = $this->structure_part($structure, 0, '', $headers); | |
1931 } | |
1932 | |
1933 // some workarounds on simple messages... | |
1934 if (empty($struct->parts)) { | |
1935 // ...don't trust given content-type | |
1936 if (!empty($headers->ctype)) { | |
1937 $struct->mime_id = '1'; | |
1938 $struct->mimetype = strtolower($headers->ctype); | |
1939 list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype); | |
1940 } | |
1941 | |
1942 // ...and charset (there's a case described in #1488968 where invalid content-type | |
1943 // results in invalid charset in BODYSTRUCTURE) | |
1944 if (!empty($headers->charset) && $headers->charset != $struct->ctype_parameters['charset']) { | |
1945 $struct->charset = $headers->charset; | |
1946 $struct->ctype_parameters['charset'] = $headers->charset; | |
1947 } | |
1948 } | |
1949 | |
1950 $headers->structure = $struct; | |
1951 | |
1952 return $this->icache['message'] = $headers; | |
1953 } | |
1954 | |
1955 /** | |
1956 * Build message part object | |
1957 * | |
1958 * @param array $part | |
1959 * @param int $count | |
1960 * @param string $parent | |
1961 */ | |
1962 protected function structure_part($part, $count = 0, $parent = '', $mime_headers = null) | |
1963 { | |
1964 $struct = new rcube_message_part; | |
1965 $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count"; | |
1966 | |
1967 // multipart | |
1968 if (is_array($part[0])) { | |
1969 $struct->ctype_primary = 'multipart'; | |
1970 | |
1971 /* RFC3501: BODYSTRUCTURE fields of multipart part | |
1972 part1 array | |
1973 part2 array | |
1974 part3 array | |
1975 .... | |
1976 1. subtype | |
1977 2. parameters (optional) | |
1978 3. description (optional) | |
1979 4. language (optional) | |
1980 5. location (optional) | |
1981 */ | |
1982 | |
1983 // find first non-array entry | |
1984 for ($i=1; $i<count($part); $i++) { | |
1985 if (!is_array($part[$i])) { | |
1986 $struct->ctype_secondary = strtolower($part[$i]); | |
1987 | |
1988 // read content type parameters | |
1989 if (is_array($part[$i+1])) { | |
1990 $struct->ctype_parameters = array(); | |
1991 for ($j=0; $j<count($part[$i+1]); $j+=2) { | |
1992 $param = strtolower($part[$i+1][$j]); | |
1993 $struct->ctype_parameters[$param] = $part[$i+1][$j+1]; | |
1994 } | |
1995 } | |
1996 | |
1997 break; | |
1998 } | |
1999 } | |
2000 | |
2001 $struct->mimetype = 'multipart/'.$struct->ctype_secondary; | |
2002 | |
2003 // build parts list for headers pre-fetching | |
2004 for ($i=0; $i<count($part); $i++) { | |
2005 if (!is_array($part[$i])) { | |
2006 break; | |
2007 } | |
2008 // fetch message headers if message/rfc822 | |
2009 // or named part (could contain Content-Location header) | |
2010 if (!is_array($part[$i][0])) { | |
2011 $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1; | |
2012 if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') { | |
2013 $mime_part_headers[] = $tmp_part_id; | |
2014 } | |
2015 else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) { | |
2016 $mime_part_headers[] = $tmp_part_id; | |
2017 } | |
2018 } | |
2019 } | |
2020 | |
2021 // pre-fetch headers of all parts (in one command for better performance) | |
2022 // @TODO: we could do this before _structure_part() call, to fetch | |
2023 // headers for parts on all levels | |
2024 if ($mime_part_headers) { | |
2025 $mime_part_headers = $this->conn->fetchMIMEHeaders($this->folder, | |
2026 $this->msg_uid, $mime_part_headers); | |
2027 } | |
2028 | |
2029 $struct->parts = array(); | |
2030 for ($i=0, $count=0; $i<count($part); $i++) { | |
2031 if (!is_array($part[$i])) { | |
2032 break; | |
2033 } | |
2034 $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1; | |
2035 $struct->parts[] = $this->structure_part($part[$i], ++$count, $struct->mime_id, | |
2036 $mime_part_headers[$tmp_part_id]); | |
2037 } | |
2038 | |
2039 return $struct; | |
2040 } | |
2041 | |
2042 /* RFC3501: BODYSTRUCTURE fields of non-multipart part | |
2043 0. type | |
2044 1. subtype | |
2045 2. parameters | |
2046 3. id | |
2047 4. description | |
2048 5. encoding | |
2049 6. size | |
2050 -- text | |
2051 7. lines | |
2052 -- message/rfc822 | |
2053 7. envelope structure | |
2054 8. body structure | |
2055 9. lines | |
2056 -- | |
2057 x. md5 (optional) | |
2058 x. disposition (optional) | |
2059 x. language (optional) | |
2060 x. location (optional) | |
2061 */ | |
2062 | |
2063 // regular part | |
2064 $struct->ctype_primary = strtolower($part[0]); | |
2065 $struct->ctype_secondary = strtolower($part[1]); | |
2066 $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary; | |
2067 | |
2068 // read content type parameters | |
2069 if (is_array($part[2])) { | |
2070 $struct->ctype_parameters = array(); | |
2071 for ($i=0; $i<count($part[2]); $i+=2) { | |
2072 $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1]; | |
2073 } | |
2074 | |
2075 if (isset($struct->ctype_parameters['charset'])) { | |
2076 $struct->charset = $struct->ctype_parameters['charset']; | |
2077 } | |
2078 } | |
2079 | |
2080 // #1487700: workaround for lack of charset in malformed structure | |
2081 if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) { | |
2082 $struct->charset = $mime_headers->charset; | |
2083 } | |
2084 | |
2085 // read content encoding | |
2086 if (!empty($part[5])) { | |
2087 $struct->encoding = strtolower($part[5]); | |
2088 $struct->headers['content-transfer-encoding'] = $struct->encoding; | |
2089 } | |
2090 | |
2091 // get part size | |
2092 if (!empty($part[6])) { | |
2093 $struct->size = intval($part[6]); | |
2094 } | |
2095 | |
2096 // read part disposition | |
2097 $di = 8; | |
2098 if ($struct->ctype_primary == 'text') { | |
2099 $di += 1; | |
2100 } | |
2101 else if ($struct->mimetype == 'message/rfc822') { | |
2102 $di += 3; | |
2103 } | |
2104 | |
2105 if (is_array($part[$di]) && count($part[$di]) == 2) { | |
2106 $struct->disposition = strtolower($part[$di][0]); | |
2107 if ($struct->disposition && $struct->disposition !== 'inline' && $struct->disposition !== 'attachment') { | |
2108 // RFC2183, Section 2.8 - unrecognized type should be treated as "attachment" | |
2109 $struct->disposition = 'attachment'; | |
2110 } | |
2111 if (is_array($part[$di][1])) { | |
2112 for ($n=0; $n<count($part[$di][1]); $n+=2) { | |
2113 $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1]; | |
2114 } | |
2115 } | |
2116 } | |
2117 | |
2118 // get message/rfc822's child-parts | |
2119 if (is_array($part[8]) && $di != 8) { | |
2120 $struct->parts = array(); | |
2121 for ($i=0, $count=0; $i<count($part[8]); $i++) { | |
2122 if (!is_array($part[8][$i])) { | |
2123 break; | |
2124 } | |
2125 $struct->parts[] = $this->structure_part($part[8][$i], ++$count, $struct->mime_id); | |
2126 } | |
2127 } | |
2128 | |
2129 // get part ID | |
2130 if (!empty($part[3])) { | |
2131 $struct->content_id = $part[3]; | |
2132 $struct->headers['content-id'] = $part[3]; | |
2133 | |
2134 if (empty($struct->disposition)) { | |
2135 $struct->disposition = 'inline'; | |
2136 } | |
2137 } | |
2138 | |
2139 // fetch message headers if message/rfc822 or named part (could contain Content-Location header) | |
2140 if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) { | |
2141 if (empty($mime_headers)) { | |
2142 $mime_headers = $this->conn->fetchPartHeader( | |
2143 $this->folder, $this->msg_uid, true, $struct->mime_id); | |
2144 } | |
2145 | |
2146 if (is_string($mime_headers)) { | |
2147 $struct->headers = rcube_mime::parse_headers($mime_headers) + $struct->headers; | |
2148 } | |
2149 else if (is_object($mime_headers)) { | |
2150 $struct->headers = get_object_vars($mime_headers) + $struct->headers; | |
2151 } | |
2152 | |
2153 // get real content-type of message/rfc822 | |
2154 if ($struct->mimetype == 'message/rfc822') { | |
2155 // single-part | |
2156 if (!is_array($part[8][0])) { | |
2157 $struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]); | |
2158 } | |
2159 // multi-part | |
2160 else { | |
2161 for ($n=0; $n<count($part[8]); $n++) { | |
2162 if (!is_array($part[8][$n])) { | |
2163 break; | |
2164 } | |
2165 } | |
2166 $struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]); | |
2167 } | |
2168 } | |
2169 | |
2170 if ($struct->ctype_primary == 'message' && empty($struct->parts)) { | |
2171 if (is_array($part[8]) && $di != 8) { | |
2172 $struct->parts[] = $this->structure_part($part[8], ++$count, $struct->mime_id); | |
2173 } | |
2174 } | |
2175 } | |
2176 | |
2177 // normalize filename property | |
2178 $this->set_part_filename($struct, $mime_headers); | |
2179 | |
2180 return $struct; | |
2181 } | |
2182 | |
2183 /** | |
2184 * Set attachment filename from message part structure | |
2185 * | |
2186 * @param rcube_message_part $part Part object | |
2187 * @param string $headers Part's raw headers | |
2188 */ | |
2189 protected function set_part_filename(&$part, $headers = null) | |
2190 { | |
2191 if (!empty($part->d_parameters['filename'])) { | |
2192 $filename_mime = $part->d_parameters['filename']; | |
2193 } | |
2194 else if (!empty($part->d_parameters['filename*'])) { | |
2195 $filename_encoded = $part->d_parameters['filename*']; | |
2196 } | |
2197 else if (!empty($part->ctype_parameters['name*'])) { | |
2198 $filename_encoded = $part->ctype_parameters['name*']; | |
2199 } | |
2200 // RFC2231 value continuations | |
2201 // TODO: this should be rewrited to support RFC2231 4.1 combinations | |
2202 else if (!empty($part->d_parameters['filename*0'])) { | |
2203 $i = 0; | |
2204 while (isset($part->d_parameters['filename*'.$i])) { | |
2205 $filename_mime .= $part->d_parameters['filename*'.$i]; | |
2206 $i++; | |
2207 } | |
2208 // some servers (eg. dovecot-1.x) have no support for parameter value continuations | |
2209 // we must fetch and parse headers "manually" | |
2210 if ($i<2) { | |
2211 if (!$headers) { | |
2212 $headers = $this->conn->fetchPartHeader( | |
2213 $this->folder, $this->msg_uid, true, $part->mime_id); | |
2214 } | |
2215 $filename_mime = ''; | |
2216 $i = 0; | |
2217 while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) { | |
2218 $filename_mime .= $matches[1]; | |
2219 $i++; | |
2220 } | |
2221 } | |
2222 } | |
2223 else if (!empty($part->d_parameters['filename*0*'])) { | |
2224 $i = 0; | |
2225 while (isset($part->d_parameters['filename*'.$i.'*'])) { | |
2226 $filename_encoded .= $part->d_parameters['filename*'.$i.'*']; | |
2227 $i++; | |
2228 } | |
2229 if ($i<2) { | |
2230 if (!$headers) { | |
2231 $headers = $this->conn->fetchPartHeader( | |
2232 $this->folder, $this->msg_uid, true, $part->mime_id); | |
2233 } | |
2234 $filename_encoded = ''; | |
2235 $i = 0; $matches = array(); | |
2236 while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) { | |
2237 $filename_encoded .= $matches[1]; | |
2238 $i++; | |
2239 } | |
2240 } | |
2241 } | |
2242 else if (!empty($part->ctype_parameters['name*0'])) { | |
2243 $i = 0; | |
2244 while (isset($part->ctype_parameters['name*'.$i])) { | |
2245 $filename_mime .= $part->ctype_parameters['name*'.$i]; | |
2246 $i++; | |
2247 } | |
2248 if ($i<2) { | |
2249 if (!$headers) { | |
2250 $headers = $this->conn->fetchPartHeader( | |
2251 $this->folder, $this->msg_uid, true, $part->mime_id); | |
2252 } | |
2253 $filename_mime = ''; | |
2254 $i = 0; $matches = array(); | |
2255 while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) { | |
2256 $filename_mime .= $matches[1]; | |
2257 $i++; | |
2258 } | |
2259 } | |
2260 } | |
2261 else if (!empty($part->ctype_parameters['name*0*'])) { | |
2262 $i = 0; | |
2263 while (isset($part->ctype_parameters['name*'.$i.'*'])) { | |
2264 $filename_encoded .= $part->ctype_parameters['name*'.$i.'*']; | |
2265 $i++; | |
2266 } | |
2267 if ($i<2) { | |
2268 if (!$headers) { | |
2269 $headers = $this->conn->fetchPartHeader( | |
2270 $this->folder, $this->msg_uid, true, $part->mime_id); | |
2271 } | |
2272 $filename_encoded = ''; | |
2273 $i = 0; $matches = array(); | |
2274 while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) { | |
2275 $filename_encoded .= $matches[1]; | |
2276 $i++; | |
2277 } | |
2278 } | |
2279 } | |
2280 // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird) | |
2281 else if (!empty($part->ctype_parameters['name'])) { | |
2282 $filename_mime = $part->ctype_parameters['name']; | |
2283 } | |
2284 // Content-Disposition | |
2285 else if (!empty($part->headers['content-description'])) { | |
2286 $filename_mime = $part->headers['content-description']; | |
2287 } | |
2288 else { | |
2289 return; | |
2290 } | |
2291 | |
2292 // decode filename | |
2293 if (!empty($filename_mime)) { | |
2294 if (!empty($part->charset)) { | |
2295 $charset = $part->charset; | |
2296 } | |
2297 else if (!empty($this->struct_charset)) { | |
2298 $charset = $this->struct_charset; | |
2299 } | |
2300 else { | |
2301 $charset = rcube_charset::detect($filename_mime, $this->default_charset); | |
2302 } | |
2303 | |
2304 $part->filename = rcube_mime::decode_mime_string($filename_mime, $charset); | |
2305 } | |
2306 else if (!empty($filename_encoded)) { | |
2307 // decode filename according to RFC 2231, Section 4 | |
2308 if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) { | |
2309 $filename_charset = $fmatches[1]; | |
2310 $filename_encoded = $fmatches[2]; | |
2311 } | |
2312 | |
2313 $part->filename = rcube_charset::convert(urldecode($filename_encoded), $filename_charset); | |
2314 } | |
2315 } | |
2316 | |
2317 /** | |
2318 * Get charset name from message structure (first part) | |
2319 * | |
2320 * @param array $structure Message structure | |
2321 * | |
2322 * @return string Charset name | |
2323 */ | |
2324 protected function structure_charset($structure) | |
2325 { | |
2326 while (is_array($structure)) { | |
2327 if (is_array($structure[2]) && $structure[2][0] == 'charset') { | |
2328 return $structure[2][1]; | |
2329 } | |
2330 $structure = $structure[0]; | |
2331 } | |
2332 } | |
2333 | |
2334 | |
2335 /** | |
2336 * Fetch message body of a specific message from the server | |
2337 * | |
2338 * @param int Message UID | |
2339 * @param string Part number | |
2340 * @param rcube_message_part Part object created by get_structure() | |
2341 * @param mixed True to print part, resource to write part contents in | |
2342 * @param resource File pointer to save the message part | |
2343 * @param boolean Disables charset conversion | |
2344 * @param int Only read this number of bytes | |
2345 * @param boolean Enables formatting of text/* parts bodies | |
2346 * | |
2347 * @return string Message/part body if not printed | |
2348 */ | |
2349 public function get_message_part($uid, $part = 1, $o_part = null, $print = null, $fp = null, | |
2350 $skip_charset_conv = false, $max_bytes = 0, $formatted = true) | |
2351 { | |
2352 if (!$this->check_connection()) { | |
2353 return null; | |
2354 } | |
2355 | |
2356 // get part data if not provided | |
2357 if (!is_object($o_part)) { | |
2358 $structure = $this->conn->getStructure($this->folder, $uid, true); | |
2359 $part_data = rcube_imap_generic::getStructurePartData($structure, $part); | |
2360 | |
2361 $o_part = new rcube_message_part; | |
2362 $o_part->ctype_primary = $part_data['type']; | |
2363 $o_part->encoding = $part_data['encoding']; | |
2364 $o_part->charset = $part_data['charset']; | |
2365 $o_part->size = $part_data['size']; | |
2366 } | |
2367 | |
2368 if ($o_part && $o_part->size) { | |
2369 $formatted = $formatted && $o_part->ctype_primary == 'text'; | |
2370 $body = $this->conn->handlePartBody($this->folder, $uid, true, | |
2371 $part ? $part : 'TEXT', $o_part->encoding, $print, $fp, $formatted, $max_bytes); | |
2372 } | |
2373 | |
2374 if ($fp || $print) { | |
2375 return true; | |
2376 } | |
2377 | |
2378 // convert charset (if text or message part) | |
2379 if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) { | |
2380 // Remove NULL characters if any (#1486189) | |
2381 if ($formatted && strpos($body, "\x00") !== false) { | |
2382 $body = str_replace("\x00", '', $body); | |
2383 } | |
2384 | |
2385 if (!$skip_charset_conv) { | |
2386 if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') { | |
2387 // try to extract charset information from HTML meta tag (#1488125) | |
2388 if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m)) { | |
2389 $o_part->charset = strtoupper($m[1]); | |
2390 } | |
2391 else { | |
2392 $o_part->charset = $this->default_charset; | |
2393 } | |
2394 } | |
2395 $body = rcube_charset::convert($body, $o_part->charset); | |
2396 } | |
2397 } | |
2398 | |
2399 return $body; | |
2400 } | |
2401 | |
2402 /** | |
2403 * Returns the whole message source as string (or saves to a file) | |
2404 * | |
2405 * @param int $uid Message UID | |
2406 * @param resource $fp File pointer to save the message | |
2407 * @param string $part Optional message part ID | |
2408 * | |
2409 * @return string Message source string | |
2410 */ | |
2411 public function get_raw_body($uid, $fp=null, $part = null) | |
2412 { | |
2413 if (!$this->check_connection()) { | |
2414 return null; | |
2415 } | |
2416 | |
2417 return $this->conn->handlePartBody($this->folder, $uid, | |
2418 true, $part, null, false, $fp); | |
2419 } | |
2420 | |
2421 /** | |
2422 * Returns the message headers as string | |
2423 * | |
2424 * @param int $uid Message UID | |
2425 * @param string $part Optional message part ID | |
2426 * | |
2427 * @return string Message headers string | |
2428 */ | |
2429 public function get_raw_headers($uid, $part = null) | |
2430 { | |
2431 if (!$this->check_connection()) { | |
2432 return null; | |
2433 } | |
2434 | |
2435 return $this->conn->fetchPartHeader($this->folder, $uid, true, $part); | |
2436 } | |
2437 | |
2438 /** | |
2439 * Sends the whole message source to stdout | |
2440 * | |
2441 * @param int $uid Message UID | |
2442 * @param bool $formatted Enables line-ending formatting | |
2443 */ | |
2444 public function print_raw_body($uid, $formatted = true) | |
2445 { | |
2446 if (!$this->check_connection()) { | |
2447 return; | |
2448 } | |
2449 | |
2450 $this->conn->handlePartBody($this->folder, $uid, true, null, null, true, null, $formatted); | |
2451 } | |
2452 | |
2453 /** | |
2454 * Set message flag to one or several messages | |
2455 * | |
2456 * @param mixed $uids Message UIDs as array or comma-separated string, or '*' | |
2457 * @param string $flag Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT | |
2458 * @param string $folder Folder name | |
2459 * @param boolean $skip_cache True to skip message cache clean up | |
2460 * | |
2461 * @return boolean Operation status | |
2462 */ | |
2463 public function set_flag($uids, $flag, $folder=null, $skip_cache=false) | |
2464 { | |
2465 if (!strlen($folder)) { | |
2466 $folder = $this->folder; | |
2467 } | |
2468 | |
2469 if (!$this->check_connection()) { | |
2470 return false; | |
2471 } | |
2472 | |
2473 $flag = strtoupper($flag); | |
2474 list($uids, $all_mode) = $this->parse_uids($uids); | |
2475 | |
2476 if (strpos($flag, 'UN') === 0) { | |
2477 $result = $this->conn->unflag($folder, $uids, substr($flag, 2)); | |
2478 } | |
2479 else { | |
2480 $result = $this->conn->flag($folder, $uids, $flag); | |
2481 } | |
2482 | |
2483 if ($result && !$skip_cache) { | |
2484 // reload message headers if cached | |
2485 // update flags instead removing from cache | |
2486 if ($mcache = $this->get_mcache_engine()) { | |
2487 $status = strpos($flag, 'UN') !== 0; | |
2488 $mflag = preg_replace('/^UN/', '', $flag); | |
2489 $mcache->change_flag($folder, $all_mode ? null : explode(',', $uids), | |
2490 $mflag, $status); | |
2491 } | |
2492 | |
2493 // clear cached counters | |
2494 if ($flag == 'SEEN' || $flag == 'UNSEEN') { | |
2495 $this->clear_messagecount($folder, array('SEEN', 'UNSEEN')); | |
2496 } | |
2497 else if ($flag == 'DELETED' || $flag == 'UNDELETED') { | |
2498 $this->clear_messagecount($folder, array('ALL', 'THREADS')); | |
2499 if ($this->options['skip_deleted']) { | |
2500 // remove cached messages | |
2501 $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids)); | |
2502 } | |
2503 } | |
2504 | |
2505 $this->set_search_dirty($folder); | |
2506 } | |
2507 | |
2508 return $result; | |
2509 } | |
2510 | |
2511 /** | |
2512 * Append a mail message (source) to a specific folder | |
2513 * | |
2514 * @param string $folder Target folder | |
2515 * @param string|array $message The message source string or filename | |
2516 * or array (of strings and file pointers) | |
2517 * @param string $headers Headers string if $message contains only the body | |
2518 * @param boolean $is_file True if $message is a filename | |
2519 * @param array $flags Message flags | |
2520 * @param mixed $date Message internal date | |
2521 * @param bool $binary Enables BINARY append | |
2522 * | |
2523 * @return int|bool Appended message UID or True on success, False on error | |
2524 */ | |
2525 public function save_message($folder, &$message, $headers='', $is_file=false, $flags = array(), $date = null, $binary = false) | |
2526 { | |
2527 if (!strlen($folder)) { | |
2528 $folder = $this->folder; | |
2529 } | |
2530 | |
2531 if (!$this->check_connection()) { | |
2532 return false; | |
2533 } | |
2534 | |
2535 // make sure folder exists | |
2536 if (!$this->folder_exists($folder)) { | |
2537 return false; | |
2538 } | |
2539 | |
2540 $date = $this->date_format($date); | |
2541 | |
2542 if ($is_file) { | |
2543 $saved = $this->conn->appendFromFile($folder, $message, $headers, $flags, $date, $binary); | |
2544 } | |
2545 else { | |
2546 $saved = $this->conn->append($folder, $message, $flags, $date, $binary); | |
2547 } | |
2548 | |
2549 if ($saved) { | |
2550 // increase messagecount of the target folder | |
2551 $this->set_messagecount($folder, 'ALL', 1); | |
2552 | |
2553 $this->plugins->exec_hook('message_saved', array( | |
2554 'folder' => $folder, | |
2555 'message' => $message, | |
2556 'headers' => $headers, | |
2557 'is_file' => $is_file, | |
2558 'flags' => $flags, | |
2559 'date' => $date, | |
2560 'binary' => $binary, | |
2561 'result' => $saved, | |
2562 )); | |
2563 } | |
2564 | |
2565 return $saved; | |
2566 } | |
2567 | |
2568 /** | |
2569 * Move a message from one folder to another | |
2570 * | |
2571 * @param mixed $uids Message UIDs as array or comma-separated string, or '*' | |
2572 * @param string $to_mbox Target folder | |
2573 * @param string $from_mbox Source folder | |
2574 * | |
2575 * @return boolean True on success, False on error | |
2576 */ | |
2577 public function move_message($uids, $to_mbox, $from_mbox='') | |
2578 { | |
2579 if (!strlen($from_mbox)) { | |
2580 $from_mbox = $this->folder; | |
2581 } | |
2582 | |
2583 if ($to_mbox === $from_mbox) { | |
2584 return false; | |
2585 } | |
2586 | |
2587 list($uids, $all_mode) = $this->parse_uids($uids); | |
2588 | |
2589 // exit if no message uids are specified | |
2590 if (empty($uids)) { | |
2591 return false; | |
2592 } | |
2593 | |
2594 if (!$this->check_connection()) { | |
2595 return false; | |
2596 } | |
2597 | |
2598 $config = rcube::get_instance()->config; | |
2599 $to_trash = $to_mbox == $config->get('trash_mbox'); | |
2600 | |
2601 // flag messages as read before moving them | |
2602 if ($to_trash && $config->get('read_when_deleted')) { | |
2603 // don't flush cache (4th argument) | |
2604 $this->set_flag($uids, 'SEEN', $from_mbox, true); | |
2605 } | |
2606 | |
2607 // move messages | |
2608 $moved = $this->conn->move($uids, $from_mbox, $to_mbox); | |
2609 | |
2610 // when moving to Trash we make sure the folder exists | |
2611 // as it's uncommon scenario we do this when MOVE fails, not before | |
2612 if (!$moved && $to_trash && $this->get_response_code() == rcube_storage::TRYCREATE) { | |
2613 if ($this->create_folder($to_mbox, true, 'trash')) { | |
2614 $moved = $this->conn->move($uids, $from_mbox, $to_mbox); | |
2615 } | |
2616 } | |
2617 | |
2618 if ($moved) { | |
2619 $this->clear_messagecount($from_mbox); | |
2620 $this->clear_messagecount($to_mbox); | |
2621 | |
2622 $this->set_search_dirty($from_mbox); | |
2623 $this->set_search_dirty($to_mbox); | |
2624 } | |
2625 // moving failed | |
2626 else if ($to_trash && $config->get('delete_always', false)) { | |
2627 $moved = $this->delete_message($uids, $from_mbox); | |
2628 } | |
2629 | |
2630 if ($moved) { | |
2631 // unset threads internal cache | |
2632 unset($this->icache['threads']); | |
2633 | |
2634 // remove message ids from search set | |
2635 if ($this->search_set && $from_mbox == $this->folder) { | |
2636 // threads are too complicated to just remove messages from set | |
2637 if ($this->search_threads || $all_mode) { | |
2638 $this->refresh_search(); | |
2639 } | |
2640 else if (!$this->search_set->incomplete) { | |
2641 $this->search_set->filter(explode(',', $uids), $this->folder); | |
2642 } | |
2643 } | |
2644 | |
2645 // remove cached messages | |
2646 // @TODO: do cache update instead of clearing it | |
2647 $this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids)); | |
2648 } | |
2649 | |
2650 return $moved; | |
2651 } | |
2652 | |
2653 /** | |
2654 * Copy a message from one folder to another | |
2655 * | |
2656 * @param mixed $uids Message UIDs as array or comma-separated string, or '*' | |
2657 * @param string $to_mbox Target folder | |
2658 * @param string $from_mbox Source folder | |
2659 * | |
2660 * @return boolean True on success, False on error | |
2661 */ | |
2662 public function copy_message($uids, $to_mbox, $from_mbox='') | |
2663 { | |
2664 if (!strlen($from_mbox)) { | |
2665 $from_mbox = $this->folder; | |
2666 } | |
2667 | |
2668 list($uids, $all_mode) = $this->parse_uids($uids); | |
2669 | |
2670 // exit if no message uids are specified | |
2671 if (empty($uids)) { | |
2672 return false; | |
2673 } | |
2674 | |
2675 if (!$this->check_connection()) { | |
2676 return false; | |
2677 } | |
2678 | |
2679 // copy messages | |
2680 $copied = $this->conn->copy($uids, $from_mbox, $to_mbox); | |
2681 | |
2682 if ($copied) { | |
2683 $this->clear_messagecount($to_mbox); | |
2684 } | |
2685 | |
2686 return $copied; | |
2687 } | |
2688 | |
2689 /** | |
2690 * Mark messages as deleted and expunge them | |
2691 * | |
2692 * @param mixed $uids Message UIDs as array or comma-separated string, or '*' | |
2693 * @param string $folder Source folder | |
2694 * | |
2695 * @return boolean True on success, False on error | |
2696 */ | |
2697 public function delete_message($uids, $folder='') | |
2698 { | |
2699 if (!strlen($folder)) { | |
2700 $folder = $this->folder; | |
2701 } | |
2702 | |
2703 list($uids, $all_mode) = $this->parse_uids($uids); | |
2704 | |
2705 // exit if no message uids are specified | |
2706 if (empty($uids)) { | |
2707 return false; | |
2708 } | |
2709 | |
2710 if (!$this->check_connection()) { | |
2711 return false; | |
2712 } | |
2713 | |
2714 $deleted = $this->conn->flag($folder, $uids, 'DELETED'); | |
2715 | |
2716 if ($deleted) { | |
2717 // send expunge command in order to have the deleted message | |
2718 // really deleted from the folder | |
2719 $this->expunge_message($uids, $folder, false); | |
2720 $this->clear_messagecount($folder); | |
2721 | |
2722 // unset threads internal cache | |
2723 unset($this->icache['threads']); | |
2724 | |
2725 $this->set_search_dirty($folder); | |
2726 | |
2727 // remove message ids from search set | |
2728 if ($this->search_set && $folder == $this->folder) { | |
2729 // threads are too complicated to just remove messages from set | |
2730 if ($this->search_threads || $all_mode) { | |
2731 $this->refresh_search(); | |
2732 } | |
2733 else if (!$this->search_set->incomplete) { | |
2734 $this->search_set->filter(explode(',', $uids)); | |
2735 } | |
2736 } | |
2737 | |
2738 // remove cached messages | |
2739 $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids)); | |
2740 } | |
2741 | |
2742 return $deleted; | |
2743 } | |
2744 | |
2745 /** | |
2746 * Send IMAP expunge command and clear cache | |
2747 * | |
2748 * @param mixed $uids Message UIDs as array or comma-separated string, or '*' | |
2749 * @param string $folder Folder name | |
2750 * @param boolean $clear_cache False if cache should not be cleared | |
2751 * | |
2752 * @return boolean True on success, False on failure | |
2753 */ | |
2754 public function expunge_message($uids, $folder = null, $clear_cache = true) | |
2755 { | |
2756 if ($uids && $this->get_capability('UIDPLUS')) { | |
2757 list($uids, $all_mode) = $this->parse_uids($uids); | |
2758 } | |
2759 else { | |
2760 $uids = null; | |
2761 } | |
2762 | |
2763 if (!strlen($folder)) { | |
2764 $folder = $this->folder; | |
2765 } | |
2766 | |
2767 if (!$this->check_connection()) { | |
2768 return false; | |
2769 } | |
2770 | |
2771 // force folder selection and check if folder is writeable | |
2772 // to prevent a situation when CLOSE is executed on closed | |
2773 // or EXPUNGE on read-only folder | |
2774 $result = $this->conn->select($folder); | |
2775 if (!$result) { | |
2776 return false; | |
2777 } | |
2778 | |
2779 if (!$this->conn->data['READ-WRITE']) { | |
2780 $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Folder is read-only"); | |
2781 return false; | |
2782 } | |
2783 | |
2784 // CLOSE(+SELECT) should be faster than EXPUNGE | |
2785 if (empty($uids) || $all_mode) { | |
2786 $result = $this->conn->close(); | |
2787 } | |
2788 else { | |
2789 $result = $this->conn->expunge($folder, $uids); | |
2790 } | |
2791 | |
2792 if ($result && $clear_cache) { | |
2793 $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids)); | |
2794 $this->clear_messagecount($folder); | |
2795 } | |
2796 | |
2797 return $result; | |
2798 } | |
2799 | |
2800 | |
2801 /* -------------------------------- | |
2802 * folder management | |
2803 * --------------------------------*/ | |
2804 | |
2805 /** | |
2806 * Public method for listing subscribed folders. | |
2807 * | |
2808 * @param string $root Optional root folder | |
2809 * @param string $name Optional name pattern | |
2810 * @param string $filter Optional filter | |
2811 * @param string $rights Optional ACL requirements | |
2812 * @param bool $skip_sort Enable to return unsorted list (for better performance) | |
2813 * | |
2814 * @return array List of folders | |
2815 */ | |
2816 public function list_folders_subscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false) | |
2817 { | |
2818 $cache_key = $root.':'.$name; | |
2819 if (!empty($filter)) { | |
2820 $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter)); | |
2821 } | |
2822 $cache_key .= ':'.$rights; | |
2823 $cache_key = 'mailboxes.'.md5($cache_key); | |
2824 | |
2825 // get cached folder list | |
2826 $a_mboxes = $this->get_cache($cache_key); | |
2827 if (is_array($a_mboxes)) { | |
2828 return $a_mboxes; | |
2829 } | |
2830 | |
2831 // Give plugins a chance to provide a list of folders | |
2832 $data = $this->plugins->exec_hook('storage_folders', | |
2833 array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB')); | |
2834 | |
2835 if (isset($data['folders'])) { | |
2836 $a_mboxes = $data['folders']; | |
2837 } | |
2838 else { | |
2839 $a_mboxes = $this->list_folders_subscribed_direct($root, $name); | |
2840 } | |
2841 | |
2842 if (!is_array($a_mboxes)) { | |
2843 return array(); | |
2844 } | |
2845 | |
2846 // filter folders list according to rights requirements | |
2847 if ($rights && $this->get_capability('ACL')) { | |
2848 $a_mboxes = $this->filter_rights($a_mboxes, $rights); | |
2849 } | |
2850 | |
2851 // INBOX should always be available | |
2852 if (in_array_nocase($root . $name, array('*', '%', 'INBOX', 'INBOX*')) | |
2853 && (!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes) | |
2854 ) { | |
2855 array_unshift($a_mboxes, 'INBOX'); | |
2856 } | |
2857 | |
2858 // sort folders (always sort for cache) | |
2859 if (!$skip_sort || $this->cache) { | |
2860 $a_mboxes = $this->sort_folder_list($a_mboxes); | |
2861 } | |
2862 | |
2863 // write folders list to cache | |
2864 $this->update_cache($cache_key, $a_mboxes); | |
2865 | |
2866 return $a_mboxes; | |
2867 } | |
2868 | |
2869 /** | |
2870 * Method for direct folders listing (LSUB) | |
2871 * | |
2872 * @param string $root Optional root folder | |
2873 * @param string $name Optional name pattern | |
2874 * | |
2875 * @return array List of subscribed folders | |
2876 * @see rcube_imap::list_folders_subscribed() | |
2877 */ | |
2878 public function list_folders_subscribed_direct($root='', $name='*') | |
2879 { | |
2880 if (!$this->check_connection()) { | |
2881 return null; | |
2882 } | |
2883 | |
2884 $config = rcube::get_instance()->config; | |
2885 | |
2886 // Server supports LIST-EXTENDED, we can use selection options | |
2887 // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED | |
2888 $list_extended = !$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED'); | |
2889 if ($list_extended) { | |
2890 // This will also set folder options, LSUB doesn't do that | |
2891 $result = $this->conn->listMailboxes($root, $name, | |
2892 NULL, array('SUBSCRIBED')); | |
2893 } | |
2894 else { | |
2895 // retrieve list of folders from IMAP server using LSUB | |
2896 $result = $this->conn->listSubscribed($root, $name); | |
2897 } | |
2898 | |
2899 if (!is_array($result)) { | |
2900 return array(); | |
2901 } | |
2902 | |
2903 // #1486796: some server configurations doesn't return folders in all namespaces | |
2904 if ($root == '' && $name == '*' && $config->get('imap_force_ns')) { | |
2905 $this->list_folders_update($result, ($list_extended ? 'ext-' : '') . 'subscribed'); | |
2906 } | |
2907 | |
2908 // Remove hidden folders | |
2909 if ($config->get('imap_skip_hidden_folders')) { | |
2910 $result = array_filter($result, function($v) { return $v[0] != '.'; }); | |
2911 } | |
2912 | |
2913 if ($list_extended) { | |
2914 // unsubscribe non-existent folders, remove from the list | |
2915 if ($name == '*' && !empty($this->conn->data['LIST'])) { | |
2916 foreach ($result as $idx => $folder) { | |
2917 if (($opts = $this->conn->data['LIST'][$folder]) | |
2918 && in_array_nocase('\\NonExistent', $opts) | |
2919 ) { | |
2920 $this->conn->unsubscribe($folder); | |
2921 unset($result[$idx]); | |
2922 } | |
2923 } | |
2924 } | |
2925 } | |
2926 else { | |
2927 // unsubscribe non-existent folders, remove them from the list | |
2928 if (!empty($result) && $name == '*') { | |
2929 $existing = $this->list_folders($root, $name); | |
2930 $nonexisting = array_diff($result, $existing); | |
2931 $result = array_diff($result, $nonexisting); | |
2932 | |
2933 foreach ($nonexisting as $folder) { | |
2934 $this->conn->unsubscribe($folder); | |
2935 } | |
2936 } | |
2937 } | |
2938 | |
2939 return $result; | |
2940 } | |
2941 | |
2942 /** | |
2943 * Get a list of all folders available on the server | |
2944 * | |
2945 * @param string $root IMAP root dir | |
2946 * @param string $name Optional name pattern | |
2947 * @param mixed $filter Optional filter | |
2948 * @param string $rights Optional ACL requirements | |
2949 * @param bool $skip_sort Enable to return unsorted list (for better performance) | |
2950 * | |
2951 * @return array Indexed array with folder names | |
2952 */ | |
2953 public function list_folders($root='', $name='*', $filter=null, $rights=null, $skip_sort=false) | |
2954 { | |
2955 $cache_key = $root.':'.$name; | |
2956 if (!empty($filter)) { | |
2957 $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter)); | |
2958 } | |
2959 $cache_key .= ':'.$rights; | |
2960 $cache_key = 'mailboxes.list.'.md5($cache_key); | |
2961 | |
2962 // get cached folder list | |
2963 $a_mboxes = $this->get_cache($cache_key); | |
2964 if (is_array($a_mboxes)) { | |
2965 return $a_mboxes; | |
2966 } | |
2967 | |
2968 // Give plugins a chance to provide a list of folders | |
2969 $data = $this->plugins->exec_hook('storage_folders', | |
2970 array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST')); | |
2971 | |
2972 if (isset($data['folders'])) { | |
2973 $a_mboxes = $data['folders']; | |
2974 } | |
2975 else { | |
2976 // retrieve list of folders from IMAP server | |
2977 $a_mboxes = $this->list_folders_direct($root, $name); | |
2978 } | |
2979 | |
2980 if (!is_array($a_mboxes)) { | |
2981 $a_mboxes = array(); | |
2982 } | |
2983 | |
2984 // INBOX should always be available | |
2985 if (in_array_nocase($root . $name, array('*', '%', 'INBOX', 'INBOX*')) | |
2986 && (!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes) | |
2987 ) { | |
2988 array_unshift($a_mboxes, 'INBOX'); | |
2989 } | |
2990 | |
2991 // cache folder attributes | |
2992 if ($root == '' && $name == '*' && empty($filter) && !empty($this->conn->data)) { | |
2993 $this->update_cache('mailboxes.attributes', $this->conn->data['LIST']); | |
2994 } | |
2995 | |
2996 // filter folders list according to rights requirements | |
2997 if ($rights && $this->get_capability('ACL')) { | |
2998 $a_mboxes = $this->filter_rights($a_mboxes, $rights); | |
2999 } | |
3000 | |
3001 // filter folders and sort them | |
3002 if (!$skip_sort) { | |
3003 $a_mboxes = $this->sort_folder_list($a_mboxes); | |
3004 } | |
3005 | |
3006 // write folders list to cache | |
3007 $this->update_cache($cache_key, $a_mboxes); | |
3008 | |
3009 return $a_mboxes; | |
3010 } | |
3011 | |
3012 /** | |
3013 * Method for direct folders listing (LIST) | |
3014 * | |
3015 * @param string $root Optional root folder | |
3016 * @param string $name Optional name pattern | |
3017 * | |
3018 * @return array List of folders | |
3019 * @see rcube_imap::list_folders() | |
3020 */ | |
3021 public function list_folders_direct($root='', $name='*') | |
3022 { | |
3023 if (!$this->check_connection()) { | |
3024 return null; | |
3025 } | |
3026 | |
3027 $result = $this->conn->listMailboxes($root, $name); | |
3028 | |
3029 if (!is_array($result)) { | |
3030 return array(); | |
3031 } | |
3032 | |
3033 $config = rcube::get_instance()->config; | |
3034 | |
3035 // #1486796: some server configurations doesn't return folders in all namespaces | |
3036 if ($root == '' && $name == '*' && $config->get('imap_force_ns')) { | |
3037 $this->list_folders_update($result); | |
3038 } | |
3039 | |
3040 // Remove hidden folders | |
3041 if ($config->get('imap_skip_hidden_folders')) { | |
3042 $result = array_filter($result, function($v) { return $v[0] != '.'; }); | |
3043 } | |
3044 | |
3045 return $result; | |
3046 } | |
3047 | |
3048 /** | |
3049 * Fix folders list by adding folders from other namespaces. | |
3050 * Needed on some servers eg. Courier IMAP | |
3051 * | |
3052 * @param array $result Reference to folders list | |
3053 * @param string $type Listing type (ext-subscribed, subscribed or all) | |
3054 */ | |
3055 protected function list_folders_update(&$result, $type = null) | |
3056 { | |
3057 $namespace = $this->get_namespace(); | |
3058 $search = array(); | |
3059 | |
3060 // build list of namespace prefixes | |
3061 foreach ((array)$namespace as $ns) { | |
3062 if (is_array($ns)) { | |
3063 foreach ($ns as $ns_data) { | |
3064 if (strlen($ns_data[0])) { | |
3065 $search[] = $ns_data[0]; | |
3066 } | |
3067 } | |
3068 } | |
3069 } | |
3070 | |
3071 if (!empty($search)) { | |
3072 // go through all folders detecting namespace usage | |
3073 foreach ($result as $folder) { | |
3074 foreach ($search as $idx => $prefix) { | |
3075 if (strpos($folder, $prefix) === 0) { | |
3076 unset($search[$idx]); | |
3077 } | |
3078 } | |
3079 if (empty($search)) { | |
3080 break; | |
3081 } | |
3082 } | |
3083 | |
3084 // get folders in hidden namespaces and add to the result | |
3085 foreach ($search as $prefix) { | |
3086 if ($type == 'ext-subscribed') { | |
3087 $list = $this->conn->listMailboxes('', $prefix . '*', null, array('SUBSCRIBED')); | |
3088 } | |
3089 else if ($type == 'subscribed') { | |
3090 $list = $this->conn->listSubscribed('', $prefix . '*'); | |
3091 } | |
3092 else { | |
3093 $list = $this->conn->listMailboxes('', $prefix . '*'); | |
3094 } | |
3095 | |
3096 if (!empty($list)) { | |
3097 $result = array_merge($result, $list); | |
3098 } | |
3099 } | |
3100 } | |
3101 } | |
3102 | |
3103 /** | |
3104 * Filter the given list of folders according to access rights | |
3105 * | |
3106 * For performance reasons we assume user has full rights | |
3107 * on all personal folders. | |
3108 */ | |
3109 protected function filter_rights($a_folders, $rights) | |
3110 { | |
3111 $regex = '/('.$rights.')/'; | |
3112 | |
3113 foreach ($a_folders as $idx => $folder) { | |
3114 if ($this->folder_namespace($folder) == 'personal') { | |
3115 continue; | |
3116 } | |
3117 | |
3118 $myrights = join('', (array)$this->my_rights($folder)); | |
3119 | |
3120 if ($myrights !== null && !preg_match($regex, $myrights)) { | |
3121 unset($a_folders[$idx]); | |
3122 } | |
3123 } | |
3124 | |
3125 return $a_folders; | |
3126 } | |
3127 | |
3128 /** | |
3129 * Get mailbox quota information | |
3130 * | |
3131 * @param string $folder Folder name | |
3132 * | |
3133 * @return mixed Quota info or False if not supported | |
3134 */ | |
3135 public function get_quota($folder = null) | |
3136 { | |
3137 if ($this->get_capability('QUOTA') && $this->check_connection()) { | |
3138 return $this->conn->getQuota($folder); | |
3139 } | |
3140 | |
3141 return false; | |
3142 } | |
3143 | |
3144 /** | |
3145 * Get folder size (size of all messages in a folder) | |
3146 * | |
3147 * @param string $folder Folder name | |
3148 * | |
3149 * @return int Folder size in bytes, False on error | |
3150 */ | |
3151 public function folder_size($folder) | |
3152 { | |
3153 if (!strlen($folder)) { | |
3154 return false; | |
3155 } | |
3156 | |
3157 if (!$this->check_connection()) { | |
3158 return 0; | |
3159 } | |
3160 | |
3161 // On Cyrus we can use special folder annotation, which should be much faster | |
3162 if ($this->get_vendor() == 'cyrus') { | |
3163 $idx = '/shared/vendor/cmu/cyrus-imapd/size'; | |
3164 $result = $this->get_metadata($folder, $idx, array(), true); | |
3165 | |
3166 if (!empty($result) && is_numeric($result[$folder][$idx])) { | |
3167 return $result[$folder][$idx]; | |
3168 } | |
3169 } | |
3170 | |
3171 // @TODO: could we try to use QUOTA here? | |
3172 $result = $this->conn->fetchHeaderIndex($folder, '1:*', 'SIZE', false); | |
3173 | |
3174 if (is_array($result)) { | |
3175 $result = array_sum($result); | |
3176 } | |
3177 | |
3178 return $result; | |
3179 } | |
3180 | |
3181 /** | |
3182 * Subscribe to a specific folder(s) | |
3183 * | |
3184 * @param array $folders Folder name(s) | |
3185 * | |
3186 * @return boolean True on success | |
3187 */ | |
3188 public function subscribe($folders) | |
3189 { | |
3190 // let this common function do the main work | |
3191 return $this->change_subscription($folders, 'subscribe'); | |
3192 } | |
3193 | |
3194 /** | |
3195 * Unsubscribe folder(s) | |
3196 * | |
3197 * @param array $a_mboxes Folder name(s) | |
3198 * | |
3199 * @return boolean True on success | |
3200 */ | |
3201 public function unsubscribe($folders) | |
3202 { | |
3203 // let this common function do the main work | |
3204 return $this->change_subscription($folders, 'unsubscribe'); | |
3205 } | |
3206 | |
3207 /** | |
3208 * Create a new folder on the server and register it in local cache | |
3209 * | |
3210 * @param string $folder New folder name | |
3211 * @param boolean $subscribe True if the new folder should be subscribed | |
3212 * @param string $type Optional folder type (junk, trash, drafts, sent, archive) | |
3213 * | |
3214 * @return boolean True on success | |
3215 */ | |
3216 public function create_folder($folder, $subscribe = false, $type = null) | |
3217 { | |
3218 if (!$this->check_connection()) { | |
3219 return false; | |
3220 } | |
3221 | |
3222 $result = $this->conn->createFolder($folder, $type ? array("\\" . ucfirst($type)) : null); | |
3223 | |
3224 // try to subscribe it | |
3225 if ($result) { | |
3226 // clear cache | |
3227 $this->clear_cache('mailboxes', true); | |
3228 | |
3229 if ($subscribe) { | |
3230 $this->subscribe($folder); | |
3231 } | |
3232 } | |
3233 | |
3234 return $result; | |
3235 } | |
3236 | |
3237 /** | |
3238 * Set a new name to an existing folder | |
3239 * | |
3240 * @param string $folder Folder to rename | |
3241 * @param string $new_name New folder name | |
3242 * | |
3243 * @return boolean True on success | |
3244 */ | |
3245 public function rename_folder($folder, $new_name) | |
3246 { | |
3247 if (!strlen($new_name)) { | |
3248 return false; | |
3249 } | |
3250 | |
3251 if (!$this->check_connection()) { | |
3252 return false; | |
3253 } | |
3254 | |
3255 $delm = $this->get_hierarchy_delimiter(); | |
3256 | |
3257 // get list of subscribed folders | |
3258 if ((strpos($folder, '%') === false) && (strpos($folder, '*') === false)) { | |
3259 $a_subscribed = $this->list_folders_subscribed('', $folder . $delm . '*'); | |
3260 $subscribed = $this->folder_exists($folder, true); | |
3261 } | |
3262 else { | |
3263 $a_subscribed = $this->list_folders_subscribed(); | |
3264 $subscribed = in_array($folder, $a_subscribed); | |
3265 } | |
3266 | |
3267 $result = $this->conn->renameFolder($folder, $new_name); | |
3268 | |
3269 if ($result) { | |
3270 // unsubscribe the old folder, subscribe the new one | |
3271 if ($subscribed) { | |
3272 $this->conn->unsubscribe($folder); | |
3273 $this->conn->subscribe($new_name); | |
3274 } | |
3275 | |
3276 // check if folder children are subscribed | |
3277 foreach ($a_subscribed as $c_subscribed) { | |
3278 if (strpos($c_subscribed, $folder.$delm) === 0) { | |
3279 $this->conn->unsubscribe($c_subscribed); | |
3280 $this->conn->subscribe(preg_replace('/^'.preg_quote($folder, '/').'/', | |
3281 $new_name, $c_subscribed)); | |
3282 | |
3283 // clear cache | |
3284 $this->clear_message_cache($c_subscribed); | |
3285 } | |
3286 } | |
3287 | |
3288 // clear cache | |
3289 $this->clear_message_cache($folder); | |
3290 $this->clear_cache('mailboxes', true); | |
3291 } | |
3292 | |
3293 return $result; | |
3294 } | |
3295 | |
3296 /** | |
3297 * Remove folder (with subfolders) from the server | |
3298 * | |
3299 * @param string $folder Folder name | |
3300 * | |
3301 * @return boolean True on success, False on failure | |
3302 */ | |
3303 function delete_folder($folder) | |
3304 { | |
3305 if (!$this->check_connection()) { | |
3306 return false; | |
3307 } | |
3308 | |
3309 $delm = $this->get_hierarchy_delimiter(); | |
3310 | |
3311 // get list of sub-folders or all folders | |
3312 // if folder name contains special characters | |
3313 $path = strspn($folder, '%*') > 0 ? ($folder . $delm) : ''; | |
3314 $sub_mboxes = $this->list_folders('', $path . '*'); | |
3315 | |
3316 // According to RFC3501 deleting a \Noselect folder | |
3317 // with subfolders may fail. To workaround this we delete | |
3318 // subfolders first (in reverse order) (#5466) | |
3319 if (!empty($sub_mboxes)) { | |
3320 foreach (array_reverse($sub_mboxes) as $mbox) { | |
3321 if (strpos($mbox, $folder . $delm) === 0) { | |
3322 if ($this->conn->deleteFolder($mbox)) { | |
3323 $this->conn->unsubscribe($mbox); | |
3324 $this->clear_message_cache($mbox); | |
3325 } | |
3326 } | |
3327 } | |
3328 } | |
3329 | |
3330 // delete the folder | |
3331 if ($result = $this->conn->deleteFolder($folder)) { | |
3332 // and unsubscribe it | |
3333 $this->conn->unsubscribe($folder); | |
3334 $this->clear_message_cache($folder); | |
3335 } | |
3336 | |
3337 $this->clear_cache('mailboxes', true); | |
3338 | |
3339 return $result; | |
3340 } | |
3341 | |
3342 /** | |
3343 * Detect special folder associations stored in storage backend | |
3344 */ | |
3345 public function get_special_folders($forced = false) | |
3346 { | |
3347 $result = parent::get_special_folders(); | |
3348 $rcube = rcube::get_instance(); | |
3349 | |
3350 // Lock SPECIAL-USE after user preferences change (#4782) | |
3351 if ($rcube->config->get('lock_special_folders')) { | |
3352 return $result; | |
3353 } | |
3354 | |
3355 if (isset($this->icache['special-use'])) { | |
3356 return array_merge($result, $this->icache['special-use']); | |
3357 } | |
3358 | |
3359 if (!$forced || !$this->get_capability('SPECIAL-USE')) { | |
3360 return $result; | |
3361 } | |
3362 | |
3363 if (!$this->check_connection()) { | |
3364 return $result; | |
3365 } | |
3366 | |
3367 $types = array_map(function($value) { return "\\" . ucfirst($value); }, rcube_storage::$folder_types); | |
3368 $special = array(); | |
3369 | |
3370 // request \Subscribed flag in LIST response as performance improvement for folder_exists() | |
3371 $folders = $this->conn->listMailboxes('', '*', array('SUBSCRIBED'), array('SPECIAL-USE')); | |
3372 | |
3373 if (!empty($folders)) { | |
3374 foreach ($folders as $folder) { | |
3375 if ($flags = $this->conn->data['LIST'][$folder]) { | |
3376 foreach ($types as $type) { | |
3377 if (in_array($type, $flags)) { | |
3378 $type = strtolower(substr($type, 1)); | |
3379 $special[$type] = $folder; | |
3380 } | |
3381 } | |
3382 } | |
3383 } | |
3384 } | |
3385 | |
3386 $this->icache['special-use'] = $special; | |
3387 unset($this->icache['special-folders']); | |
3388 | |
3389 return array_merge($result, $special); | |
3390 } | |
3391 | |
3392 /** | |
3393 * Set special folder associations stored in storage backend | |
3394 */ | |
3395 public function set_special_folders($specials) | |
3396 { | |
3397 if (!$this->get_capability('SPECIAL-USE') || !$this->get_capability('METADATA')) { | |
3398 return false; | |
3399 } | |
3400 | |
3401 if (!$this->check_connection()) { | |
3402 return false; | |
3403 } | |
3404 | |
3405 $folders = $this->get_special_folders(true); | |
3406 $old = (array) $this->icache['special-use']; | |
3407 | |
3408 foreach ($specials as $type => $folder) { | |
3409 if (in_array($type, rcube_storage::$folder_types)) { | |
3410 $old_folder = $old[$type]; | |
3411 if ($old_folder !== $folder) { | |
3412 // unset old-folder metadata | |
3413 if ($old_folder !== null) { | |
3414 $this->delete_metadata($old_folder, array('/private/specialuse')); | |
3415 } | |
3416 // set new folder metadata | |
3417 if ($folder) { | |
3418 $this->set_metadata($folder, array('/private/specialuse' => "\\" . ucfirst($type))); | |
3419 } | |
3420 } | |
3421 } | |
3422 } | |
3423 | |
3424 $this->icache['special-use'] = $specials; | |
3425 unset($this->icache['special-folders']); | |
3426 | |
3427 return true; | |
3428 } | |
3429 | |
3430 /** | |
3431 * Checks if folder exists and is subscribed | |
3432 * | |
3433 * @param string $folder Folder name | |
3434 * @param boolean $subscription Enable subscription checking | |
3435 * | |
3436 * @return boolean TRUE or FALSE | |
3437 */ | |
3438 public function folder_exists($folder, $subscription = false) | |
3439 { | |
3440 if ($folder == 'INBOX') { | |
3441 return true; | |
3442 } | |
3443 | |
3444 $key = $subscription ? 'subscribed' : 'existing'; | |
3445 | |
3446 if (is_array($this->icache[$key]) && in_array($folder, $this->icache[$key])) { | |
3447 return true; | |
3448 } | |
3449 | |
3450 if (!$this->check_connection()) { | |
3451 return false; | |
3452 } | |
3453 | |
3454 if ($subscription) { | |
3455 // It's possible we already called LIST command, check LIST data | |
3456 if (!empty($this->conn->data['LIST']) && !empty($this->conn->data['LIST'][$folder]) | |
3457 && in_array_nocase('\\Subscribed', $this->conn->data['LIST'][$folder]) | |
3458 ) { | |
3459 $a_folders = array($folder); | |
3460 } | |
3461 else { | |
3462 $a_folders = $this->conn->listSubscribed('', $folder); | |
3463 } | |
3464 } | |
3465 else { | |
3466 // It's possible we already called LIST command, check LIST data | |
3467 if (!empty($this->conn->data['LIST']) && isset($this->conn->data['LIST'][$folder])) { | |
3468 $a_folders = array($folder); | |
3469 } | |
3470 else { | |
3471 $a_folders = $this->conn->listMailboxes('', $folder); | |
3472 } | |
3473 } | |
3474 | |
3475 if (is_array($a_folders) && in_array($folder, $a_folders)) { | |
3476 $this->icache[$key][] = $folder; | |
3477 return true; | |
3478 } | |
3479 | |
3480 return false; | |
3481 } | |
3482 | |
3483 /** | |
3484 * Returns the namespace where the folder is in | |
3485 * | |
3486 * @param string $folder Folder name | |
3487 * | |
3488 * @return string One of 'personal', 'other' or 'shared' | |
3489 */ | |
3490 public function folder_namespace($folder) | |
3491 { | |
3492 if ($folder == 'INBOX') { | |
3493 return 'personal'; | |
3494 } | |
3495 | |
3496 foreach ($this->namespace as $type => $namespace) { | |
3497 if (is_array($namespace)) { | |
3498 foreach ($namespace as $ns) { | |
3499 if ($len = strlen($ns[0])) { | |
3500 if (($len > 1 && $folder == substr($ns[0], 0, -1)) | |
3501 || strpos($folder, $ns[0]) === 0 | |
3502 ) { | |
3503 return $type; | |
3504 } | |
3505 } | |
3506 } | |
3507 } | |
3508 } | |
3509 | |
3510 return 'personal'; | |
3511 } | |
3512 | |
3513 /** | |
3514 * Modify folder name according to personal namespace prefix. | |
3515 * For output it removes prefix of the personal namespace if it's possible. | |
3516 * For input it adds the prefix. Use it before creating a folder in root | |
3517 * of the folders tree. | |
3518 * | |
3519 * @param string $folder Folder name | |
3520 * @param string $mode Mode name (out/in) | |
3521 * | |
3522 * @return string Folder name | |
3523 */ | |
3524 public function mod_folder($folder, $mode = 'out') | |
3525 { | |
3526 $prefix = $this->namespace['prefix_' . $mode]; // see set_env() | |
3527 | |
3528 if ($prefix === null || $prefix === '' | |
3529 || !($prefix_len = strlen($prefix)) || !strlen($folder) | |
3530 ) { | |
3531 return $folder; | |
3532 } | |
3533 | |
3534 // remove prefix for output | |
3535 if ($mode == 'out') { | |
3536 if (substr($folder, 0, $prefix_len) === $prefix) { | |
3537 return substr($folder, $prefix_len); | |
3538 } | |
3539 | |
3540 return $folder; | |
3541 } | |
3542 | |
3543 // add prefix for input (e.g. folder creation) | |
3544 return $prefix . $folder; | |
3545 } | |
3546 | |
3547 /** | |
3548 * Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors | |
3549 * | |
3550 * @param string $folder Folder name | |
3551 * @param bool $force Set to True if attributes should be refreshed | |
3552 * | |
3553 * @return array Options list | |
3554 */ | |
3555 public function folder_attributes($folder, $force=false) | |
3556 { | |
3557 // get attributes directly from LIST command | |
3558 if (!empty($this->conn->data['LIST']) && is_array($this->conn->data['LIST'][$folder])) { | |
3559 $opts = $this->conn->data['LIST'][$folder]; | |
3560 } | |
3561 // get cached folder attributes | |
3562 else if (!$force) { | |
3563 $opts = $this->get_cache('mailboxes.attributes'); | |
3564 $opts = $opts[$folder]; | |
3565 } | |
3566 | |
3567 if (!is_array($opts)) { | |
3568 if (!$this->check_connection()) { | |
3569 return array(); | |
3570 } | |
3571 | |
3572 $this->conn->listMailboxes('', $folder); | |
3573 $opts = $this->conn->data['LIST'][$folder]; | |
3574 } | |
3575 | |
3576 return is_array($opts) ? $opts : array(); | |
3577 } | |
3578 | |
3579 /** | |
3580 * Gets connection (and current folder) data: UIDVALIDITY, EXISTS, RECENT, | |
3581 * PERMANENTFLAGS, UIDNEXT, UNSEEN | |
3582 * | |
3583 * @param string $folder Folder name | |
3584 * | |
3585 * @return array Data | |
3586 */ | |
3587 public function folder_data($folder) | |
3588 { | |
3589 if (!strlen($folder)) { | |
3590 $folder = $this->folder !== null ? $this->folder : 'INBOX'; | |
3591 } | |
3592 | |
3593 if ($this->conn->selected != $folder) { | |
3594 if (!$this->check_connection()) { | |
3595 return array(); | |
3596 } | |
3597 | |
3598 if ($this->conn->select($folder)) { | |
3599 $this->folder = $folder; | |
3600 } | |
3601 else { | |
3602 return null; | |
3603 } | |
3604 } | |
3605 | |
3606 $data = $this->conn->data; | |
3607 | |
3608 // add (E)SEARCH result for ALL UNDELETED query | |
3609 if (!empty($this->icache['undeleted_idx']) | |
3610 && $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder | |
3611 ) { | |
3612 $data['UNDELETED'] = $this->icache['undeleted_idx']; | |
3613 } | |
3614 | |
3615 return $data; | |
3616 } | |
3617 | |
3618 /** | |
3619 * Returns extended information about the folder | |
3620 * | |
3621 * @param string $folder Folder name | |
3622 * | |
3623 * @return array Data | |
3624 */ | |
3625 public function folder_info($folder) | |
3626 { | |
3627 if ($this->icache['options'] && $this->icache['options']['name'] == $folder) { | |
3628 return $this->icache['options']; | |
3629 } | |
3630 | |
3631 // get cached metadata | |
3632 $cache_key = 'mailboxes.folder-info.' . $folder; | |
3633 $cached = $this->get_cache($cache_key); | |
3634 | |
3635 if (is_array($cached)) { | |
3636 return $cached; | |
3637 } | |
3638 | |
3639 $acl = $this->get_capability('ACL'); | |
3640 $namespace = $this->get_namespace(); | |
3641 $options = array(); | |
3642 | |
3643 // check if the folder is a namespace prefix | |
3644 if (!empty($namespace)) { | |
3645 $mbox = $folder . $this->delimiter; | |
3646 foreach ($namespace as $ns) { | |
3647 if (!empty($ns)) { | |
3648 foreach ($ns as $item) { | |
3649 if ($item[0] === $mbox) { | |
3650 $options['is_root'] = true; | |
3651 break 2; | |
3652 } | |
3653 } | |
3654 } | |
3655 } | |
3656 } | |
3657 // check if the folder is other user virtual-root | |
3658 if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) { | |
3659 $parts = explode($this->delimiter, $folder); | |
3660 if (count($parts) == 2) { | |
3661 $mbox = $parts[0] . $this->delimiter; | |
3662 foreach ($namespace['other'] as $item) { | |
3663 if ($item[0] === $mbox) { | |
3664 $options['is_root'] = true; | |
3665 break; | |
3666 } | |
3667 } | |
3668 } | |
3669 } | |
3670 | |
3671 $options['name'] = $folder; | |
3672 $options['attributes'] = $this->folder_attributes($folder, true); | |
3673 $options['namespace'] = $this->folder_namespace($folder); | |
3674 $options['special'] = $this->is_special_folder($folder); | |
3675 | |
3676 // Set 'noselect' flag | |
3677 if (is_array($options['attributes'])) { | |
3678 foreach ($options['attributes'] as $attrib) { | |
3679 $attrib = strtolower($attrib); | |
3680 if ($attrib == '\noselect' || $attrib == '\nonexistent') { | |
3681 $options['noselect'] = true; | |
3682 } | |
3683 } | |
3684 } | |
3685 else { | |
3686 $options['noselect'] = true; | |
3687 } | |
3688 | |
3689 // Get folder rights (MYRIGHTS) | |
3690 if ($acl && ($rights = $this->my_rights($folder))) { | |
3691 $options['rights'] = $rights; | |
3692 } | |
3693 | |
3694 // Set 'norename' flag | |
3695 if (!empty($options['rights'])) { | |
3696 $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']); | |
3697 | |
3698 if (!$options['noselect']) { | |
3699 $options['noselect'] = !in_array('r', $options['rights']); | |
3700 } | |
3701 } | |
3702 else { | |
3703 $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal'; | |
3704 } | |
3705 | |
3706 // update caches | |
3707 $this->icache['options'] = $options; | |
3708 $this->update_cache($cache_key, $options); | |
3709 | |
3710 return $options; | |
3711 } | |
3712 | |
3713 /** | |
3714 * Synchronizes messages cache. | |
3715 * | |
3716 * @param string $folder Folder name | |
3717 */ | |
3718 public function folder_sync($folder) | |
3719 { | |
3720 if ($mcache = $this->get_mcache_engine()) { | |
3721 $mcache->synchronize($folder); | |
3722 } | |
3723 } | |
3724 | |
3725 /** | |
3726 * Get message header names for rcube_imap_generic::fetchHeader(s) | |
3727 * | |
3728 * @return string Space-separated list of header names | |
3729 */ | |
3730 protected function get_fetch_headers() | |
3731 { | |
3732 if (!empty($this->options['fetch_headers'])) { | |
3733 $headers = explode(' ', $this->options['fetch_headers']); | |
3734 } | |
3735 else { | |
3736 $headers = array(); | |
3737 } | |
3738 | |
3739 if ($this->messages_caching || $this->options['all_headers']) { | |
3740 $headers = array_merge($headers, $this->all_headers); | |
3741 } | |
3742 | |
3743 return $headers; | |
3744 } | |
3745 | |
3746 | |
3747 /* ----------------------------------------- | |
3748 * ACL and METADATA/ANNOTATEMORE methods | |
3749 * ----------------------------------------*/ | |
3750 | |
3751 /** | |
3752 * Changes the ACL on the specified folder (SETACL) | |
3753 * | |
3754 * @param string $folder Folder name | |
3755 * @param string $user User name | |
3756 * @param string $acl ACL string | |
3757 * | |
3758 * @return boolean True on success, False on failure | |
3759 * @since 0.5-beta | |
3760 */ | |
3761 public function set_acl($folder, $user, $acl) | |
3762 { | |
3763 if (!$this->get_capability('ACL')) { | |
3764 return false; | |
3765 } | |
3766 | |
3767 if (!$this->check_connection()) { | |
3768 return false; | |
3769 } | |
3770 | |
3771 $this->clear_cache('mailboxes.folder-info.' . $folder); | |
3772 | |
3773 return $this->conn->setACL($folder, $user, $acl); | |
3774 } | |
3775 | |
3776 /** | |
3777 * Removes any <identifier,rights> pair for the | |
3778 * specified user from the ACL for the specified | |
3779 * folder (DELETEACL) | |
3780 * | |
3781 * @param string $folder Folder name | |
3782 * @param string $user User name | |
3783 * | |
3784 * @return boolean True on success, False on failure | |
3785 * @since 0.5-beta | |
3786 */ | |
3787 public function delete_acl($folder, $user) | |
3788 { | |
3789 if (!$this->get_capability('ACL')) { | |
3790 return false; | |
3791 } | |
3792 | |
3793 if (!$this->check_connection()) { | |
3794 return false; | |
3795 } | |
3796 | |
3797 return $this->conn->deleteACL($folder, $user); | |
3798 } | |
3799 | |
3800 /** | |
3801 * Returns the access control list for folder (GETACL) | |
3802 * | |
3803 * @param string $folder Folder name | |
3804 * | |
3805 * @return array User-rights array on success, NULL on error | |
3806 * @since 0.5-beta | |
3807 */ | |
3808 public function get_acl($folder) | |
3809 { | |
3810 if (!$this->get_capability('ACL')) { | |
3811 return null; | |
3812 } | |
3813 | |
3814 if (!$this->check_connection()) { | |
3815 return null; | |
3816 } | |
3817 | |
3818 return $this->conn->getACL($folder); | |
3819 } | |
3820 | |
3821 /** | |
3822 * Returns information about what rights can be granted to the | |
3823 * user (identifier) in the ACL for the folder (LISTRIGHTS) | |
3824 * | |
3825 * @param string $folder Folder name | |
3826 * @param string $user User name | |
3827 * | |
3828 * @return array List of user rights | |
3829 * @since 0.5-beta | |
3830 */ | |
3831 public function list_rights($folder, $user) | |
3832 { | |
3833 if (!$this->get_capability('ACL')) { | |
3834 return null; | |
3835 } | |
3836 | |
3837 if (!$this->check_connection()) { | |
3838 return null; | |
3839 } | |
3840 | |
3841 return $this->conn->listRights($folder, $user); | |
3842 } | |
3843 | |
3844 /** | |
3845 * Returns the set of rights that the current user has to | |
3846 * folder (MYRIGHTS) | |
3847 * | |
3848 * @param string $folder Folder name | |
3849 * | |
3850 * @return array MYRIGHTS response on success, NULL on error | |
3851 * @since 0.5-beta | |
3852 */ | |
3853 public function my_rights($folder) | |
3854 { | |
3855 if (!$this->get_capability('ACL')) { | |
3856 return null; | |
3857 } | |
3858 | |
3859 if (!$this->check_connection()) { | |
3860 return null; | |
3861 } | |
3862 | |
3863 return $this->conn->myRights($folder); | |
3864 } | |
3865 | |
3866 /** | |
3867 * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION) | |
3868 * | |
3869 * @param string $folder Folder name (empty for server metadata) | |
3870 * @param array $entries Entry-value array (use NULL value as NIL) | |
3871 * | |
3872 * @return boolean True on success, False on failure | |
3873 * @since 0.5-beta | |
3874 */ | |
3875 public function set_metadata($folder, $entries) | |
3876 { | |
3877 if (!$this->check_connection()) { | |
3878 return false; | |
3879 } | |
3880 | |
3881 $this->clear_cache('mailboxes.metadata.', true); | |
3882 | |
3883 if ($this->get_capability('METADATA') || | |
3884 (!strlen($folder) && $this->get_capability('METADATA-SERVER')) | |
3885 ) { | |
3886 return $this->conn->setMetadata($folder, $entries); | |
3887 } | |
3888 else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) { | |
3889 foreach ((array)$entries as $entry => $value) { | |
3890 list($ent, $attr) = $this->md2annotate($entry); | |
3891 $entries[$entry] = array($ent, $attr, $value); | |
3892 } | |
3893 return $this->conn->setAnnotation($folder, $entries); | |
3894 } | |
3895 | |
3896 return false; | |
3897 } | |
3898 | |
3899 /** | |
3900 * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION) | |
3901 * | |
3902 * @param string $folder Folder name (empty for server metadata) | |
3903 * @param array $entries Entry names array | |
3904 * | |
3905 * @return boolean True on success, False on failure | |
3906 * @since 0.5-beta | |
3907 */ | |
3908 public function delete_metadata($folder, $entries) | |
3909 { | |
3910 if (!$this->check_connection()) { | |
3911 return false; | |
3912 } | |
3913 | |
3914 $this->clear_cache('mailboxes.metadata.', true); | |
3915 | |
3916 if ($this->get_capability('METADATA') || | |
3917 (!strlen($folder) && $this->get_capability('METADATA-SERVER')) | |
3918 ) { | |
3919 return $this->conn->deleteMetadata($folder, $entries); | |
3920 } | |
3921 else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) { | |
3922 foreach ((array)$entries as $idx => $entry) { | |
3923 list($ent, $attr) = $this->md2annotate($entry); | |
3924 $entries[$idx] = array($ent, $attr, NULL); | |
3925 } | |
3926 return $this->conn->setAnnotation($folder, $entries); | |
3927 } | |
3928 | |
3929 return false; | |
3930 } | |
3931 | |
3932 /** | |
3933 * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION) | |
3934 * | |
3935 * @param string $folder Folder name (empty for server metadata) | |
3936 * @param array $entries Entries | |
3937 * @param array $options Command options (with MAXSIZE and DEPTH keys) | |
3938 * @param bool $force Disables cache use | |
3939 * | |
3940 * @return array Metadata entry-value hash array on success, NULL on error | |
3941 * @since 0.5-beta | |
3942 */ | |
3943 public function get_metadata($folder, $entries, $options = array(), $force = false) | |
3944 { | |
3945 $entries = (array) $entries; | |
3946 | |
3947 if (!$force) { | |
3948 // create cache key | |
3949 // @TODO: this is the simplest solution, but we do the same with folders list | |
3950 // maybe we should store data per-entry and merge on request | |
3951 sort($options); | |
3952 sort($entries); | |
3953 $cache_key = 'mailboxes.metadata.' . $folder; | |
3954 $cache_key .= '.' . md5(serialize($options).serialize($entries)); | |
3955 | |
3956 // get cached data | |
3957 $cached_data = $this->get_cache($cache_key); | |
3958 | |
3959 if (is_array($cached_data)) { | |
3960 return $cached_data; | |
3961 } | |
3962 } | |
3963 | |
3964 if (!$this->check_connection()) { | |
3965 return null; | |
3966 } | |
3967 | |
3968 if ($this->get_capability('METADATA') || | |
3969 (!strlen($folder) && $this->get_capability('METADATA-SERVER')) | |
3970 ) { | |
3971 $res = $this->conn->getMetadata($folder, $entries, $options); | |
3972 } | |
3973 else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) { | |
3974 $queries = array(); | |
3975 $res = array(); | |
3976 | |
3977 // Convert entry names | |
3978 foreach ($entries as $entry) { | |
3979 list($ent, $attr) = $this->md2annotate($entry); | |
3980 $queries[$attr][] = $ent; | |
3981 } | |
3982 | |
3983 // @TODO: Honor MAXSIZE and DEPTH options | |
3984 foreach ($queries as $attrib => $entry) { | |
3985 $result = $this->conn->getAnnotation($folder, $entry, $attrib); | |
3986 | |
3987 // an error, invalidate any previous getAnnotation() results | |
3988 if (!is_array($result)) { | |
3989 return null; | |
3990 } | |
3991 else { | |
3992 foreach ($result as $fldr => $data) { | |
3993 $res[$fldr] = array_merge((array) $res[$fldr], $data); | |
3994 } | |
3995 } | |
3996 } | |
3997 } | |
3998 | |
3999 if (isset($res)) { | |
4000 if (!$force) { | |
4001 $this->update_cache($cache_key, $res); | |
4002 } | |
4003 | |
4004 return $res; | |
4005 } | |
4006 } | |
4007 | |
4008 /** | |
4009 * Converts the METADATA extension entry name into the correct | |
4010 * entry-attrib names for older ANNOTATEMORE version. | |
4011 * | |
4012 * @param string $entry Entry name | |
4013 * | |
4014 * @return array Entry-attribute list, NULL if not supported (?) | |
4015 */ | |
4016 protected function md2annotate($entry) | |
4017 { | |
4018 if (substr($entry, 0, 7) == '/shared') { | |
4019 return array(substr($entry, 7), 'value.shared'); | |
4020 } | |
4021 else if (substr($entry, 0, 8) == '/private') { | |
4022 return array(substr($entry, 8), 'value.priv'); | |
4023 } | |
4024 | |
4025 // @TODO: log error | |
4026 } | |
4027 | |
4028 | |
4029 /* -------------------------------- | |
4030 * internal caching methods | |
4031 * --------------------------------*/ | |
4032 | |
4033 /** | |
4034 * Enable or disable indexes caching | |
4035 * | |
4036 * @param string $type Cache type (@see rcube::get_cache) | |
4037 */ | |
4038 public function set_caching($type) | |
4039 { | |
4040 if ($type) { | |
4041 $this->caching = $type; | |
4042 } | |
4043 else { | |
4044 if ($this->cache) { | |
4045 $this->cache->close(); | |
4046 } | |
4047 $this->cache = null; | |
4048 $this->caching = false; | |
4049 } | |
4050 } | |
4051 | |
4052 /** | |
4053 * Getter for IMAP cache object | |
4054 */ | |
4055 protected function get_cache_engine() | |
4056 { | |
4057 if ($this->caching && !$this->cache) { | |
4058 $rcube = rcube::get_instance(); | |
4059 $ttl = $rcube->config->get('imap_cache_ttl', '10d'); | |
4060 $this->cache = $rcube->get_cache('IMAP', $this->caching, $ttl); | |
4061 } | |
4062 | |
4063 return $this->cache; | |
4064 } | |
4065 | |
4066 /** | |
4067 * Returns cached value | |
4068 * | |
4069 * @param string $key Cache key | |
4070 * | |
4071 * @return mixed | |
4072 */ | |
4073 public function get_cache($key) | |
4074 { | |
4075 if ($cache = $this->get_cache_engine()) { | |
4076 return $cache->get($key); | |
4077 } | |
4078 } | |
4079 | |
4080 /** | |
4081 * Update cache | |
4082 * | |
4083 * @param string $key Cache key | |
4084 * @param mixed $data Data | |
4085 */ | |
4086 public function update_cache($key, $data) | |
4087 { | |
4088 if ($cache = $this->get_cache_engine()) { | |
4089 $cache->set($key, $data); | |
4090 } | |
4091 } | |
4092 | |
4093 /** | |
4094 * Clears the cache. | |
4095 * | |
4096 * @param string $key Cache key name or pattern | |
4097 * @param boolean $prefix_mode Enable it to clear all keys starting | |
4098 * with prefix specified in $key | |
4099 */ | |
4100 public function clear_cache($key = null, $prefix_mode = false) | |
4101 { | |
4102 if ($cache = $this->get_cache_engine()) { | |
4103 $cache->remove($key, $prefix_mode); | |
4104 } | |
4105 } | |
4106 | |
4107 | |
4108 /* -------------------------------- | |
4109 * message caching methods | |
4110 * --------------------------------*/ | |
4111 | |
4112 /** | |
4113 * Enable or disable messages caching | |
4114 * | |
4115 * @param boolean $set Flag | |
4116 * @param int $mode Cache mode | |
4117 */ | |
4118 public function set_messages_caching($set, $mode = null) | |
4119 { | |
4120 if ($set) { | |
4121 $this->messages_caching = true; | |
4122 | |
4123 if ($mode && ($cache = $this->get_mcache_engine())) { | |
4124 $cache->set_mode($mode); | |
4125 } | |
4126 } | |
4127 else { | |
4128 if ($this->mcache) { | |
4129 $this->mcache->close(); | |
4130 } | |
4131 $this->mcache = null; | |
4132 $this->messages_caching = false; | |
4133 } | |
4134 } | |
4135 | |
4136 /** | |
4137 * Getter for messages cache object | |
4138 */ | |
4139 protected function get_mcache_engine() | |
4140 { | |
4141 if ($this->messages_caching && !$this->mcache) { | |
4142 $rcube = rcube::get_instance(); | |
4143 if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) { | |
4144 $ttl = $rcube->config->get('messages_cache_ttl', '10d'); | |
4145 $threshold = $rcube->config->get('messages_cache_threshold', 50); | |
4146 $this->mcache = new rcube_imap_cache( | |
4147 $dbh, $this, $userid, $this->options['skip_deleted'], $ttl, $threshold); | |
4148 } | |
4149 } | |
4150 | |
4151 return $this->mcache; | |
4152 } | |
4153 | |
4154 /** | |
4155 * Clears the messages cache. | |
4156 * | |
4157 * @param string $folder Folder name | |
4158 * @param array $uids Optional message UIDs to remove from cache | |
4159 */ | |
4160 protected function clear_message_cache($folder = null, $uids = null) | |
4161 { | |
4162 if ($mcache = $this->get_mcache_engine()) { | |
4163 $mcache->clear($folder, $uids); | |
4164 } | |
4165 } | |
4166 | |
4167 /** | |
4168 * Delete outdated cache entries | |
4169 */ | |
4170 function cache_gc() | |
4171 { | |
4172 rcube_imap_cache::gc(); | |
4173 } | |
4174 | |
4175 | |
4176 /* -------------------------------- | |
4177 * protected methods | |
4178 * --------------------------------*/ | |
4179 | |
4180 /** | |
4181 * Validate the given input and save to local properties | |
4182 * | |
4183 * @param string $sort_field Sort column | |
4184 * @param string $sort_order Sort order | |
4185 */ | |
4186 protected function set_sort_order($sort_field, $sort_order) | |
4187 { | |
4188 if ($sort_field != null) { | |
4189 $this->sort_field = asciiwords($sort_field); | |
4190 } | |
4191 if ($sort_order != null) { | |
4192 $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC'; | |
4193 } | |
4194 } | |
4195 | |
4196 /** | |
4197 * Sort folders first by default folders and then in alphabethical order | |
4198 * | |
4199 * @param array $a_folders Folders list | |
4200 * @param bool $skip_default Skip default folders handling | |
4201 * | |
4202 * @return array Sorted list | |
4203 */ | |
4204 public function sort_folder_list($a_folders, $skip_default = false) | |
4205 { | |
4206 $specials = array_merge(array('INBOX'), array_values($this->get_special_folders())); | |
4207 $folders = array(); | |
4208 | |
4209 // convert names to UTF-8 | |
4210 foreach ($a_folders as $folder) { | |
4211 // for better performance skip encoding conversion | |
4212 // if the string does not look like UTF7-IMAP | |
4213 $folders[$folder] = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); | |
4214 } | |
4215 | |
4216 // sort folders | |
4217 // asort($folders, SORT_LOCALE_STRING) is not properly sorting case sensitive names | |
4218 uasort($folders, array($this, 'sort_folder_comparator')); | |
4219 | |
4220 $folders = array_keys($folders); | |
4221 | |
4222 if ($skip_default) { | |
4223 return $folders; | |
4224 } | |
4225 | |
4226 // force the type of folder name variable (#1485527) | |
4227 $folders = array_map('strval', $folders); | |
4228 $out = array(); | |
4229 | |
4230 // finally we must put special folders on top and rebuild the list | |
4231 // to move their subfolders where they belong... | |
4232 $specials = array_unique(array_intersect($specials, $folders)); | |
4233 $folders = array_merge($specials, array_diff($folders, $specials)); | |
4234 | |
4235 $this->sort_folder_specials(null, $folders, $specials, $out); | |
4236 | |
4237 return $out; | |
4238 } | |
4239 | |
4240 /** | |
4241 * Recursive function to put subfolders of special folders in place | |
4242 */ | |
4243 protected function sort_folder_specials($folder, &$list, &$specials, &$out) | |
4244 { | |
4245 foreach ($list as $key => $name) { | |
4246 if ($folder === null || strpos($name, $folder.$this->delimiter) === 0) { | |
4247 $out[] = $name; | |
4248 unset($list[$key]); | |
4249 | |
4250 if (!empty($specials) && ($found = array_search($name, $specials)) !== false) { | |
4251 unset($specials[$found]); | |
4252 $this->sort_folder_specials($name, $list, $specials, $out); | |
4253 } | |
4254 } | |
4255 } | |
4256 | |
4257 reset($list); | |
4258 } | |
4259 | |
4260 /** | |
4261 * Callback for uasort() that implements correct | |
4262 * locale-aware case-sensitive sorting | |
4263 */ | |
4264 protected function sort_folder_comparator($str1, $str2) | |
4265 { | |
4266 if ($this->sort_folder_collator === null) { | |
4267 $this->sort_folder_collator = false; | |
4268 | |
4269 // strcoll() does not work with UTF8 locale on Windows, | |
4270 // use Collator from the intl extension | |
4271 if (stripos(PHP_OS, 'win') === 0 && function_exists('collator_compare')) { | |
4272 $locale = $this->options['language'] ?: 'en_US'; | |
4273 $this->sort_folder_collator = collator_create($locale) ?: false; | |
4274 } | |
4275 } | |
4276 | |
4277 $path1 = explode($this->delimiter, $str1); | |
4278 $path2 = explode($this->delimiter, $str2); | |
4279 | |
4280 foreach ($path1 as $idx => $folder1) { | |
4281 $folder2 = $path2[$idx]; | |
4282 | |
4283 if ($folder1 === $folder2) { | |
4284 continue; | |
4285 } | |
4286 | |
4287 if ($this->sort_folder_collator) { | |
4288 return collator_compare($this->sort_folder_collator, $folder1, $folder2); | |
4289 } | |
4290 | |
4291 return strcoll($folder1, $folder2); | |
4292 } | |
4293 } | |
4294 | |
4295 /** | |
4296 * Find UID of the specified message sequence ID | |
4297 * | |
4298 * @param int $id Message (sequence) ID | |
4299 * @param string $folder Folder name | |
4300 * | |
4301 * @return int Message UID | |
4302 */ | |
4303 public function id2uid($id, $folder = null) | |
4304 { | |
4305 if (!strlen($folder)) { | |
4306 $folder = $this->folder; | |
4307 } | |
4308 | |
4309 if (!$this->check_connection()) { | |
4310 return null; | |
4311 } | |
4312 | |
4313 return $this->conn->ID2UID($folder, $id); | |
4314 } | |
4315 | |
4316 /** | |
4317 * Subscribe/unsubscribe a list of folders and update local cache | |
4318 */ | |
4319 protected function change_subscription($folders, $mode) | |
4320 { | |
4321 $updated = 0; | |
4322 $folders = (array) $folders; | |
4323 | |
4324 if (!empty($folders)) { | |
4325 if (!$this->check_connection()) { | |
4326 return false; | |
4327 } | |
4328 | |
4329 foreach ($folders as $folder) { | |
4330 $updated += (int) $this->conn->{$mode}($folder); | |
4331 } | |
4332 } | |
4333 | |
4334 // clear cached folders list(s) | |
4335 if ($updated) { | |
4336 $this->clear_cache('mailboxes', true); | |
4337 } | |
4338 | |
4339 return $updated == count($folders); | |
4340 } | |
4341 | |
4342 /** | |
4343 * Increde/decrese messagecount for a specific folder | |
4344 */ | |
4345 protected function set_messagecount($folder, $mode, $increment) | |
4346 { | |
4347 if (!is_numeric($increment)) { | |
4348 return false; | |
4349 } | |
4350 | |
4351 $mode = strtoupper($mode); | |
4352 $a_folder_cache = $this->get_cache('messagecount'); | |
4353 | |
4354 if (!is_array($a_folder_cache[$folder]) || !isset($a_folder_cache[$folder][$mode])) { | |
4355 return false; | |
4356 } | |
4357 | |
4358 // add incremental value to messagecount | |
4359 $a_folder_cache[$folder][$mode] += $increment; | |
4360 | |
4361 // there's something wrong, delete from cache | |
4362 if ($a_folder_cache[$folder][$mode] < 0) { | |
4363 unset($a_folder_cache[$folder][$mode]); | |
4364 } | |
4365 | |
4366 // write back to cache | |
4367 $this->update_cache('messagecount', $a_folder_cache); | |
4368 | |
4369 return true; | |
4370 } | |
4371 | |
4372 /** | |
4373 * Remove messagecount of a specific folder from cache | |
4374 */ | |
4375 protected function clear_messagecount($folder, $mode = array()) | |
4376 { | |
4377 $a_folder_cache = $this->get_cache('messagecount'); | |
4378 | |
4379 if (is_array($a_folder_cache[$folder])) { | |
4380 if (!empty($mode)) { | |
4381 foreach ((array) $mode as $key) { | |
4382 unset($a_folder_cache[$folder][$key]); | |
4383 } | |
4384 } | |
4385 else { | |
4386 unset($a_folder_cache[$folder]); | |
4387 } | |
4388 $this->update_cache('messagecount', $a_folder_cache); | |
4389 } | |
4390 } | |
4391 | |
4392 /** | |
4393 * Converts date string/object into IMAP date/time format | |
4394 */ | |
4395 protected function date_format($date) | |
4396 { | |
4397 if (empty($date)) { | |
4398 return null; | |
4399 } | |
4400 | |
4401 if (!is_object($date) || !is_a($date, 'DateTime')) { | |
4402 try { | |
4403 $timestamp = rcube_utils::strtotime($date); | |
4404 $date = new DateTime("@".$timestamp); | |
4405 } | |
4406 catch (Exception $e) { | |
4407 return null; | |
4408 } | |
4409 } | |
4410 | |
4411 return $date->format('d-M-Y H:i:s O'); | |
4412 } | |
4413 | |
4414 /** | |
4415 * This is our own debug handler for the IMAP connection | |
4416 */ | |
4417 public function debug_handler(&$imap, $message) | |
4418 { | |
4419 rcube::write_log('imap', $message); | |
4420 } | |
4421 } |