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