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