1 /* statuscache_db.c -- Status caching routines
2 *
3 * Copyright (c) 1994-2008 Carnegie Mellon University. All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in
14 * the documentation and/or other materials provided with the
15 * distribution.
16 *
17 * 3. The name "Carnegie Mellon University" must not be used to
18 * endorse or promote products derived from this software without
19 * prior written permission. For permission or any legal
20 * details, please contact
21 * Carnegie Mellon University
22 * Center for Technology Transfer and Enterprise Creation
23 * 4615 Forbes Avenue
24 * Suite 302
25 * Pittsburgh, PA 15213
26 * (412) 268-7393, fax: (412) 268-7395
27 * innovation@andrew.cmu.edu
28 *
29 * 4. Redistributions of any form whatsoever must retain the following
30 * acknowledgment:
31 * "This product includes software developed by Computing Services
32 * at Carnegie Mellon University (http://www.cmu.edu/computing/)."
33 *
34 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
35 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
36 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
37 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
38 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
39 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
40 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
41 */
42
43 #include <config.h>
44
45 #include <stdio.h>
46 #include <stdlib.h>
47 #include <string.h>
48 #ifdef HAVE_UNISTD_H
49 #include <unistd.h>
50 #endif
51 #include <sys/types.h>
52 #include <sys/stat.h>
53 #include <sys/uio.h>
54 #include <fcntl.h>
55 #include <syslog.h>
56
57 #include "assert.h"
58 #include "cyrusdb.h"
59 #include "imapd.h"
60 #include "global.h"
61 #include "mboxlist.h"
62 #include "mailbox.h"
63 #include "seen.h"
64 #include "util.h"
65 #include "xmalloc.h"
66 #include "xstrlcpy.h"
67
68 /* generated headers are not necessarily in current directory */
69 #include "imap/imap_err.h"
70
71 #include "statuscache.h"
72
73 #define DB config_statuscache_db
74
75 static struct db *statuscachedb = NULL;
76 static int _initted = 0;
77
78 /********************* CACHE METHODS ***********************/
79
statuscache_open(void)80 static void statuscache_open(void)
81 {
82 if (!config_getswitch(IMAPOPT_STATUSCACHE))
83 return;
84
85 char *fname = xstrdupnull(config_getstring(IMAPOPT_STATUSCACHE_DB_PATH));
86 if (!fname)
87 fname = strconcat(config_dir, FNAME_STATUSCACHEDB, (char *)NULL);
88
89 int r = cyrusdb_open(DB, fname, CYRUSDB_CREATE, &statuscachedb);
90 if (r) {
91 syslog(LOG_ERR, "DBERROR: opening %s: %s", fname,
92 cyrusdb_strerror(r));
93 syslog(LOG_ERR, "statuscache in degraded mode");
94 statuscachedb = NULL;
95 }
96
97 free(fname);
98 }
99
statuscache_close(void)100 static void statuscache_close(void)
101 {
102 if (!statuscachedb) return;
103
104 int r = cyrusdb_close(statuscachedb);
105 if (r) {
106 syslog(LOG_ERR, "DBERROR: error closing statuscache: %s",
107 cyrusdb_strerror(r));
108 }
109
110 statuscachedb = NULL;
111 }
112
done_cb(void * rock)113 static void done_cb(void *rock __attribute__((unused)))
114 {
115 statuscache_close();
116 }
117
init_internal()118 static void init_internal()
119 {
120 if (_initted) return;
121 statuscache_open();
122 cyrus_modules_add(done_cb, NULL);
123 _initted = 1;
124 }
125
statuscache_buildkey(const char * mboxname,const char * userid,struct buf * buf)126 static void statuscache_buildkey(const char *mboxname, const char *userid,
127 struct buf *buf)
128 {
129 buf_setcstr(buf, mboxname);
130 /* double % is a safe separator, it can't exist in a mailboxname */
131 buf_putc(buf, '%');
132 if (userid) {
133 buf_putc(buf, '%');
134 buf_appendcstr(buf, userid);
135 }
136 }
137
statuscache_read_index(const char * mboxname,struct statusdata * sdata)138 static void statuscache_read_index(const char *mboxname, struct statusdata *sdata)
139 {
140 struct buf keybuf = BUF_INITIALIZER;
141 const char *data = NULL;
142 size_t datalen = 0;
143
144 /* Don't access DB if it hasn't been opened */
145 if (!statuscachedb)
146 return;
147
148 /* Check if there is an entry in the database */
149 statuscache_buildkey(mboxname, NULL, &keybuf);
150 int r = cyrusdb_fetch(statuscachedb, keybuf.s, keybuf.len, &data, &datalen, NULL);
151 buf_free(&keybuf);
152
153 if (r || !data || !datalen)
154 return;
155
156 const char *dend = data + datalen;
157
158 char *p = (char *)data;
159 if (*p++ != 'I') return;
160 if (*p++ != ' ') return;
161
162 unsigned version = (unsigned) strtoul(p, &p, 10);
163 if (version != (unsigned) STATUSCACHE_VERSION) {
164 /* Wrong version */
165 return;
166 }
167
168 if (*p++ != ' ') return;
169 if (*p++ != '(') return;
170
171 // read the matched items
172 if (p < dend) sdata->messages = strtoul(p, &p, 10);
173 if (p < dend) sdata->uidnext = strtoul(p, &p, 10);
174 if (p < dend) sdata->uidvalidity = strtoul(p, &p, 10);
175 if (p < dend) sdata->mboptions = strtoul(p, &p, 10);
176 if (p < dend) sdata->size = strtoull(p, &p, 10);
177 if (p < dend) sdata->createdmodseq = strtoull(p, &p, 10);
178 if (p < dend) sdata->highestmodseq = strtoull(p, &p, 10);
179
180 if (*p++ != ')') return;
181
182 /* Sanity check the data */
183 if (!sdata->highestmodseq)
184 return;
185
186 sdata->statusitems |= STATUS_INDEXITEMS | STATUS_UIDVALIDITY;
187 }
188
statuscache_read_seen(const char * mboxname,const char * userid,struct statusdata * sdata)189 static void statuscache_read_seen(const char *mboxname, const char *userid,
190 struct statusdata *sdata)
191 {
192 struct buf keybuf = BUF_INITIALIZER;
193 const char *data = NULL;
194 size_t datalen = 0;
195
196 if (!userid)
197 return;
198
199 // if no messages, other counts must also be zero
200 if (!sdata->messages) {
201 sdata->recent = 0;
202 sdata->unseen = 0;
203 sdata->userid = userid;
204 sdata->statusitems |= STATUS_SEENITEMS;
205 return;
206 }
207
208 /* Don't access DB if it hasn't been opened */
209 if (!statuscachedb)
210 return;
211
212 // we must have a HIGHESTMODSEQ to compare against
213 if (!(sdata->statusitems & STATUS_HIGHESTMODSEQ))
214 return;
215
216 /* Check if there is an entry in the database */
217 statuscache_buildkey(mboxname, userid, &keybuf);
218 int r = cyrusdb_fetch(statuscachedb, keybuf.s, keybuf.len, &data, &datalen, NULL);
219 buf_free(&keybuf);
220
221 if (r || !data || !datalen)
222 return;
223
224 const char *dend = data + datalen;
225 char *p = (char *)data;
226 if (*p++ != 'S') return;
227 if (*p++ != ' ') return;
228
229 unsigned version = (unsigned) strtoul(p, &p, 10);
230 if (version != (unsigned) STATUSCACHE_VERSION) {
231 /* Wrong version */
232 return;
233 }
234
235 if (*p++ != ' ') return;
236 if (*p++ != '(') return;
237
238 // read the matched items
239 if (p < dend) sdata->recent = strtoul(p, &p, 10);
240 if (p < dend) sdata->unseen = strtoul(p, &p, 10);
241 modseq_t highestmodseq = strtoull(p, &p, 10);
242
243 if (*p++ != ')') return;
244
245 // doesn't match non-unseen key
246 if (highestmodseq != sdata->highestmodseq)
247 return;
248
249 sdata->userid = userid;
250 sdata->statusitems |= STATUS_SEENITEMS;
251 }
252
statuscache_lookup(const char * mboxname,const char * userid,unsigned statusitems,struct statusdata * sdata)253 static int statuscache_lookup(const char *mboxname, const char *userid,
254 unsigned statusitems, struct statusdata *sdata)
255 {
256 // nothing to read!
257 if (!(statusitems & (STATUS_INDEXITEMS|STATUS_SEENITEMS)))
258 return 0;
259
260 init_internal();
261
262 statuscache_read_index(mboxname, sdata);
263 if (statusitems & STATUS_SEENITEMS)
264 statuscache_read_seen(mboxname, userid, sdata);
265
266 // did we get everything we wanted?
267 if ((sdata->statusitems & statusitems) != statusitems)
268 return IMAP_NO_NOSUCHMSG;
269
270 return 0;
271 }
272
statuscache_store(const char * mboxname,struct statusdata * sdata,struct txn ** tidptr)273 static int statuscache_store(const char *mboxname,
274 struct statusdata *sdata,
275 struct txn **tidptr)
276 {
277 struct buf keybuf = BUF_INITIALIZER;
278 struct buf databuf = BUF_INITIALIZER;
279 int r = 0;
280
281 statuscache_buildkey(mboxname, /*userid*/NULL, &keybuf);
282
283 /* if we don't have a full index, just nuke the key */
284 if (!sdata || (sdata->statusitems & STATUS_INDEXITEMS) != STATUS_INDEXITEMS) {
285 r = cyrusdb_delete(statuscachedb, keybuf.s, keybuf.len, tidptr, 1);
286 if (r != CYRUSDB_OK) {
287 syslog(LOG_ERR, "DBERROR: error deleting statuscache for: %s (%s)",
288 mboxname, cyrusdb_strerror(r));
289 }
290 goto done;
291 }
292
293
294 buf_printf(&databuf,
295 "I %u (%u %u %u %u %llu " MODSEQ_FMT " " MODSEQ_FMT ")",
296 STATUSCACHE_VERSION,
297 sdata->messages, sdata->uidnext,
298 sdata->uidvalidity, sdata->mboptions, sdata->size,
299 sdata->createdmodseq, sdata->highestmodseq);
300
301 r = cyrusdb_store(statuscachedb, keybuf.s, keybuf.len, databuf.s, databuf.len, tidptr);
302
303 if (r != CYRUSDB_OK) {
304 syslog(LOG_ERR, "DBERROR: error updating database: %s (%s)",
305 mboxname, cyrusdb_strerror(r));
306 goto done;
307 }
308
309 if ((sdata->statusitems & STATUS_SEENITEMS) != STATUS_SEENITEMS)
310 goto done;
311
312 // if there's no userid, we don't store this stuff
313 if (!sdata->userid)
314 goto done;
315
316 statuscache_buildkey(mboxname, sdata->userid, &keybuf);
317
318 /* The trailing whitespace is necessary because we
319 * use non-length-based functions to parse the values.
320 * Any non-digit char would be fine, but whitespace
321 * looks less ugly in dbtool output */
322 buf_reset(&databuf);
323 buf_printf(&databuf,
324 "S %u (%u %u " MODSEQ_FMT ")",
325 STATUSCACHE_VERSION,
326 sdata->recent, sdata->unseen,
327 sdata->highestmodseq);
328
329 r = cyrusdb_store(statuscachedb, keybuf.s, keybuf.len, databuf.s, databuf.len, tidptr);
330
331 if (r != CYRUSDB_OK) {
332 syslog(LOG_ERR, "DBERROR: error updating database: %s (%s)",
333 mboxname, cyrusdb_strerror(r));
334 goto done;
335 }
336
337 done:
338 buf_free(&keybuf);
339 buf_free(&databuf);
340 return r;
341 }
342
statuscache_invalidate(const char * mboxname,struct statusdata * sdata)343 HIDDEN int statuscache_invalidate(const char *mboxname, struct statusdata *sdata)
344 {
345 int doclose = 0;
346 struct txn *tid = NULL;
347
348 /* if it's disabled then skip */
349 if (!config_getswitch(IMAPOPT_STATUSCACHE))
350 return 0;
351
352 /* if it's not already open, open and close it for just this */
353 if (!statuscachedb) {
354 statuscache_open();
355 // failed to open, oh well
356 if (!statuscachedb)
357 return 0;
358 doclose = 1;
359 }
360
361 int r = statuscache_store(mboxname, sdata, &tid);
362
363 if (!r) {
364 cyrusdb_commit(statuscachedb, tid);
365 }
366 else {
367 syslog(LOG_NOTICE, "DBERROR: failed to store statuscache data for %s", mboxname);
368 if (tid) cyrusdb_abort(statuscachedb, tid);
369 }
370
371 // if we opened the DB, close it now
372 if (doclose)
373 statuscache_close();
374
375 return 0;
376 }
377
378
379
380 /****************** STATUSDATA FILLING METHODS ************************/
381
status_fill_mbentry(const mbentry_t * mbentry,struct statusdata * sdata)382 HIDDEN void status_fill_mbentry(const mbentry_t *mbentry, struct statusdata *sdata)
383 {
384 assert(mbentry);
385 assert(sdata);
386
387 sdata->uidvalidity = mbentry->uidvalidity;
388 sdata->mailboxid = mbentry->uniqueid;
389
390 sdata->statusitems |= STATUS_MBENTRYITEMS;
391 }
392
status_fill_mailbox(struct mailbox * mailbox,struct statusdata * sdata)393 HIDDEN void status_fill_mailbox(struct mailbox *mailbox, struct statusdata *sdata)
394 {
395 assert(mailbox);
396 assert(sdata);
397
398 sdata->messages = mailbox->i.exists;
399 sdata->uidnext = mailbox->i.last_uid+1;
400 sdata->mboptions = mailbox->i.options;
401 sdata->size = mailbox->i.quota_mailbox_used;
402 sdata->createdmodseq = mailbox->i.createdmodseq;
403 sdata->highestmodseq = mailbox->i.highestmodseq;
404
405 // mbentry items are also available from an open mailbox
406 sdata->uidvalidity = mailbox->i.uidvalidity;
407 sdata->mailboxid = mailbox->uniqueid;
408
409 sdata->statusitems |= STATUS_INDEXITEMS | STATUS_MBENTRYITEMS;
410 }
411
status_fill_seen(const char * userid,struct statusdata * sdata,unsigned numrecent,unsigned numunseen)412 HIDDEN void status_fill_seen(const char *userid, struct statusdata *sdata,
413 unsigned numrecent, unsigned numunseen)
414 {
415 assert(userid);
416 assert(sdata);
417
418 // we need a matching parent record to exist for these values to be valid
419 assert(sdata->statusitems & STATUS_HIGHESTMODSEQ);
420
421 sdata->userid = userid;
422 sdata->recent = numrecent;
423 sdata->unseen = numunseen;
424
425 sdata->statusitems |= STATUS_SEENITEMS;
426 }
427
status_load_mailbox(struct mailbox * mailbox,const char * userid,unsigned statusitems,struct statusdata * sdata)428 static int status_load_mailbox(struct mailbox *mailbox, const char *userid,
429 unsigned statusitems, struct statusdata *sdata)
430 {
431 status_fill_mailbox(mailbox, sdata);
432
433 if ((statusitems & STATUS_SEENITEMS) && mailbox->i.exists) {
434 unsigned numrecent = 0;
435 unsigned numunseen = 0;
436 /* Read \Seen state */
437 struct seqset *seq = NULL;
438 int internalseen = mailbox_internal_seen(mailbox, userid);
439 unsigned recentuid;
440
441 if (internalseen) {
442 recentuid = mailbox->i.recentuid;
443 } else {
444 struct seen *seendb = NULL;
445 struct seendata sd = SEENDATA_INITIALIZER;
446
447 int r = seen_open(userid, SEEN_CREATE, &seendb);
448 if (!r) r = seen_read(seendb, mailbox->uniqueid, &sd);
449 seen_close(&seendb);
450 if (r) return r;
451
452 recentuid = sd.lastuid;
453 seq = seqset_parse(sd.seenuids, NULL, recentuid);
454 seen_freedata(&sd);
455 }
456
457 struct mailbox_iter *iter = mailbox_iter_init(mailbox, 0, ITER_SKIP_EXPUNGED);
458 const message_t *msg;
459 while ((msg = mailbox_iter_step(iter))) {
460 const struct index_record *record = msg_record(msg);
461 if (record->uid > recentuid)
462 numrecent++;
463 if (internalseen) {
464 if (!(record->system_flags & FLAG_SEEN))
465 numunseen++;
466 }
467 else {
468 if (!seqset_ismember(seq, record->uid))
469 numunseen++;
470 }
471 }
472 mailbox_iter_done(&iter);
473 seqset_free(seq);
474
475 status_fill_seen(userid, sdata, numrecent, numunseen);
476 }
477
478 statuscache_invalidate(mailbox->name, sdata);
479
480 return 0;
481 }
482
status_lookup_internal(const char * mboxname,const char * userid,unsigned statusitems,struct statusdata * sdata)483 static int status_lookup_internal(const char *mboxname, const char *userid,
484 unsigned statusitems, struct statusdata *sdata)
485 {
486 struct mailbox *mailbox = NULL;
487 int r = 0;
488
489 /* Check status cache if possible */
490 if (config_getswitch(IMAPOPT_STATUSCACHE)) {
491 /* Do actual lookup of cache item. */
492 r = statuscache_lookup(mboxname, userid, statusitems, sdata);
493
494 /* Seen/recent status uses "push" invalidation events from
495 * seen_db.c. This avoids needing to open cyrus.header to get
496 * the mailbox uniqueid to open the seen db and get the
497 * unseen_mtime and recentuid.
498 */
499
500 if (!r) {
501 syslog(LOG_DEBUG, "statuscache, '%s', '%s', '0x%02x', 'yes'",
502 mboxname, userid, statusitems);
503 return 0;
504 }
505
506 syslog(LOG_DEBUG, "statuscache, '%s', '%s', '0x%02x', 'no'",
507 mboxname, userid, statusitems);
508 }
509
510 /* Missing or invalid cache entry */
511 r = mailbox_open_irl(mboxname, &mailbox);
512 if (r) return r;
513
514 r = status_load_mailbox(mailbox, userid, statusitems, sdata);
515
516 /* cache the new value while unlocking */
517 if (!r) mailbox_unlock_index(mailbox, sdata);
518 mailbox_close(&mailbox);
519
520 return r;
521 }
522
status_lookup_mbentry(const mbentry_t * mbentry,const char * userid,unsigned statusitems,struct statusdata * sdata)523 EXPORTED int status_lookup_mbentry(const mbentry_t *mbentry, const char *userid,
524 unsigned statusitems, struct statusdata *sdata)
525 {
526 // check if we can get everything we need from the mbentry
527 status_fill_mbentry(mbentry, sdata);
528 if ((sdata->statusitems & statusitems) == statusitems)
529 return 0;
530
531 return status_lookup_internal(mbentry->name, userid, statusitems, sdata);
532 }
533
status_lookup_mboxname(const char * mboxname,const char * userid,unsigned statusitems,struct statusdata * sdata)534 EXPORTED int status_lookup_mboxname(const char *mboxname, const char *userid,
535 unsigned statusitems, struct statusdata *sdata)
536 {
537 // we want an mbentry first, just in case we can get everything from there
538 if (statusitems & STATUS_MAILBOXID) {
539 mbentry_t *mbentry = NULL;
540 int r = mboxlist_lookup_allow_all(mboxname, &mbentry, NULL);
541 if (r) return r;
542 r = status_lookup_mbentry(mbentry, userid, statusitems, sdata);
543 mboxlist_entry_free(&mbentry);
544 return r;
545 }
546
547 return status_lookup_internal(mboxname, userid, statusitems, sdata);
548 }
549
550
551 // this one has literally no smarts at all
status_lookup_mbname(const mbname_t * mbname,const char * userid,unsigned statusitems,struct statusdata * sdata)552 EXPORTED int status_lookup_mbname(const mbname_t *mbname, const char *userid,
553 unsigned statusitems, struct statusdata *sdata)
554 {
555 return status_lookup_mboxname(mbname_intname(mbname), userid, statusitems, sdata);
556 }
557
558 /*
559 * Performs a STATUS command on an open mailbox
560 */
status_lookup_mailbox(struct mailbox * mailbox,const char * userid,unsigned statusitems,struct statusdata * sdata)561 EXPORTED int status_lookup_mailbox(struct mailbox *mailbox, const char *userid,
562 unsigned statusitems, struct statusdata *sdata)
563 {
564 // check if we already have all the data we need (includes any possible mbentry)
565 status_fill_mailbox(mailbox, sdata);
566 if ((sdata->statusitems & statusitems) == statusitems)
567 return 0;
568
569 /* Check status cache if possible */
570 if (config_getswitch(IMAPOPT_STATUSCACHE)) {
571 /* Do actual lookup of cache item. */
572 int r = statuscache_lookup(mailbox->name, userid, statusitems, sdata);
573
574 /* Seen/recent status uses "push" invalidation events from
575 * seen_db.c. This avoids needing to open cyrus.header to get
576 * the mailbox uniqueid to open the seen db and get the
577 * unseen_mtime and recentuid.
578 */
579
580 if (!r) {
581 syslog(LOG_DEBUG, "statuscache, '%s', '%s', '0x%02x', 'yes'",
582 mailbox->name, userid, statusitems);
583 return 0;
584 }
585
586 syslog(LOG_DEBUG, "statuscache, '%s', '%s', '0x%02x', 'no'",
587 mailbox->name, userid, statusitems);
588 }
589
590 return status_load_mailbox(mailbox, userid, statusitems, sdata);
591 }
592