4 +-----------------------------------------------------------------------+
5 | program/include/rcube_imap_cache.php |
7 | This file is part of the Roundcube Webmail client |
8 | Copyright (C) 2005-2011, The Roundcube Dev Team |
9 | Licensed under the GNU GPL |
12 | Caching of IMAP folder contents (messages and index) |
14 +-----------------------------------------------------------------------+
15 | Author: Thomas Bruederli <roundcube@gmail.com> |
16 | Author: Aleksander Machniak <alec@alec.pl> |
17 +-----------------------------------------------------------------------+
19 $Id: rcube_imap_cache.php 5761 2012-01-12 14:54:21Z alec $
25 * Interface class for accessing Roundcube messages cache
28 * @author Thomas Bruederli <roundcube@gmail.com>
29 * @author Aleksander Machniak <alec@alec.pl>
32 class rcube_imap_cache
35 * Instance of rcube_imap
42 * Instance of rcube_mdb2
56 * Internal (in-memory) cache
60 private $icache = array();
62 private $skip_deleted = false;
65 * List of known flags. Thanks to this we can handle flag changes
66 * with good performance. Bad thing is we need to know used flags.
68 public $flags = array(
69 1 => 'SEEN', // RFC3501
70 2 => 'DELETED', // RFC3501
71 4 => 'ANSWERED', // RFC3501
72 8 => 'FLAGGED', // RFC3501
73 16 => 'DRAFT', // RFC3501
74 32 => 'MDNSENT', // RFC3503
75 64 => 'FORWARDED', // RFC5550
76 128 => 'SUBMITPENDING', // RFC5550
77 256 => 'SUBMITTED', // RFC5550
91 function __construct($db, $imap, $userid, $skip_deleted)
95 $this->userid = (int)$userid;
96 $this->skip_deleted = $skip_deleted;
101 * Cleanup actions (on shutdown).
103 public function close()
105 $this->save_icache();
106 $this->icache = null;
111 * Return (sorted) messages index.
112 * If index doesn't exist or is invalid, will be updated.
114 * @param string $mailbox Folder name
115 * @param string $sort_field Sorting column
116 * @param string $sort_order Sorting order (ASC|DESC)
117 * @param bool $exiting Skip index initialization if it doesn't exist in DB
119 * @return array Messages index
121 function get_index($mailbox, $sort_field = null, $sort_order = null, $existing = false)
123 if (empty($this->icache[$mailbox]))
124 $this->icache[$mailbox] = array();
126 $sort_order = strtoupper($sort_order) == 'ASC' ? 'ASC' : 'DESC';
128 // Seek in internal cache
129 if (array_key_exists('index', $this->icache[$mailbox])) {
130 // The index was fetched from database already, but not validated yet
131 if (!array_key_exists('result', $this->icache[$mailbox]['index'])) {
132 $index = $this->icache[$mailbox]['index'];
134 // We've got a valid index
135 else if ($sort_field == 'ANY' || $this->icache[$mailbox]['index']['sort_field'] == $sort_field
137 if ($this->icache[$mailbox]['index']['sort_order'] == $sort_order)
138 return $this->icache[$mailbox]['index']['result'];
140 return array_reverse($this->icache[$mailbox]['index']['result'], true);
144 // Get index from DB (if DB wasn't already queried)
145 if (empty($index) && empty($this->icache[$mailbox]['index_queried'])) {
146 $index = $this->get_index_row($mailbox);
148 // set the flag that DB was already queried for index
149 // this way we'll be able to skip one SELECT, when
150 // get_index() is called more than once
151 $this->icache[$mailbox]['index_queried'] = true;
156 // @TODO: Think about skipping validation checks.
157 // If we could check only every 10 minutes, we would be able to skip
158 // expensive checks, mailbox selection or even IMAP connection, this would require
159 // additional logic to force cache invalidation in some cases
160 // and many rcube_imap changes to connect when needed
162 // Entry exists, check cache status
163 if (!empty($index)) {
166 if ($sort_field == 'ANY') {
167 $sort_field = $index['sort_field'];
170 if ($sort_field != $index['sort_field']) {
174 $is_valid = $this->validate($mailbox, $index, $exists);
178 // build index, assign sequence IDs to unique IDs
179 $data = array_combine($index['seq'], $index['uid']);
180 // revert the order if needed
181 if ($index['sort_order'] != $sort_order)
182 $data = array_reverse($data, true);
189 else if ($sort_field == 'ANY') {
193 // Got it in internal cache, so the row already exist
194 $exists = array_key_exists('index', $this->icache[$mailbox]);
197 // Index not found, not valid or sort field changed, get index from IMAP server
198 if ($data === null) {
199 // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
200 $mbox_data = $this->imap->mailbox_data($mailbox);
201 $data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
204 $this->add_index_row($mailbox, $sort_field, $sort_order, $data, $mbox_data,
205 $exists, $index['modseq']);
208 $this->icache[$mailbox]['index'] = array(
210 'sort_field' => $sort_field,
211 'sort_order' => $sort_order,
212 'modseq' => !empty($index['modseq']) ? $index['modseq'] : $mbox_data['HIGHESTMODSEQ']
220 * Return messages thread.
221 * If threaded index doesn't exist or is invalid, will be updated.
223 * @param string $mailbox Folder name
224 * @param string $sort_field Sorting column
225 * @param string $sort_order Sorting order (ASC|DESC)
227 * @return array Messages threaded index
229 function get_thread($mailbox)
231 if (empty($this->icache[$mailbox]))
232 $this->icache[$mailbox] = array();
234 // Seek in internal cache
235 if (array_key_exists('thread', $this->icache[$mailbox])) {
237 $this->icache[$mailbox]['thread']['tree'],
238 $this->icache[$mailbox]['thread']['depth'],
239 $this->icache[$mailbox]['thread']['children'],
243 // Get thread from DB (if DB wasn't already queried)
244 if (empty($this->icache[$mailbox]['thread_queried'])) {
245 $index = $this->get_thread_row($mailbox);
247 // set the flag that DB was already queried for thread
248 // this way we'll be able to skip one SELECT, when
249 // get_thread() is called more than once or after clear()
250 $this->icache[$mailbox]['thread_queried'] = true;
255 // Entry exist, check cache status
256 if (!empty($index)) {
258 $is_valid = $this->validate($mailbox, $index, $exists);
265 // Index not found or not valid, get index from IMAP server
266 if ($index === null) {
267 // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
268 $mbox_data = $this->imap->mailbox_data($mailbox);
270 if ($mbox_data['EXISTS']) {
271 // get all threads (default sort order)
272 list ($thread_tree, $msg_depth, $has_children) = $this->imap->fetch_threads($mailbox, true);
276 'tree' => !empty($thread_tree) ? $thread_tree : array(),
277 'depth' => !empty($msg_depth) ? $msg_depth : array(),
278 'children' => !empty($has_children) ? $has_children : array(),
282 $this->add_thread_row($mailbox, $index, $mbox_data, $exists);
285 $this->icache[$mailbox]['thread'] = $index;
287 return array($index['tree'], $index['depth'], $index['children']);
292 * Returns list of messages (headers). See rcube_imap::fetch_headers().
294 * @param string $mailbox Folder name
295 * @param array $msgs Message sequence numbers
296 * @param bool $is_uid True if $msgs contains message UIDs
298 * @return array The list of messages (rcube_mail_header) indexed by UID
300 function get_messages($mailbox, $msgs = array(), $is_uid = true)
306 // @TODO: it would be nice if we could work with UIDs only
307 // then index would be not needed. For now we need it to
308 // map id to uid here and to update message id for cached message
310 // Convert IDs to UIDs
311 $index = $this->get_index($mailbox, 'ANY');
313 foreach ($msgs as $idx => $msgid)
314 if ($uid = $index[$msgid])
318 // Fetch messages from cache
319 $sql_result = $this->db->query(
320 "SELECT uid, data, flags"
321 ." FROM ".get_table_name('cache_messages')
322 ." WHERE user_id = ?"
324 ." AND uid IN (".$this->db->array2list($msgs, 'integer').")",
325 $this->userid, $mailbox);
327 $msgs = array_flip($msgs);
330 while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
331 $uid = intval($sql_arr['uid']);
332 $result[$uid] = $this->build_message($sql_arr);
334 // save memory, we don't need message body here (?)
335 $result[$uid]->body = null;
337 // update message ID according to index data
338 if (!empty($index) && ($id = array_search($uid, $index)))
339 $result[$uid]->id = $id;
341 if (!empty($result[$uid])) {
346 // Fetch not found messages from IMAP server
348 $messages = $this->imap->fetch_headers($mailbox, array_keys($msgs), true, true);
350 // Insert to DB and add to result list
351 if (!empty($messages)) {
352 foreach ($messages as $msg) {
353 $this->add_message($mailbox, $msg, !array_key_exists($msg->uid, $result));
354 $result[$msg->uid] = $msg;
364 * Returns message data.
366 * @param string $mailbox Folder name
367 * @param int $uid Message UID
368 * @param bool $update If message doesn't exists in cache it will be fetched
370 * @param bool $no_cache Enables internal cache usage
372 * @return rcube_mail_header Message data
374 function get_message($mailbox, $uid, $update = true, $cache = true)
376 // Check internal cache
377 if (($message = $this->icache['message'])
378 && $message['mailbox'] == $mailbox && $message['object']->uid == $uid
380 return $this->icache['message']['object'];
383 $sql_result = $this->db->query(
385 ." FROM ".get_table_name('cache_messages')
386 ." WHERE user_id = ?"
389 $this->userid, $mailbox, (int)$uid);
391 if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
392 $message = $this->build_message($sql_arr);
395 // update message ID according to index data
396 $index = $this->get_index($mailbox, 'ANY');
397 if (!empty($index) && ($id = array_search($uid, $index)))
401 // Get the message from IMAP server
402 if (empty($message) && $update) {
403 $message = $this->imap->get_headers($uid, $mailbox, true);
404 // cache will be updated in close(), see below
407 // Save the message in internal cache, will be written to DB in close()
408 // Common scenario: user opens unseen message
409 // - get message (SELECT)
410 // - set message headers/structure (INSERT or UPDATE)
411 // - set \Seen flag (UPDATE)
412 // This way we can skip one UPDATE
413 if (!empty($message) && $cache) {
414 // Save current message from internal cache
415 $this->save_icache();
417 $this->icache['message'] = array(
418 'object' => $message,
419 'mailbox' => $mailbox,
421 'md5sum' => md5(serialize($message)),
430 * Saves the message in cache.
432 * @param string $mailbox Folder name
433 * @param rcube_mail_header $message Message data
434 * @param bool $force Skips message in-cache existance check
436 function add_message($mailbox, $message, $force = false)
438 if (!is_object($message) || empty($message->uid))
441 $msg = serialize($this->db->encode(clone $message));
444 if (!empty($message->flags)) {
445 foreach ($this->flags as $idx => $flag)
446 if (!empty($message->flags[$flag]))
451 // update cache record (even if it exists, the update
452 // here will work as select, assume row exist if affected_rows=0)
454 $res = $this->db->query(
455 "UPDATE ".get_table_name('cache_messages')
456 ." SET flags = ?, data = ?, changed = ".$this->db->now()
457 ." WHERE user_id = ?"
460 $flags, $msg, $this->userid, $mailbox, (int) $message->uid);
462 if ($this->db->affected_rows())
468 "INSERT INTO ".get_table_name('cache_messages')
469 ." (user_id, mailbox, uid, flags, changed, data)"
470 ." VALUES (?, ?, ?, ?, ".$this->db->now().", ?)",
471 $this->userid, $mailbox, (int) $message->uid, $flags, $msg);
476 * Sets the flag for specified message.
478 * @param string $mailbox Folder name
479 * @param array $uids Message UIDs or null to change flag
480 * of all messages in a folder
481 * @param string $flag The name of the flag
482 * @param bool $enabled Flag state
484 function change_flag($mailbox, $uids, $flag, $enabled = false)
486 $flag = strtoupper($flag);
487 $idx = (int) array_search($flag, $this->flags);
493 // Internal cache update
494 if ($uids && count($uids) == 1 && ($uid = current($uids))
495 && ($message = $this->icache['message'])
496 && $message['mailbox'] == $mailbox && $message['object']->uid == $uid
498 $message['object']->flags[$flag] = $enabled;
503 "UPDATE ".get_table_name('cache_messages')
504 ." SET changed = ".$this->db->now()
505 .", flags = flags ".($enabled ? "+ $idx" : "- $idx")
506 ." WHERE user_id = ?"
508 .($uids !== null ? " AND uid IN (".$this->db->array2list((array)$uids, 'integer').")" : "")
509 ." AND (flags & $idx) ".($enabled ? "= 0" : "= $idx"),
510 $this->userid, $mailbox);
515 * Removes message(s) from cache.
517 * @param string $mailbox Folder name
518 * @param array $uids Message UIDs, NULL removes all messages
520 function remove_message($mailbox = null, $uids = null)
522 if (!strlen($mailbox)) {
524 "DELETE FROM ".get_table_name('cache_messages')
525 ." WHERE user_id = ?",
529 // Remove the message from internal cache
530 if (!empty($uids) && !is_array($uids) && ($message = $this->icache['message'])
531 && $message['mailbox'] == $mailbox && $message['object']->uid == $uids
533 $this->icache['message'] = null;
537 "DELETE FROM ".get_table_name('cache_messages')
538 ." WHERE user_id = ?"
539 ." AND mailbox = ".$this->db->quote($mailbox)
540 .($uids !== null ? " AND uid IN (".$this->db->array2list((array)$uids, 'integer').")" : ""),
548 * Clears index cache.
550 * @param string $mailbox Folder name
551 * @param bool $remove Enable to remove the DB row
553 function remove_index($mailbox = null, $remove = false)
555 // The index should be only removed from database when
556 // UIDVALIDITY was detected or the mailbox is empty
557 // otherwise use 'valid' flag to not loose HIGHESTMODSEQ value
560 "DELETE FROM ".get_table_name('cache_index')
561 ." WHERE user_id = ".intval($this->userid)
562 .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : "")
566 "UPDATE ".get_table_name('cache_index')
568 ." WHERE user_id = ".intval($this->userid)
569 .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : "")
572 if (strlen($mailbox)) {
573 unset($this->icache[$mailbox]['index']);
574 // Index removed, set flag to skip SELECT query in get_index()
575 $this->icache[$mailbox]['index_queried'] = true;
578 $this->icache = array();
583 * Clears thread cache.
585 * @param string $mailbox Folder name
587 function remove_thread($mailbox = null)
590 "DELETE FROM ".get_table_name('cache_thread')
591 ." WHERE user_id = ".intval($this->userid)
592 .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : "")
595 if (strlen($mailbox)) {
596 unset($this->icache[$mailbox]['thread']);
597 // Thread data removed, set flag to skip SELECT query in get_thread()
598 $this->icache[$mailbox]['thread_queried'] = true;
601 $this->icache = array();
608 * @param string $mailbox Folder name
609 * @param array $uids Message UIDs, NULL removes all messages in a folder
611 function clear($mailbox = null, $uids = null)
613 $this->remove_index($mailbox, true);
614 $this->remove_thread($mailbox);
615 $this->remove_message($mailbox, $uids);
620 * @param string $mailbox Folder name
621 * @param int $id Message (sequence) ID
623 * @return int Message UID
625 function id2uid($mailbox, $id)
627 if (!empty($this->icache['pending_index_update']))
630 // get index if it exists
631 $index = $this->get_index($mailbox, 'ANY', null, true);
638 * @param string $mailbox Folder name
639 * @param int $uid Message UID
641 * @return int Message (sequence) ID
643 function uid2id($mailbox, $uid)
645 if (!empty($this->icache['pending_index_update']))
648 // get index if it exists
649 $index = $this->get_index($mailbox, 'ANY', null, true);
651 return array_search($uid, (array)$index);
655 * Fetches index data from database
657 private function get_index_row($mailbox)
660 $sql_result = $this->db->query(
662 ." FROM ".get_table_name('cache_index')
663 ." WHERE user_id = ?"
665 $this->userid, $mailbox);
667 if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
668 $data = explode('@', $sql_arr['data']);
671 'valid' => $sql_arr['valid'],
672 'seq' => explode(',', $data[0]),
673 'uid' => explode(',', $data[1]),
674 'sort_field' => $data[2],
675 'sort_order' => $data[3],
676 'deleted' => $data[4],
677 'validity' => $data[5],
678 'uidnext' => $data[6],
679 'modseq' => $data[7],
688 * Fetches thread data from database
690 private function get_thread_row($mailbox)
692 // Get thread from DB
693 $sql_result = $this->db->query(
695 ." FROM ".get_table_name('cache_thread')
696 ." WHERE user_id = ?"
698 $this->userid, $mailbox);
700 if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
701 $data = explode('@', $sql_arr['data']);
703 // Uncompress data, see add_thread_row()
704 // $data[0] = str_replace(array('*', '^', '#'), array(';a:0:{}', 'i:', ';a:1:'), $data[0]);
705 $data[0] = unserialize($data[0]);
707 // build 'depth' and 'children' arrays
708 $depth = $children = array();
709 $this->build_thread_data($data[0], $depth, $children);
714 'children' => $children,
715 'deleted' => $data[1],
716 'validity' => $data[2],
717 'uidnext' => $data[3],
726 * Saves index data into database
728 private function add_index_row($mailbox, $sort_field, $sort_order,
729 $data = array(), $mbox_data = array(), $exists = false, $modseq = null)
732 implode(',', array_keys($data)),
733 implode(',', array_values($data)),
736 (int) $this->skip_deleted,
737 (int) $mbox_data['UIDVALIDITY'],
738 (int) $mbox_data['UIDNEXT'],
739 $modseq ? $modseq : $mbox_data['HIGHESTMODSEQ'],
741 $data = implode('@', $data);
744 $sql_result = $this->db->query(
745 "UPDATE ".get_table_name('cache_index')
746 ." SET data = ?, valid = 1, changed = ".$this->db->now()
747 ." WHERE user_id = ?"
749 $data, $this->userid, $mailbox);
751 $sql_result = $this->db->query(
752 "INSERT INTO ".get_table_name('cache_index')
753 ." (user_id, mailbox, data, valid, changed)"
754 ." VALUES (?, ?, ?, 1, ".$this->db->now().")",
755 $this->userid, $mailbox, $data);
760 * Saves thread data into database
762 private function add_thread_row($mailbox, $data = array(), $mbox_data = array(), $exists = false)
764 $tree = serialize($data['tree']);
765 // This significantly reduces data length
766 // $tree = str_replace(array(';a:0:{}', 'i:', ';a:1:'), array('*', '^', '#'), $tree);
770 (int) $this->skip_deleted,
771 (int) $mbox_data['UIDVALIDITY'],
772 (int) $mbox_data['UIDNEXT'],
774 $data = implode('@', $data);
777 $sql_result = $this->db->query(
778 "UPDATE ".get_table_name('cache_thread')
779 ." SET data = ?, changed = ".$this->db->now()
780 ." WHERE user_id = ?"
782 $data, $this->userid, $mailbox);
784 $sql_result = $this->db->query(
785 "INSERT INTO ".get_table_name('cache_thread')
786 ." (user_id, mailbox, data, changed)"
787 ." VALUES (?, ?, ?, ".$this->db->now().")",
788 $this->userid, $mailbox, $data);
793 * Checks index/thread validity
795 private function validate($mailbox, $index, &$exists = true)
797 $is_thread = isset($index['tree']);
799 // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
800 $mbox_data = $this->imap->mailbox_data($mailbox);
802 // @TODO: Think about skipping validation checks.
803 // If we could check only every 10 minutes, we would be able to skip
804 // expensive checks, mailbox selection or even IMAP connection, this would require
805 // additional logic to force cache invalidation in some cases
806 // and many rcube_imap changes to connect when needed
809 if ($index['validity'] != $mbox_data['UIDVALIDITY']) {
810 $this->clear($mailbox);
815 // Folder is empty but cache isn't
816 if (empty($mbox_data['EXISTS'])) {
817 if (!empty($index['seq']) || !empty($index['tree'])) {
818 $this->clear($mailbox);
823 // Folder is not empty but cache is
824 else if (empty($index['seq']) && empty($index['tree'])) {
825 unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
830 if (!$is_thread && empty($index['valid'])) {
831 unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
835 // Index was created with different skip_deleted setting
836 if ($this->skip_deleted != $index['deleted']) {
840 // Check HIGHESTMODSEQ
841 if (!empty($index['modseq']) && !empty($mbox_data['HIGHESTMODSEQ'])
842 && $index['modseq'] == $mbox_data['HIGHESTMODSEQ']
848 if ($index['uidnext'] != $mbox_data['UIDNEXT']) {
849 unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
853 // @TODO: find better validity check for threaded index
855 // check messages number...
856 if (!$this->skip_deleted && $mbox_data['EXISTS'] != @max(array_keys($index['depth']))) {
862 // The rest of checks, more expensive
863 if (!empty($this->skip_deleted)) {
864 // compare counts if available
865 if ($mbox_data['COUNT_UNDELETED'] != null
866 && $mbox_data['COUNT_UNDELETED'] != count($index['uid'])) {
870 if ($mbox_data['ALL_UNDELETED'] != null) {
871 $uids_new = rcube_imap_generic::uncompressMessageSet($mbox_data['ALL_UNDELETED']);
872 $uids_old = $index['uid'];
874 if (count($uids_new) != count($uids_old)) {
878 sort($uids_new, SORT_NUMERIC);
879 sort($uids_old, SORT_NUMERIC);
881 if ($uids_old != $uids_new)
885 // get all undeleted messages excluding cached UIDs
886 $ids = $this->imap->search_once($mailbox, 'ALL UNDELETED NOT UID '.
887 rcube_imap_generic::compressMessageSet($index['uid']));
895 // check messages number...
896 if ($mbox_data['EXISTS'] != max($index['seq'])) {
900 if (max($index['uid']) != $this->imap->id2uid($mbox_data['EXISTS'], $mailbox, true)) {
910 * Synchronizes the mailbox.
912 * @param string $mailbox Folder name
914 function synchronize($mailbox)
916 // RFC4549: Synchronization Operations for Disconnected IMAP4 Clients
917 // RFC4551: IMAP Extension for Conditional STORE Operation
918 // or Quick Flag Changes Resynchronization
919 // RFC5162: IMAP Extensions for Quick Mailbox Resynchronization
921 // @TODO: synchronize with other methods?
922 $qresync = $this->imap->get_capability('QRESYNC');
923 $condstore = $qresync ? true : $this->imap->get_capability('CONDSTORE');
925 if (!$qresync && !$condstore) {
930 $index = $this->get_index_row($mailbox);
934 // set the flag that DB was already queried for index
935 // this way we'll be able to skip one SELECT in get_index()
936 $this->icache[$mailbox]['index_queried'] = true;
940 $this->icache[$mailbox]['index'] = $index;
942 // no last HIGHESTMODSEQ value
943 if (empty($index['modseq'])) {
947 // NOTE: make sure the mailbox isn't selected, before
948 // enabling QRESYNC and invoking SELECT
949 if ($this->imap->conn->selected !== null) {
950 $this->imap->conn->close();
954 $res = $this->imap->conn->enable($qresync ? 'QRESYNC' : 'CONDSTORE');
955 if (!is_array($res)) {
959 // Get mailbox data (UIDVALIDITY, HIGHESTMODSEQ, counters, etc.)
960 $mbox_data = $this->imap->mailbox_data($mailbox);
962 if (empty($mbox_data)) {
967 if ($index['validity'] != $mbox_data['UIDVALIDITY']) {
968 $this->clear($mailbox);
972 // QRESYNC not supported on specified mailbox
973 if (!empty($mbox_data['NOMODSEQ']) || empty($mbox_data['HIGHESTMODSEQ'])) {
978 if ($mbox_data['HIGHESTMODSEQ'] == $index['modseq']) {
984 $sql_result = $this->db->query(
986 ." FROM ".get_table_name('cache_messages')
987 ." WHERE user_id = ?"
989 $this->userid, $mailbox);
991 while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
992 $uids[] = $sql_arr['uid'];
995 // No messages in database, nothing to sync
1000 // Get modified flags and vanished messages
1001 // UID FETCH 1:* (FLAGS) (CHANGEDSINCE 0123456789 VANISHED)
1002 $result = $this->imap->conn->fetch($mailbox,
1003 !empty($uids) ? $uids : '1:*', true, array('FLAGS'),
1004 $index['modseq'], $qresync);
1006 $invalidated = false;
1008 if (!empty($result)) {
1009 foreach ($result as $id => $msg) {
1011 // Remove deleted message
1012 if ($this->skip_deleted && !empty($msg->flags['DELETED'])) {
1013 $this->remove_message($mailbox, $uid);
1015 if (!$invalidated) {
1016 $invalidated = true;
1017 // Invalidate thread indexes (?)
1018 $this->remove_thread($mailbox);
1020 $index['valid'] = false;
1026 if (!empty($msg->flags)) {
1027 foreach ($this->flags as $idx => $flag)
1028 if (!empty($msg->flags[$flag]))
1033 "UPDATE ".get_table_name('cache_messages')
1034 ." SET flags = ?, changed = ".$this->db->now()
1035 ." WHERE user_id = ?"
1039 $flags, $this->userid, $mailbox, $uid, $flags);
1045 $mbox_data = $this->imap->mailbox_data($mailbox);
1048 if (!empty($mbox_data['VANISHED'])) {
1049 $uids = rcube_imap_generic::uncompressMessageSet($mbox_data['VANISHED']);
1050 if (!empty($uids)) {
1051 // remove messages from database
1052 $this->remove_message($mailbox, $uids);
1054 // Invalidate thread indexes (?)
1055 $this->remove_thread($mailbox);
1057 $index['valid'] = false;
1062 $sort_field = $index['sort_field'];
1063 $sort_order = $index['sort_order'];
1067 if (!$this->validate($mailbox, $index, $exists)) {
1069 $data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
1072 $data = array_combine($index['seq'], $index['uid']);
1075 // update index and/or HIGHESTMODSEQ value
1076 $this->add_index_row($mailbox, $sort_field, $sort_order, $data, $mbox_data, $exists);
1078 // update internal cache for get_index()
1079 $this->icache[$mailbox]['index']['result'] = $data;
1084 * Converts cache row into message object.
1086 * @param array $sql_arr Message row data
1088 * @return rcube_mail_header Message object
1090 private function build_message($sql_arr)
1092 $message = $this->db->decode(unserialize($sql_arr['data']));
1095 $message->flags = array();
1096 foreach ($this->flags as $idx => $flag)
1097 if (($sql_arr['flags'] & $idx) == $idx)
1098 $message->flags[$flag] = true;
1106 * Creates 'depth' and 'children' arrays from stored thread 'tree' data.
1108 private function build_thread_data($data, &$depth, &$children, $level = 0)
1110 foreach ((array)$data as $key => $val) {
1111 $empty = empty($val) || !is_array($val);
1112 $children[$key] = !$empty;
1113 $depth[$key] = $level;
1115 $this->build_thread_data($val, $depth, $children, $level + 1);
1122 * Saves message stored in internal cache
1124 private function save_icache()
1126 // Save current message from internal cache
1127 if ($message = $this->icache['message']) {
1128 // clean up some object's data
1129 $object = $this->message_object_prepare($message['object']);
1131 // calculate current md5 sum
1132 $md5sum = md5(serialize($object));
1134 if ($message['md5sum'] != $md5sum) {
1135 $this->add_message($message['mailbox'], $object, !$message['exists']);
1138 $this->icache['message']['md5sum'] = $md5sum;
1144 * Prepares message object to be stored in database.
1146 private function message_object_prepare($msg)
1148 // Remove body too big (>25kB)
1149 if ($msg->body && strlen($msg->body) > 25 * 1024) {
1153 // Fix mimetype which might be broken by some code when message is displayed
1154 // Another solution would be to use object's copy in rcube_message class
1155 // to prevent related issues, however I'm not sure which is better
1156 if ($msg->mimetype) {
1157 list($msg->ctype_primary, $msg->ctype_secondary) = explode('/', $msg->mimetype);
1160 if (is_array($msg->structure->parts)) {
1161 foreach ($msg->structure->parts as $idx => $part) {
1162 $msg->structure->parts[$idx] = $this->message_object_prepare($part);
1171 * Fetches index data from IMAP server
1173 private function get_index_data($mailbox, $sort_field, $sort_order, $mbox_data = array())
1177 if (empty($mbox_data)) {
1178 $mbox_data = $this->imap->mailbox_data($mailbox);
1181 // Prevent infinite loop.
1182 // It happens when rcube_imap::message_index_direct() is called.
1183 // There id2uid() is called which will again call get_index() and so on.
1184 if (!$sort_field && !$this->skip_deleted)
1185 $this->icache['pending_index_update'] = true;
1187 if ($mbox_data['EXISTS']) {
1188 // fetch sorted sequence numbers
1189 $data_seq = $this->imap->message_index_direct($mailbox, $sort_field, $sort_order);
1191 if (!empty($data_seq)) {
1192 // Seek in internal cache
1193 if (array_key_exists('index', (array)$this->icache[$mailbox])
1194 && array_key_exists('result', (array)$this->icache[$mailbox]['index'])
1196 $data_uid = $this->icache[$mailbox]['index']['result'];
1198 $data_uid = $this->imap->conn->fetchUIDs($mailbox, $data_seq);
1201 if (!empty($data_uid)) {
1202 foreach ($data_seq as $seq)
1203 if ($uid = $data_uid[$seq])
1209 // Reset internal flags
1210 $this->icache['pending_index_update'] = false;