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