/* statuscache_db.c -- Status caching routines
*
* Copyright (c) 1994-2008 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* $Id: statuscache_db.c,v 1.8 2010/01/06 17:01:41 murch Exp $
*/
#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <fcntl.h>
#include <ctype.h>
#include <syslog.h>
#include "assert.h"
#include "cyrusdb.h"
#include "exitcodes.h"
#include "imapd.h"
#include "global.h"
#include "imap_err.h"
#include "mboxlist.h"
#include "mailbox.h"
#include "seen.h"
#include "util.h"
#include "xmalloc.h"
#include "xstrlcpy.h"
#include "statuscache.h"
#define DB config_statuscache_db
struct db *statuscachedb;
static int statuscache_dbopen = 0;
void statuscache_open(const char *fname)
{
int ret;
char *tofree = NULL;
if (!fname)
fname = config_getstring(IMAPOPT_STATUSCACHE_DB_PATH);
/* create db file name */
if (!fname) {
tofree = strconcat(config_dir, FNAME_STATUSCACHEDB, (char *)NULL);
fname = tofree;
}
ret = DB->open(fname, CYRUSDB_CREATE, &statuscachedb);
if (ret != 0) {
syslog(LOG_ERR, "DBERROR: opening %s: %s", fname,
cyrusdb_strerror(ret));
syslog(LOG_ERR, "statuscache in degraded mode");
return;
}
free(tofree);
statuscache_dbopen = 1;
}
void statuscache_close(void)
{
int r;
if (statuscache_dbopen) {
r = DB->close(statuscachedb);
if (r) {
syslog(LOG_ERR, "DBERROR: error closing statuscache: %s",
cyrusdb_strerror(r));
}
statuscache_dbopen = 0;
}
}
void statuscache_fill(struct statusdata *sdata, const char *userid,
struct mailbox *mailbox, unsigned statusitems,
unsigned numrecent, unsigned numunseen)
{
assert(sdata);
assert(mailbox);
sdata->userid = userid;
sdata->statusitems = statusitems;
sdata->messages = mailbox->i.exists;
sdata->recent = numrecent;
sdata->uidnext = mailbox->i.last_uid+1;
sdata->uidvalidity = mailbox->i.uidvalidity;
sdata->unseen = numunseen;
sdata->highestmodseq = mailbox->i.highestmodseq;
}
void statuscache_done(void)
{
/* DB->done() handled by cyrus_done() */
}
static char *statuscache_buildkey(const char *mailboxname, const char *userid,
int *keylen)
{
static char key[MAX_MAILBOX_BUFFER];
int len;
/* Build statuscache key */
len = strlcpy(key, mailboxname, sizeof(key));
/* double % is a safe separator, it can't exist in a mailboxname */
key[len++] = '%';
key[len++] = '%';
if (userid)
len += strlcpy(key + len, userid, sizeof(key) - len);
*keylen = len;
return key;
}
/*
* Performs a STATUS command - note: state MAY be NULL here.
*/
int status_lookup(const char *mboxname, const char *userid,
unsigned statusitems, struct statusdata *sdata)
{
struct mailbox *mailbox = NULL;
unsigned numrecent = 0;
unsigned numunseen = 0;
unsigned c_statusitems;
int r;
/* Check status cache if possible */
if (config_getswitch(IMAPOPT_STATUSCACHE)) {
/* Do actual lookup of cache item. */
r = statuscache_lookup(mboxname, userid, statusitems, sdata);
/* Seen/recent status uses "push" invalidation events from
* seen_db.c. This avoids needing to open cyrus.header to get
* the mailbox uniqueid to open the seen db and get the
* unseen_mtime and recentuid.
*/
if (!r) {
syslog(LOG_DEBUG, "statuscache, '%s', '%s', '0x%02x', 'yes'",
mboxname, userid, statusitems);
return 0;
}
syslog(LOG_DEBUG, "statuscache, '%s', '%s', '0x%02x', 'no'",
mboxname, userid, statusitems);
}
/* Missing or invalid cache entry */
r = mailbox_open_irl(mboxname, &mailbox);
if (r) return r;
/* We always have message count, uidnext,
uidvalidity, and highestmodseq for cache */
c_statusitems = STATUS_MESSAGES | STATUS_UIDNEXT |
STATUS_UIDVALIDITY | STATUS_HIGHESTMODSEQ;
if (!mailbox->i.exists) {
/* no messages, so these two must also be zero */
c_statusitems |= STATUS_RECENT | STATUS_UNSEEN;
}
else if (statusitems & (STATUS_RECENT | STATUS_UNSEEN)) {
/* Read \Seen state */
struct seqset *seq = NULL;
uint32_t recno;
struct index_record record;
int internalseen = mailbox_internal_seen(mailbox, userid);
unsigned recentuid;
if (internalseen) {
recentuid = mailbox->i.recentuid;
} else {
struct seen *seendb = NULL;
struct seendata sd = SEENDATA_INITIALIZER;
r = seen_open(userid, SEEN_CREATE, &seendb);
if (!r) r = seen_read(seendb, mailbox->uniqueid, &sd);
seen_close(&seendb);
if (r) goto done;
recentuid = sd.lastuid;
seq = seqset_parse(sd.seenuids, NULL, recentuid);
seen_freedata(&sd);
}
for (recno = 1; recno <= mailbox->i.num_records; recno++) {
if (mailbox_read_index_record(mailbox, recno, &record))
continue;
if (record.system_flags & FLAG_EXPUNGED)
continue;
if (record.uid > recentuid)
numrecent++;
if (internalseen) {
if (!(record.system_flags & FLAG_SEEN))
numunseen++;
}
else {
if (!seqset_ismember(seq, record.uid))
numunseen++;
}
}
/* we've calculated the correct values for both */
c_statusitems |= STATUS_RECENT | STATUS_UNSEEN;
}
statuscache_fill(sdata, userid, mailbox, c_statusitems,
numrecent, numunseen);
/* cache the new value while unlocking */
mailbox_unlock_index(mailbox, sdata);
done:
mailbox_close(&mailbox);
return r;
}
int statuscache_lookup(const char *mboxname, const char *userid,
unsigned statusitems, struct statusdata *sdata)
{
int keylen, datalen, r = 0;
const char *data = NULL, *dend;
char *p, *key = statuscache_buildkey(mboxname, userid, &keylen);
unsigned version;
/* Don't access DB if it hasn't been opened */
if (!statuscache_dbopen)
return IMAP_NO_NOSUCHMSG;
memset(sdata, 0, sizeof(struct statusdata));
/* Check if there is an entry in the database */
do {
r = DB->fetch(statuscachedb, key, keylen, &data, &datalen, NULL);
} while (r == CYRUSDB_AGAIN);
if (r || !data || ((size_t) datalen < sizeof(unsigned))) {
return IMAP_NO_NOSUCHMSG;
}
dend = data + datalen;
version = (unsigned) strtoul(data, &p, 10);
if (version != (unsigned) STATUSCACHE_VERSION) {
/* Wrong version */
return IMAP_NO_NOSUCHMSG;
}
if (p < dend) sdata->statusitems = strtoul(p, &p, 10);
if (p < dend) sdata->messages = strtoul(p, &p, 10);
if (p < dend) sdata->recent = strtoul(p, &p, 10);
if (p < dend) sdata->uidnext = strtoul(p, &p, 10);
if (p < dend) sdata->uidvalidity = strtoul(p, &p, 10);
if (p < dend) sdata->unseen = strtoul(p, &p, 10);
#ifdef HAVE_LONG_LONG_INT
if (p < dend) sdata->highestmodseq = strtoull(p, &p, 10);
#else
if (p < dend) sdata->highestmodseq = strtoul(p, &p, 10);
#endif
/* Sanity check the data */
if (!sdata->statusitems || !sdata->uidnext || !sdata->uidvalidity) {
return IMAP_NO_NOSUCHMSG;
}
if ((sdata->statusitems & statusitems) != statusitems) {
/* Don't have all of the requested information */
return IMAP_NO_NOSUCHMSG;
}
return 0;
}
static int statuscache_update_txn(const char *mboxname,
struct statusdata *sdata,
struct txn **tidptr)
{
char data[250]; /* enough room for 11*(UULONG + SP) */
int keylen, datalen;
char *key = statuscache_buildkey(mboxname, sdata->userid, &keylen);
int r;
/* Don't access DB if it hasn't been opened */
if (!statuscache_dbopen)
return 0;
/* The trailing whitespace is necessary because we
* use non-length-based functions to parse the values.
* Any non-digit char would be fine, but whitespace
* looks less ugly in dbtool output */
datalen = snprintf(data, sizeof(data),
"%u %u %u %u %u %u %u " MODSEQ_FMT " ",
STATUSCACHE_VERSION,
sdata->statusitems, sdata->messages,
sdata->recent, sdata->uidnext,
sdata->uidvalidity, sdata->unseen,
sdata->highestmodseq);
r = DB->store(statuscachedb, key, keylen, data, datalen, tidptr);
if (r != CYRUSDB_OK) {
syslog(LOG_ERR, "DBERROR: error updating database: %s (%s)",
mboxname, cyrusdb_strerror(r));
}
return r;
}
int statuscache_update(const char *mboxname, struct statusdata *sdata)
{
statuscache_update_txn(mboxname, sdata, NULL);
return 0;
}
struct statuscache_deleterock {
struct db *db;
struct txn *tid;
};
static int delete_cb(void *rockp,
const char *key, int keylen,
const char *data __attribute__((unused)),
int datalen __attribute__((unused)))
{
int r;
char buf[4096];
struct statuscache_deleterock *rp = (struct statuscache_deleterock *)rockp;
/* error if it's too big */
if (keylen > 4096)
return 1;
/* we need to cache a copy, because the delete might re-map
* the mmap space */
memcpy(buf, key, keylen);
/* Delete db entry */
r = DB->delete(rp->db, buf, keylen, &rp->tid, 1);
if (r != CYRUSDB_OK) {
syslog(LOG_ERR, "DBERROR: error deleting from database: %s",
cyrusdb_strerror(r));
}
return 0;
}
int statuscache_invalidate(const char *mboxname, struct statusdata *sdata)
{
int keylen, r;
char *key;
int doclose = 0;
struct statuscache_deleterock drock;
/* if it's disabled then skip */
if (!config_getswitch(IMAPOPT_STATUSCACHE))
return 0;
/* Open DB if it hasn't been opened */
if (!statuscache_dbopen) {
statuscache_open(NULL);
doclose = 1;
}
drock.db = statuscachedb;
drock.tid = NULL;
key = statuscache_buildkey(mboxname, /*userid*/NULL, &keylen);
r = DB->foreach(drock.db, key, keylen, NULL, delete_cb,
&drock, &drock.tid);
if (r != CYRUSDB_OK) {
syslog(LOG_ERR, "DBERROR: error invalidating: %s (%s)",
mboxname, cyrusdb_strerror(r));
}
if (!r && sdata) {
r = statuscache_update_txn(mboxname, sdata, &drock.tid);
}
if (r != CYRUSDB_OK)
DB->abort(drock.db, drock.tid);
else
DB->commit(drock.db, drock.tid);
if (doclose)
statuscache_close();
return 0;
}