thomascube
2012-02-11 f9a967763e2a4cc595b44f40ac1ca666b2a02af6
commit | author | age
80152b 1 <?php
A 2
3 /*
4  +-----------------------------------------------------------------------+
5  | program/include/rcube_imap_cache.php                                  |
6  |                                                                       |
7  | This file is part of the Roundcube Webmail client                     |
8  | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
7fe381 9  |                                                                       |
T 10  | Licensed under the GNU General Public License version 3 or            |
11  | any later version with exceptions for skins & plugins.                |
12  | See the README file for a full license statement.                     |
80152b 13  |                                                                       |
A 14  | PURPOSE:                                                              |
15  |   Caching of IMAP folder contents (messages and index)                |
16  |                                                                       |
17  +-----------------------------------------------------------------------+
18  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
19  | Author: Aleksander Machniak <alec@alec.pl>                            |
20  +-----------------------------------------------------------------------+
21
22  $Id$
23
24 */
25
26
27 /**
28  * Interface class for accessing Roundcube messages cache
29  *
30  * @package    Cache
31  * @author     Thomas Bruederli <roundcube@gmail.com>
32  * @author     Aleksander Machniak <alec@alec.pl>
33  * @version    1.0
34  */
35 class rcube_imap_cache
36 {
37     /**
38      * Instance of rcube_imap
39      *
40      * @var rcube_imap
41      */
42     private $imap;
43
44     /**
45      * Instance of rcube_mdb2
46      *
47      * @var rcube_mdb2
48      */
49     private $db;
50
51     /**
52      * User ID
53      *
54      * @var int
55      */
56     private $userid;
57
58     /**
59      * Internal (in-memory) cache
60      *
61      * @var array
62      */
63     private $icache = array();
64
65     private $skip_deleted = false;
66
609d39 67     /**
A 68      * List of known flags. Thanks to this we can handle flag changes
69      * with good performance. Bad thing is we need to know used flags.
70      */
71     public $flags = array(
72         1       => 'SEEN',          // RFC3501
73         2       => 'DELETED',       // RFC3501
74         4       => 'ANSWERED',      // RFC3501
75         8       => 'FLAGGED',       // RFC3501
76         16      => 'DRAFT',         // RFC3501
77         32      => 'MDNSENT',       // RFC3503
78         64      => 'FORWARDED',     // RFC5550
79         128     => 'SUBMITPENDING', // RFC5550
80         256     => 'SUBMITTED',     // RFC5550
81         512     => 'JUNK',
82         1024    => 'NONJUNK',
83         2048    => 'LABEL1',
84         4096    => 'LABEL2',
85         8192    => 'LABEL3',
86         16384   => 'LABEL4',
87         32768   => 'LABEL5',
88     );
80152b 89
A 90
91     /**
92      * Object constructor.
93      */
94     function __construct($db, $imap, $userid, $skip_deleted)
95     {
96         $this->db           = $db;
97         $this->imap         = $imap;
98         $this->userid       = (int)$userid;
99         $this->skip_deleted = $skip_deleted;
100     }
101
102
103     /**
104      * Cleanup actions (on shutdown).
105      */
106     public function close()
107     {
108         $this->save_icache();
109         $this->icache = null;
110     }
111
112
113     /**
40c45e 114      * Return (sorted) messages index (UIDs).
80152b 115      * If index doesn't exist or is invalid, will be updated.
A 116      *
117      * @param string  $mailbox     Folder name
118      * @param string  $sort_field  Sorting column
119      * @param string  $sort_order  Sorting order (ASC|DESC)
120      * @param bool    $exiting     Skip index initialization if it doesn't exist in DB
121      *
122      * @return array Messages index
123      */
124     function get_index($mailbox, $sort_field = null, $sort_order = null, $existing = false)
125     {
a267c6 126         if (empty($this->icache[$mailbox])) {
80152b 127             $this->icache[$mailbox] = array();
a267c6 128         }
2a5702 129
80152b 130         $sort_order = strtoupper($sort_order) == 'ASC' ? 'ASC' : 'DESC';
A 131
132         // Seek in internal cache
609d39 133         if (array_key_exists('index', $this->icache[$mailbox])) {
A 134             // The index was fetched from database already, but not validated yet
40c45e 135             if (!array_key_exists('object', $this->icache[$mailbox]['index'])) {
609d39 136                 $index = $this->icache[$mailbox]['index'];
A 137             }
138             // We've got a valid index
40c45e 139             else if ($sort_field == 'ANY' || $this->icache[$mailbox]['index']['sort_field'] == $sort_field) {
A 140                 $result = $this->icache[$mailbox]['index']['object'];
c321a9 141                 if ($result->get_parameters('ORDER') != $sort_order) {
40c45e 142                     $result->revert();
A 143                 }
144                 return $result;
609d39 145             }
80152b 146         }
A 147
148         // Get index from DB (if DB wasn't already queried)
609d39 149         if (empty($index) && empty($this->icache[$mailbox]['index_queried'])) {
80152b 150             $index = $this->get_index_row($mailbox);
A 151
152             // set the flag that DB was already queried for index
153             // this way we'll be able to skip one SELECT, when
154             // get_index() is called more than once
155             $this->icache[$mailbox]['index_queried'] = true;
156         }
609d39 157
A 158         $data = null;
80152b 159
A 160         // @TODO: Think about skipping validation checks.
161         // If we could check only every 10 minutes, we would be able to skip
162         // expensive checks, mailbox selection or even IMAP connection, this would require
163         // additional logic to force cache invalidation in some cases
164         // and many rcube_imap changes to connect when needed
165
609d39 166         // Entry exists, check cache status
80152b 167         if (!empty($index)) {
A 168             $exists = true;
169
170             if ($sort_field == 'ANY') {
171                 $sort_field = $index['sort_field'];
172             }
173
174             if ($sort_field != $index['sort_field']) {
175                 $is_valid = false;
176             }
177             else {
178                 $is_valid = $this->validate($mailbox, $index, $exists);
179             }
2a5702 180
80152b 181             if ($is_valid) {
40c45e 182                 $data = $index['object'];
80152b 183                 // revert the order if needed
c321a9 184                 if ($data->get_parameters('ORDER') != $sort_order) {
40c45e 185                     $data->revert();
A 186                 }
80152b 187             }
A 188         }
189         else {
190             if ($existing) {
191                 return null;
192             }
193             else if ($sort_field == 'ANY') {
194                 $sort_field = '';
195             }
609d39 196
A 197             // Got it in internal cache, so the row already exist
198             $exists = array_key_exists('index', $this->icache[$mailbox]);
80152b 199         }
A 200
201         // Index not found, not valid or sort field changed, get index from IMAP server
202         if ($data === null) {
203             // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
c321a9 204             $mbox_data = $this->imap->folder_data($mailbox);
609d39 205             $data      = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
80152b 206
A 207             // insert/update
40c45e 208             $this->add_index_row($mailbox, $sort_field, $data, $mbox_data, $exists, $index['modseq']);
80152b 209         }
A 210
211         $this->icache[$mailbox]['index'] = array(
40c45e 212             'object'     => $data,
80152b 213             'sort_field' => $sort_field,
609d39 214             'modseq'     => !empty($index['modseq']) ? $index['modseq'] : $mbox_data['HIGHESTMODSEQ']
80152b 215         );
A 216
217         return $data;
218     }
219
220
221     /**
222      * Return messages thread.
223      * If threaded index doesn't exist or is invalid, will be updated.
224      *
225      * @param string  $mailbox     Folder name
226      * @param string  $sort_field  Sorting column
227      * @param string  $sort_order  Sorting order (ASC|DESC)
228      *
229      * @return array Messages threaded index
230      */
231     function get_thread($mailbox)
232     {
a267c6 233         if (empty($this->icache[$mailbox])) {
80152b 234             $this->icache[$mailbox] = array();
a267c6 235         }
80152b 236
A 237         // Seek in internal cache
238         if (array_key_exists('thread', $this->icache[$mailbox])) {
40c45e 239             return $this->icache[$mailbox]['thread']['object'];
80152b 240         }
A 241
609d39 242         // Get thread from DB (if DB wasn't already queried)
A 243         if (empty($this->icache[$mailbox]['thread_queried'])) {
244             $index = $this->get_thread_row($mailbox);
245
246             // set the flag that DB was already queried for thread
247             // this way we'll be able to skip one SELECT, when
248             // get_thread() is called more than once or after clear()
249             $this->icache[$mailbox]['thread_queried'] = true;
250         }
80152b 251
A 252         // Entry exist, check cache status
253         if (!empty($index)) {
254             $exists   = true;
255             $is_valid = $this->validate($mailbox, $index, $exists);
256
257             if (!$is_valid) {
258                 $index = null;
259             }
260         }
261
262         // Index not found or not valid, get index from IMAP server
263         if ($index === null) {
264             // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
c321a9 265             $mbox_data = $this->imap->folder_data($mailbox);
80152b 266
A 267             if ($mbox_data['EXISTS']) {
268                 // get all threads (default sort order)
40c45e 269                 $threads = $this->imap->fetch_threads($mailbox, true);
A 270             }
271             else {
2a5702 272                 $threads = new rcube_result_thread($mailbox, '* THREAD');
80152b 273             }
A 274
40c45e 275             $index['object'] = $threads;
80152b 276
A 277             // insert/update
40c45e 278             $this->add_thread_row($mailbox, $threads, $mbox_data, $exists);
80152b 279         }
A 280
281         $this->icache[$mailbox]['thread'] = $index;
282
40c45e 283         return $index['object'];
80152b 284     }
A 285
286
287     /**
288      * Returns list of messages (headers). See rcube_imap::fetch_headers().
289      *
290      * @param string $mailbox  Folder name
40c45e 291      * @param array  $msgs     Message UIDs
80152b 292      *
A 293      * @return array The list of messages (rcube_mail_header) indexed by UID
294      */
40c45e 295     function get_messages($mailbox, $msgs = array())
80152b 296     {
A 297         if (empty($msgs)) {
298             return array();
299         }
300
301         // Fetch messages from cache
302         $sql_result = $this->db->query(
609d39 303             "SELECT uid, data, flags"
80152b 304             ." FROM ".get_table_name('cache_messages')
A 305             ." WHERE user_id = ?"
306                 ." AND mailbox = ?"
307                 ." AND uid IN (".$this->db->array2list($msgs, 'integer').")",
308             $this->userid, $mailbox);
309
310         $msgs   = array_flip($msgs);
311         $result = array();
312
313         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
314             $uid          = intval($sql_arr['uid']);
315             $result[$uid] = $this->build_message($sql_arr);
609d39 316
A 317             // save memory, we don't need message body here (?)
80152b 318             $result[$uid]->body = null;
609d39 319
80152b 320             if (!empty($result[$uid])) {
A 321                 unset($msgs[$uid]);
322             }
323         }
324
325         // Fetch not found messages from IMAP server
326         if (!empty($msgs)) {
40c45e 327             $messages = $this->imap->fetch_headers($mailbox, array_keys($msgs), false, true);
80152b 328
A 329             // Insert to DB and add to result list
330             if (!empty($messages)) {
331                 foreach ($messages as $msg) {
332                     $this->add_message($mailbox, $msg, !array_key_exists($msg->uid, $result));
333                     $result[$msg->uid] = $msg;
334                 }
335             }
336         }
337
338         return $result;
339     }
340
341
342     /**
343      * Returns message data.
344      *
345      * @param string $mailbox  Folder name
346      * @param int    $uid      Message UID
609d39 347      * @param bool   $update   If message doesn't exists in cache it will be fetched
A 348      *                         from IMAP server
349      * @param bool   $no_cache Enables internal cache usage
80152b 350      *
A 351      * @return rcube_mail_header Message data
352      */
609d39 353     function get_message($mailbox, $uid, $update = true, $cache = true)
80152b 354     {
A 355         // Check internal cache
356         if (($message = $this->icache['message'])
357             && $message['mailbox'] == $mailbox && $message['object']->uid == $uid
358         ) {
359             return $this->icache['message']['object'];
360         }
361
362         $sql_result = $this->db->query(
609d39 363             "SELECT flags, data"
80152b 364             ." FROM ".get_table_name('cache_messages')
A 365             ." WHERE user_id = ?"
366                 ." AND mailbox = ?"
367                 ." AND uid = ?",
368                 $this->userid, $mailbox, (int)$uid);
369
370         if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
371             $message = $this->build_message($sql_arr);
372             $found   = true;
373         }
374
375         // Get the message from IMAP server
609d39 376         if (empty($message) && $update) {
c321a9 377             $message = $this->imap->get_message_headers($uid, $mailbox, true);
80152b 378             // cache will be updated in close(), see below
A 379         }
380
381         // Save the message in internal cache, will be written to DB in close()
382         // Common scenario: user opens unseen message
383         // - get message (SELECT)
384         // - set message headers/structure (INSERT or UPDATE)
385         // - set \Seen flag (UPDATE)
386         // This way we can skip one UPDATE
609d39 387         if (!empty($message) && $cache) {
80152b 388             // Save current message from internal cache
A 389             $this->save_icache();
390
391             $this->icache['message'] = array(
392                 'object'  => $message,
393                 'mailbox' => $mailbox,
394                 'exists'  => $found,
395                 'md5sum'  => md5(serialize($message)),
396             );
397         }
398
399         return $message;
400     }
401
402
403     /**
404      * Saves the message in cache.
405      *
406      * @param string            $mailbox  Folder name
407      * @param rcube_mail_header $message  Message data
408      * @param bool              $force    Skips message in-cache existance check
409      */
410     function add_message($mailbox, $message, $force = false)
411     {
a267c6 412         if (!is_object($message) || empty($message->uid)) {
80152b 413             return;
a267c6 414         }
80152b 415
609d39 416         $msg   = serialize($this->db->encode(clone $message));
A 417         $flags = 0;
80152b 418
609d39 419         if (!empty($message->flags)) {
a267c6 420             foreach ($this->flags as $idx => $flag) {
A 421                 if (!empty($message->flags[$flag])) {
609d39 422                     $flags += $idx;
a267c6 423                 }
A 424             }
609d39 425         }
A 426         unset($msg->flags);
80152b 427
A 428         // update cache record (even if it exists, the update
429         // here will work as select, assume row exist if affected_rows=0)
430         if (!$force) {
431             $res = $this->db->query(
432                 "UPDATE ".get_table_name('cache_messages')
609d39 433                 ." SET flags = ?, data = ?, changed = ".$this->db->now()
80152b 434                 ." WHERE user_id = ?"
A 435                     ." AND mailbox = ?"
436                     ." AND uid = ?",
609d39 437                 $flags, $msg, $this->userid, $mailbox, (int) $message->uid);
80152b 438
a267c6 439             if ($this->db->affected_rows()) {
80152b 440                 return;
a267c6 441             }
80152b 442         }
A 443
444         // insert new record
445         $this->db->query(
446             "INSERT INTO ".get_table_name('cache_messages')
609d39 447             ." (user_id, mailbox, uid, flags, changed, data)"
A 448             ." VALUES (?, ?, ?, ?, ".$this->db->now().", ?)",
449             $this->userid, $mailbox, (int) $message->uid, $flags, $msg);
80152b 450     }
A 451
452
453     /**
454      * Sets the flag for specified message.
455      *
456      * @param string  $mailbox  Folder name
457      * @param array   $uids     Message UIDs or null to change flag
458      *                          of all messages in a folder
459      * @param string  $flag     The name of the flag
460      * @param bool    $enabled  Flag state
461      */
462     function change_flag($mailbox, $uids, $flag, $enabled = false)
463     {
609d39 464         $flag = strtoupper($flag);
A 465         $idx  = (int) array_search($flag, $this->flags);
80152b 466
609d39 467         if (!$idx) {
A 468             return;
469         }
80152b 470
609d39 471         // Internal cache update
A 472         if ($uids && count($uids) == 1 && ($uid = current($uids))
473             && ($message = $this->icache['message'])
474             && $message['mailbox'] == $mailbox && $message['object']->uid == $uid
475         ) {
476             $message['object']->flags[$flag] = $enabled;
477             return;
80152b 478         }
609d39 479
A 480         $this->db->query(
481             "UPDATE ".get_table_name('cache_messages')
482             ." SET changed = ".$this->db->now()
483             .", flags = flags ".($enabled ? "+ $idx" : "- $idx")
484             ." WHERE user_id = ?"
485                 ." AND mailbox = ?"
486                 .($uids !== null ? " AND uid IN (".$this->db->array2list((array)$uids, 'integer').")" : "")
487                 ." AND (flags & $idx) ".($enabled ? "= 0" : "= $idx"),
488             $this->userid, $mailbox);
80152b 489     }
A 490
491
492     /**
493      * Removes message(s) from cache.
494      *
495      * @param string $mailbox  Folder name
496      * @param array  $uids     Message UIDs, NULL removes all messages
497      */
498     function remove_message($mailbox = null, $uids = null)
499     {
500         if (!strlen($mailbox)) {
501             $this->db->query(
502                 "DELETE FROM ".get_table_name('cache_messages')
503                 ." WHERE user_id = ?",
504                 $this->userid);
505         }
506         else {
507             // Remove the message from internal cache
508             if (!empty($uids) && !is_array($uids) && ($message = $this->icache['message'])
509                 && $message['mailbox'] == $mailbox && $message['object']->uid == $uids
510             ) {
511                 $this->icache['message'] = null;
512             }
513
514             $this->db->query(
515                 "DELETE FROM ".get_table_name('cache_messages')
516                 ." WHERE user_id = ?"
517                     ." AND mailbox = ".$this->db->quote($mailbox)
518                     .($uids !== null ? " AND uid IN (".$this->db->array2list((array)$uids, 'integer').")" : ""),
519                 $this->userid);
520         }
521
522     }
523
524
525     /**
526      * Clears index cache.
527      *
528      * @param string  $mailbox     Folder name
609d39 529      * @param bool    $remove      Enable to remove the DB row
80152b 530      */
609d39 531     function remove_index($mailbox = null, $remove = false)
80152b 532     {
609d39 533         // The index should be only removed from database when
A 534         // UIDVALIDITY was detected or the mailbox is empty
535         // otherwise use 'valid' flag to not loose HIGHESTMODSEQ value
a267c6 536         if ($remove) {
609d39 537             $this->db->query(
A 538                 "DELETE FROM ".get_table_name('cache_index')
539                 ." WHERE user_id = ".intval($this->userid)
540                     .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : "")
541             );
a267c6 542         }
A 543         else {
609d39 544             $this->db->query(
A 545                 "UPDATE ".get_table_name('cache_index')
546                 ." SET valid = 0"
547                 ." WHERE user_id = ".intval($this->userid)
548                     .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : "")
549             );
a267c6 550         }
80152b 551
609d39 552         if (strlen($mailbox)) {
80152b 553             unset($this->icache[$mailbox]['index']);
609d39 554             // Index removed, set flag to skip SELECT query in get_index()
A 555             $this->icache[$mailbox]['index_queried'] = true;
556         }
a267c6 557         else {
80152b 558             $this->icache = array();
a267c6 559         }
80152b 560     }
A 561
562
563     /**
564      * Clears thread cache.
565      *
566      * @param string  $mailbox     Folder name
567      */
568     function remove_thread($mailbox = null)
569     {
570         $this->db->query(
571             "DELETE FROM ".get_table_name('cache_thread')
572             ." WHERE user_id = ".intval($this->userid)
573                 .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : "")
574         );
575
609d39 576         if (strlen($mailbox)) {
80152b 577             unset($this->icache[$mailbox]['thread']);
609d39 578             // Thread data removed, set flag to skip SELECT query in get_thread()
A 579             $this->icache[$mailbox]['thread_queried'] = true;
580         }
a267c6 581         else {
80152b 582             $this->icache = array();
a267c6 583         }
80152b 584     }
A 585
586
587     /**
588      * Clears the cache.
589      *
590      * @param string $mailbox  Folder name
591      * @param array  $uids     Message UIDs, NULL removes all messages in a folder
592      */
593     function clear($mailbox = null, $uids = null)
594     {
609d39 595         $this->remove_index($mailbox, true);
80152b 596         $this->remove_thread($mailbox);
A 597         $this->remove_message($mailbox, $uids);
598     }
599
600
601     /**
602      * Fetches index data from database
603      */
604     private function get_index_row($mailbox)
605     {
606         // Get index from DB
607         $sql_result = $this->db->query(
609d39 608             "SELECT data, valid"
80152b 609             ." FROM ".get_table_name('cache_index')
A 610             ." WHERE user_id = ?"
611                 ." AND mailbox = ?",
612             $this->userid, $mailbox);
613
614         if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
40c45e 615             $data  = explode('@', $sql_arr['data']);
A 616             $index = @unserialize($data[0]);
617             unset($data[0]);
618
619             if (empty($index)) {
620                 $index = new rcube_result_index($mailbox);
621             }
80152b 622
A 623             return array(
609d39 624                 'valid'      => $sql_arr['valid'],
40c45e 625                 'object'     => $index,
A 626                 'sort_field' => $data[1],
627                 'deleted'    => $data[2],
628                 'validity'   => $data[3],
629                 'uidnext'    => $data[4],
630                 'modseq'     => $data[5],
80152b 631             );
A 632         }
633
634         return null;
635     }
636
637
638     /**
639      * Fetches thread data from database
640      */
641     private function get_thread_row($mailbox)
642     {
643         // Get thread from DB
644         $sql_result = $this->db->query(
645             "SELECT data"
646             ." FROM ".get_table_name('cache_thread')
647             ." WHERE user_id = ?"
648                 ." AND mailbox = ?",
649             $this->userid, $mailbox);
650
651         if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
40c45e 652             $data   = explode('@', $sql_arr['data']);
A 653             $thread = @unserialize($data[0]);
654             unset($data[0]);
80152b 655
40c45e 656             if (empty($thread)) {
2a5702 657                 $thread = new rcube_result_thread($mailbox);
40c45e 658             }
80152b 659
A 660             return array(
40c45e 661                 'object'   => $thread,
80152b 662                 'deleted'  => $data[1],
A 663                 'validity' => $data[2],
664                 'uidnext'  => $data[3],
665             );
666         }
667
668         return null;
669     }
670
671
672     /**
673      * Saves index data into database
674      */
40c45e 675     private function add_index_row($mailbox, $sort_field,
A 676         $data, $mbox_data = array(), $exists = false, $modseq = null)
80152b 677     {
A 678         $data = array(
40c45e 679             serialize($data),
80152b 680             $sort_field,
A 681             (int) $this->skip_deleted,
682             (int) $mbox_data['UIDVALIDITY'],
683             (int) $mbox_data['UIDNEXT'],
609d39 684             $modseq ? $modseq : $mbox_data['HIGHESTMODSEQ'],
80152b 685         );
A 686         $data = implode('@', $data);
687
a267c6 688         if ($exists) {
80152b 689             $sql_result = $this->db->query(
A 690                 "UPDATE ".get_table_name('cache_index')
609d39 691                 ." SET data = ?, valid = 1, changed = ".$this->db->now()
80152b 692                 ." WHERE user_id = ?"
A 693                     ." AND mailbox = ?",
694                 $data, $this->userid, $mailbox);
a267c6 695         }
A 696         else {
80152b 697             $sql_result = $this->db->query(
A 698                 "INSERT INTO ".get_table_name('cache_index')
609d39 699                 ." (user_id, mailbox, data, valid, changed)"
A 700                 ." VALUES (?, ?, ?, 1, ".$this->db->now().")",
80152b 701                 $this->userid, $mailbox, $data);
a267c6 702         }
80152b 703     }
A 704
705
706     /**
707      * Saves thread data into database
708      */
40c45e 709     private function add_thread_row($mailbox, $data, $mbox_data = array(), $exists = false)
80152b 710     {
A 711         $data = array(
40c45e 712             serialize($data),
80152b 713             (int) $this->skip_deleted,
A 714             (int) $mbox_data['UIDVALIDITY'],
715             (int) $mbox_data['UIDNEXT'],
716         );
717         $data = implode('@', $data);
718
a267c6 719         if ($exists) {
80152b 720             $sql_result = $this->db->query(
A 721                 "UPDATE ".get_table_name('cache_thread')
722                 ." SET data = ?, changed = ".$this->db->now()
723                 ." WHERE user_id = ?"
724                     ." AND mailbox = ?",
725                 $data, $this->userid, $mailbox);
a267c6 726         }
A 727         else {
80152b 728             $sql_result = $this->db->query(
A 729                 "INSERT INTO ".get_table_name('cache_thread')
730                 ." (user_id, mailbox, data, changed)"
731                 ." VALUES (?, ?, ?, ".$this->db->now().")",
732                 $this->userid, $mailbox, $data);
a267c6 733         }
80152b 734     }
A 735
736
737     /**
738      * Checks index/thread validity
739      */
740     private function validate($mailbox, $index, &$exists = true)
741     {
40c45e 742         $object    = $index['object'];
A 743         $is_thread = is_a($object, 'rcube_result_thread');
80152b 744
A 745         // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
c321a9 746         $mbox_data = $this->imap->folder_data($mailbox);
80152b 747
A 748         // @TODO: Think about skipping validation checks.
749         // If we could check only every 10 minutes, we would be able to skip
750         // expensive checks, mailbox selection or even IMAP connection, this would require
751         // additional logic to force cache invalidation in some cases
752         // and many rcube_imap changes to connect when needed
753
754         // Check UIDVALIDITY
755         if ($index['validity'] != $mbox_data['UIDVALIDITY']) {
609d39 756             $this->clear($mailbox);
80152b 757             $exists = false;
A 758             return false;
759         }
760
761         // Folder is empty but cache isn't
37d511 762         if (empty($mbox_data['EXISTS'])) {
c321a9 763             if (!$object->is_empty()) {
37d511 764                 $this->clear($mailbox);
A 765                 $exists = false;
766                 return false;
767             }
768         }
769         // Folder is not empty but cache is
c321a9 770         else if ($object->is_empty()) {
37d511 771             unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
80152b 772             return false;
A 773         }
774
609d39 775         // Validation flag
A 776         if (!$is_thread && empty($index['valid'])) {
40c45e 777             unset($this->icache[$mailbox]['index']);
80152b 778             return false;
A 779         }
780
781         // Index was created with different skip_deleted setting
782         if ($this->skip_deleted != $index['deleted']) {
609d39 783             return false;
A 784         }
785
786         // Check HIGHESTMODSEQ
787         if (!empty($index['modseq']) && !empty($mbox_data['HIGHESTMODSEQ'])
788             && $index['modseq'] == $mbox_data['HIGHESTMODSEQ']
789         ) {
790             return true;
791         }
792
793         // Check UIDNEXT
794         if ($index['uidnext'] != $mbox_data['UIDNEXT']) {
795             unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
80152b 796             return false;
A 797         }
798
799         // @TODO: find better validity check for threaded index
800         if ($is_thread) {
801             // check messages number...
c321a9 802             if (!$this->skip_deleted && $mbox_data['EXISTS'] != $object->count_messages()) {
80152b 803                 return false;
A 804             }
805             return true;
806         }
807
808         // The rest of checks, more expensive
809         if (!empty($this->skip_deleted)) {
810             // compare counts if available
40c45e 811             if (!empty($mbox_data['UNDELETED'])
A 812                 && $mbox_data['UNDELETED']->count() != $object->count()
813             ) {
80152b 814                 return false;
A 815             }
816             // compare UID sets
40c45e 817             if (!empty($mbox_data['UNDELETED'])) {
A 818                 $uids_new = $mbox_data['UNDELETED']->get();
819                 $uids_old = $object->get();
80152b 820
A 821                 if (count($uids_new) != count($uids_old)) {
822                     return false;
823                 }
824
825                 sort($uids_new, SORT_NUMERIC);
826                 sort($uids_old, SORT_NUMERIC);
827
828                 if ($uids_old != $uids_new)
829                     return false;
830             }
831             else {
832                 // get all undeleted messages excluding cached UIDs
833                 $ids = $this->imap->search_once($mailbox, 'ALL UNDELETED NOT UID '.
40c45e 834                     rcube_imap_generic::compressMessageSet($object->get()));
80152b 835
c321a9 836                 if (!$ids->is_empty()) {
37d511 837                     return false;
80152b 838                 }
A 839             }
840         }
841         else {
842             // check messages number...
40c45e 843             if ($mbox_data['EXISTS'] != $object->count()) {
80152b 844                 return false;
A 845             }
846             // ... and max UID
40c45e 847             if ($object->max() != $this->imap->id2uid($mbox_data['EXISTS'], $mailbox, true)) {
80152b 848                 return false;
A 849             }
850         }
851
852         return true;
853     }
854
855
856     /**
609d39 857      * Synchronizes the mailbox.
A 858      *
859      * @param string $mailbox Folder name
860      */
861     function synchronize($mailbox)
862     {
863         // RFC4549: Synchronization Operations for Disconnected IMAP4 Clients
864         // RFC4551: IMAP Extension for Conditional STORE Operation
865         //          or Quick Flag Changes Resynchronization
866         // RFC5162: IMAP Extensions for Quick Mailbox Resynchronization
867
868         // @TODO: synchronize with other methods?
869         $qresync   = $this->imap->get_capability('QRESYNC');
870         $condstore = $qresync ? true : $this->imap->get_capability('CONDSTORE');
871
872         if (!$qresync && !$condstore) {
873             return;
874         }
875
876         // Get stored index
877         $index = $this->get_index_row($mailbox);
878
879         // database is empty
880         if (empty($index)) {
881             // set the flag that DB was already queried for index
882             // this way we'll be able to skip one SELECT in get_index()
883             $this->icache[$mailbox]['index_queried'] = true;
884             return;
885         }
886
887         $this->icache[$mailbox]['index'] = $index;
888
889         // no last HIGHESTMODSEQ value
890         if (empty($index['modseq'])) {
891             return;
892         }
893
c321a9 894         if (!$this->imap->check_connection()) {
T 895             return;
896         }
897
609d39 898         // NOTE: make sure the mailbox isn't selected, before
A 899         // enabling QRESYNC and invoking SELECT
900         if ($this->imap->conn->selected !== null) {
901             $this->imap->conn->close();
902         }
903
904         // Enable QRESYNC
905         $res = $this->imap->conn->enable($qresync ? 'QRESYNC' : 'CONDSTORE');
906         if (!is_array($res)) {
907             return;
908         }
909
910         // Get mailbox data (UIDVALIDITY, HIGHESTMODSEQ, counters, etc.)
c321a9 911         $mbox_data = $this->imap->folder_data($mailbox);
609d39 912
A 913         if (empty($mbox_data)) {
914              return;
915         }
916
917         // Check UIDVALIDITY
918         if ($index['validity'] != $mbox_data['UIDVALIDITY']) {
919             $this->clear($mailbox);
920             return;
921         }
922
923         // QRESYNC not supported on specified mailbox
924         if (!empty($mbox_data['NOMODSEQ']) || empty($mbox_data['HIGHESTMODSEQ'])) {
925             return;
926         }
927
928         // Nothing new
929         if ($mbox_data['HIGHESTMODSEQ'] == $index['modseq']) {
930             return;
931         }
932
933         // Get known uids
934         $uids = array();
935         $sql_result = $this->db->query(
936             "SELECT uid"
937             ." FROM ".get_table_name('cache_messages')
938             ." WHERE user_id = ?"
939                 ." AND mailbox = ?",
940             $this->userid, $mailbox);
941
942         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
943           $uids[] = $sql_arr['uid'];
944         }
945
946         // No messages in database, nothing to sync
947         if (empty($uids)) {
948             return;
949         }
950
951         // Get modified flags and vanished messages
952         // UID FETCH 1:* (FLAGS) (CHANGEDSINCE 0123456789 VANISHED)
953         $result = $this->imap->conn->fetch($mailbox,
954             !empty($uids) ? $uids : '1:*', true, array('FLAGS'),
955             $index['modseq'], $qresync);
956
37d511 957         $invalidated = false;
A 958
609d39 959         if (!empty($result)) {
A 960             foreach ($result as $id => $msg) {
961                 $uid = $msg->uid;
962                 // Remove deleted message
963                 if ($this->skip_deleted && !empty($msg->flags['DELETED'])) {
964                     $this->remove_message($mailbox, $uid);
37d511 965
A 966                     if (!$invalidated) {
967                         $invalidated = true;
968                         // Invalidate thread indexes (?)
969                         $this->remove_thread($mailbox);
970                         // Invalidate index
971                         $index['valid'] = false;
972                     }
609d39 973                     continue;
A 974                 }
975
976                 $flags = 0;
977                 if (!empty($msg->flags)) {
978                     foreach ($this->flags as $idx => $flag)
979                         if (!empty($msg->flags[$flag]))
980                             $flags += $idx;
981                 }
982
983                 $this->db->query(
984                     "UPDATE ".get_table_name('cache_messages')
985                     ." SET flags = ?, changed = ".$this->db->now()
986                     ." WHERE user_id = ?"
987                         ." AND mailbox = ?"
988                         ." AND uid = ?"
989                         ." AND flags <> ?",
990                     $flags, $this->userid, $mailbox, $uid, $flags);
991             }
992         }
993
994         // Get VANISHED
995         if ($qresync) {
c321a9 996             $mbox_data = $this->imap->folder_data($mailbox);
609d39 997
A 998             // Removed messages
999             if (!empty($mbox_data['VANISHED'])) {
1000                 $uids = rcube_imap_generic::uncompressMessageSet($mbox_data['VANISHED']);
1001                 if (!empty($uids)) {
1002                     // remove messages from database
1003                     $this->remove_message($mailbox, $uids);
1004
1005                     // Invalidate thread indexes (?)
1006                     $this->remove_thread($mailbox);
37d511 1007                     // Invalidate index
A 1008                     $index['valid'] = false;
609d39 1009                 }
A 1010             }
1011         }
1012
1013         $sort_field = $index['sort_field'];
c321a9 1014         $sort_order = $index['object']->get_parameters('ORDER');
609d39 1015         $exists     = true;
A 1016
1017         // Validate index
1018         if (!$this->validate($mailbox, $index, $exists)) {
1019             // Update index
1020             $data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
1021         }
1022         else {
40c45e 1023             $data = $index['object'];
609d39 1024         }
A 1025
1026         // update index and/or HIGHESTMODSEQ value
40c45e 1027         $this->add_index_row($mailbox, $sort_field, $data, $mbox_data, $exists);
609d39 1028
A 1029         // update internal cache for get_index()
40c45e 1030         $this->icache[$mailbox]['index']['object'] = $data;
609d39 1031     }
A 1032
1033
1034     /**
80152b 1035      * Converts cache row into message object.
A 1036      *
1037      * @param array $sql_arr Message row data
1038      *
1039      * @return rcube_mail_header Message object
1040      */
1041     private function build_message($sql_arr)
1042     {
1043         $message = $this->db->decode(unserialize($sql_arr['data']));
1044
1045         if ($message) {
609d39 1046             $message->flags = array();
a267c6 1047             foreach ($this->flags as $idx => $flag) {
A 1048                 if (($sql_arr['flags'] & $idx) == $idx) {
609d39 1049                     $message->flags[$flag] = true;
a267c6 1050                 }
A 1051            }
80152b 1052         }
A 1053
1054         return $message;
1055     }
1056
1057
1058     /**
1059      * Saves message stored in internal cache
1060      */
1061     private function save_icache()
1062     {
1063         // Save current message from internal cache
1064         if ($message = $this->icache['message']) {
71f72f 1065             // clean up some object's data
A 1066             $object = $this->message_object_prepare($message['object']);
80152b 1067
A 1068             // calculate current md5 sum
1069             $md5sum = md5(serialize($object));
1070
1071             if ($message['md5sum'] != $md5sum) {
1072                 $this->add_message($message['mailbox'], $object, !$message['exists']);
1073             }
1074
1075             $this->icache['message']['md5sum'] = $md5sum;
1076         }
1077     }
1078
71f72f 1079
A 1080     /**
1081      * Prepares message object to be stored in database.
1082      */
609d39 1083     private function message_object_prepare($msg)
71f72f 1084     {
609d39 1085         // Remove body too big (>25kB)
A 1086         if ($msg->body && strlen($msg->body) > 25 * 1024) {
71f72f 1087             unset($msg->body);
A 1088         }
1089
1090         // Fix mimetype which might be broken by some code when message is displayed
1091         // Another solution would be to use object's copy in rcube_message class
1092         // to prevent related issues, however I'm not sure which is better
1093         if ($msg->mimetype) {
1094             list($msg->ctype_primary, $msg->ctype_secondary) = explode('/', $msg->mimetype);
1095         }
1096
1097         if (is_array($msg->structure->parts)) {
1098             foreach ($msg->structure->parts as $idx => $part) {
609d39 1099                 $msg->structure->parts[$idx] = $this->message_object_prepare($part);
71f72f 1100             }
A 1101         }
1102
1103         return $msg;
1104     }
609d39 1105
A 1106
1107     /**
1108      * Fetches index data from IMAP server
1109      */
1110     private function get_index_data($mailbox, $sort_field, $sort_order, $mbox_data = array())
1111     {
1112         if (empty($mbox_data)) {
c321a9 1113             $mbox_data = $this->imap->folder_data($mailbox);
609d39 1114         }
A 1115
1116         if ($mbox_data['EXISTS']) {
1117             // fetch sorted sequence numbers
c321a9 1118             $index = $this->imap->index_direct($mailbox, $sort_field, $sort_order);
40c45e 1119         }
A 1120         else {
1121             $index = new rcube_result_index($mailbox, '* SORT');
609d39 1122         }
A 1123
40c45e 1124         return $index;
609d39 1125     }
80152b 1126 }