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 }