Page MenuHomePhorge

annotate.c
No OneTemporary

Authored By
Unknown
Size
88 KB
Referenced Files
None
Subscribers
None

annotate.c

/* annotate.c -- Annotation manipulation 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: annotate.c,v 1.47 2010/01/06 17:01:30 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 "acl.h"
#include "assert.h"
#include "cyrusdb.h"
#include "exitcodes.h"
#include "glob.h"
#include "hash.h"
#include "imapd.h"
#include "global.h"
#include "times.h"
#include "imap/imap_err.h"
#include "mboxlist.h"
#include "util.h"
#include "xmalloc.h"
#include "ptrarray.h"
#include "xstrlcpy.h"
#include "xstrlcat.h"
#include "tok.h"
#include "quota.h"
#include "annotate.h"
#include "sync_log.h"
#define DEBUG 0
#define ANNOTATION_SCOPE_UNKNOWN (-1)
enum {
ANNOTATION_SCOPE_SERVER = 1,
ANNOTATION_SCOPE_MAILBOX = 2,
ANNOTATION_SCOPE_MESSAGE = 3
};
typedef struct annotate_entrydesc annotate_entrydesc_t;
struct annotate_entry_list
{
struct annotate_entry_list *next;
const annotate_entrydesc_t *desc;
char *name;
/* used for storing */
struct buf shared;
struct buf priv;
int have_shared;
int have_priv;
};
/* Encapsulates all the state involved in providing the scope
* for setting or getting a single annotation */
struct annotate_state
{
/*
* Common between storing and fetching
*/
int which; /* ANNOTATION_SCOPE_* */
struct mboxlist_entry *mbentry; /* for _MAILBOX */
int mbentry_is_ours;
struct mailbox *mailbox; /* for _MAILBOX, _MESSAGE */
int mailbox_is_ours;
unsigned int uid; /* for _MESSAGE */
const char *acl; /* for _MESSAGE */
annotate_db_t *d;
/* authentication state */
const char *userid;
int isadmin;
struct auth_state *auth_state;
struct annotate_entry_list *entry_list;
/* for proxies */
struct hash_table entry_table;
struct hash_table server_table;
/*
* Fetching.
*/
unsigned attribs;
struct entryattlist **entryatts;
unsigned found;
/* For proxies (a null entry_list indicates that we ONLY proxy) */
/* if these are NULL, we have had a local exact match, and we
DO NOT proxy */
const char *orig_mailbox;
const strarray_t *orig_entry;
const strarray_t *orig_attribute;
int maxsize;
int *sizeptr;
/* state for output_entryatt */
struct attvaluelist *attvalues;
char lastname[MAX_MAILBOX_BUFFER]; /* internal */
char lastentry[MAX_MAILBOX_BUFFER];
uint32_t lastuid;
annotate_fetch_cb_t callback;
void *callback_rock;
/*
* Storing.
*/
/* number of mailboxes matching the pattern */
unsigned count;
};
enum {
ATTRIB_VALUE_SHARED = (1<<0),
ATTRIB_VALUE_PRIV = (1<<1),
ATTRIB_SIZE_SHARED = (1<<2),
ATTRIB_SIZE_PRIV = (1<<3),
ATTRIB_DEPRECATED = (1<<4)
};
typedef enum {
ANNOTATION_PROXY_T_INVALID = 0,
PROXY_ONLY = 1,
BACKEND_ONLY = 2,
PROXY_AND_BACKEND = 3
} annotation_proxy_t;
enum {
ATTRIB_TYPE_STRING,
ATTRIB_TYPE_BOOLEAN,
ATTRIB_TYPE_UINT,
ATTRIB_TYPE_INT
};
#define ATTRIB_NO_FETCH_ACL_CHECK (1<<30)
struct annotate_entrydesc
{
const char *name; /* entry name */
int type; /* entry type */
annotation_proxy_t proxytype; /* mask of allowed server types */
int attribs; /* mask of allowed attributes */
int extra_rights; /* for set of shared mailbox annotations */
/* function to get the entry */
void (*get)(annotate_state_t *state,
struct annotate_entry_list *entry);
/* function to set the entry */
int (*set)(annotate_state_t *state,
struct annotate_entry_list *entry);
void *rock; /* rock passed to get() function */
};
struct annotate_db
{
annotate_db_t *next;
int refcount;
char *mboxname;
char *filename;
struct db *db;
struct txn *txn;
int in_txn;
};
struct annotate_recalc_state
{
struct mailbox *mailbox;
int reconstruct;
annotate_db_t *d; /* reference to per-mailbox db */
unsigned int num_uids;
hash_table counts_by_uid;
};
#define DB config_annotation_db
static annotate_db_t *all_dbs_head = NULL;
static annotate_db_t *all_dbs_tail = NULL;
#define tid(d) ((d)->in_txn ? &(d)->txn : NULL)
int (*proxy_fetch_func)(const char *server, const char *mbox_pat,
const strarray_t *entry_pat,
const strarray_t *attribute_pat) = NULL;
int (*proxy_store_func)(const char *server, const char *mbox_pat,
struct entryattlist *entryatts) = NULL;
static ptrarray_t message_entries = PTRARRAY_INITIALIZER;
static ptrarray_t mailbox_entries = PTRARRAY_INITIALIZER;
static ptrarray_t server_entries = PTRARRAY_INITIALIZER;
static void annotate_state_unset_scope(annotate_state_t *state);
static int annotate_state_set_scope(annotate_state_t *state,
struct mboxlist_entry *mbentry,
struct mailbox *mailbox,
unsigned int uid);
static void init_annotation_definitions(void);
static int annotation_set_tofile(annotate_state_t *state,
struct annotate_entry_list *entry);
static int annotation_set_todb(annotate_state_t *state,
struct annotate_entry_list *entry);
static int annotation_set_mailboxopt(annotate_state_t *state,
struct annotate_entry_list *entry);
static int annotation_set_pop3showafter(annotate_state_t *state,
struct annotate_entry_list *entry);
static int annotation_set_specialuse(annotate_state_t *state,
struct annotate_entry_list *entry);
static int _annotate_rewrite(struct mailbox *oldmailbox,
uint32_t olduid,
const char *olduserid,
struct mailbox *newmailbox,
uint32_t newuid,
const char *newuserid,
int copy);
static int _annotate_may_store(annotate_state_t *state,
int is_shared,
const annotate_entrydesc_t *desc);
static void annotate_begin(annotate_db_t *d);
static void annotate_abort(annotate_db_t *d);
static int annotate_commit(annotate_db_t *d);
/* String List Management */
/*
* Append 's' to the strlist 'l'.
*/
void appendstrlist(struct strlist **l, char *s)
{
struct strlist **tail = l;
while (*tail) tail = &(*tail)->next;
*tail = (struct strlist *)xmalloc(sizeof(struct strlist));
(*tail)->s = xstrdup(s);
(*tail)->p = 0;
(*tail)->next = 0;
}
/*
* Append 's' to the strlist 'l', compiling it as a pattern.
* Caller must pass in memory that is freed when the strlist is freed.
*/
void appendstrlistpat(struct strlist **l, char *s)
{
struct strlist **tail = l;
while (*tail) tail = &(*tail)->next;
*tail = (struct strlist *)xmalloc(sizeof(struct strlist));
(*tail)->s = s;
(*tail)->p = charset_compilepat(s);
(*tail)->next = 0;
}
/*
* Free the strlist 'l'
*/
void freestrlist(struct strlist *l)
{
struct strlist *n;
while (l) {
n = l->next;
free(l->s);
if (l->p) charset_freepat(l->p);
free((char *)l);
l = n;
}
}
/* Attribute Management (also used by the ID command) */
/*
* Append the 'attrib'/'value' pair to the attvaluelist 'l'.
*/
void appendattvalue(struct attvaluelist **l,
const char *attrib,
const struct buf *value)
{
struct attvaluelist **tail = l;
while (*tail) tail = &(*tail)->next;
*tail = xzmalloc(sizeof(struct attvaluelist));
(*tail)->attrib = xstrdup(attrib);
buf_copy(&(*tail)->value, value);
}
/*
* Duplicate the attvaluelist @src to @dst.
*/
void dupattvalues(struct attvaluelist **dst,
const struct attvaluelist *src)
{
for ( ; src ; src = src->next)
appendattvalue(dst, src->attrib, &src->value);
}
/*
* Free the attvaluelist 'l'
*/
void freeattvalues(struct attvaluelist *l)
{
struct attvaluelist *n;
while (l) {
n = l->next;
free(l->attrib);
buf_free(&l->value);
free(l);
l = n;
}
}
/*
* Append the 'entry'/'attvalues' pair to the entryattlist 'l'.
*/
void appendentryatt(struct entryattlist **l, const char *entry,
struct attvaluelist *attvalues)
{
struct entryattlist **tail = l;
while (*tail) tail = &(*tail)->next;
*tail = (struct entryattlist *)xmalloc(sizeof(struct entryattlist));
(*tail)->entry = xstrdup(entry);
(*tail)->attvalues = attvalues;
(*tail)->next = NULL;
}
void setentryatt(struct entryattlist **l, const char *entry,
const char *attrib, const struct buf *value)
{
struct entryattlist *ee;
for (ee = *l ; ee ; ee = ee->next) {
if (!strcmp(ee->entry, entry))
break;
}
if (!ee) {
struct attvaluelist *atts = NULL;
appendattvalue(&atts, attrib, value);
appendentryatt(l, entry, atts);
}
else {
struct attvaluelist *av;
for (av = ee->attvalues ; av ; av = av->next) {
if (!strcmp(av->attrib, attrib))
break;
}
if (av)
buf_copy(&av->value, value);
else
appendattvalue(&ee->attvalues, attrib, value);
}
}
void clearentryatt(struct entryattlist **l, const char *entry,
const char *attrib)
{
struct entryattlist *ea, **pea;
struct attvaluelist *av, **pav;
for (pea = l ; *pea ; pea = &(*pea)->next) {
if (!strcmp((*pea)->entry, entry))
break;
}
ea = *pea;
if (!ea)
return; /* entry not found */
for (pav = &(*pea)->attvalues ; *pav ; pav = &(*pav)->next) {
if (!strcmp((*pav)->attrib, attrib))
break;
}
av = *pav;
if (!av)
return; /* attrib not found */
/* detach and free attvaluelist */
*pav = av->next;
free(av->attrib);
buf_free(&av->value);
free(av);
if (!ea->attvalues) {
/* ->attvalues is now empty, so we can detach and free *pea too */
*pea = ea->next;
free(ea->entry);
free(ea);
}
}
/*
* Duplicate the entryattlist @src to @dst.
*/
void dupentryatt(struct entryattlist **dst,
const struct entryattlist *src)
{
for ( ; src ; src = src->next) {
struct attvaluelist *attvalues = NULL;
dupattvalues(&attvalues, src->attvalues);
appendentryatt(dst, src->entry, attvalues);
}
}
/*
* Count the storage used by entryattlist 'l'
*/
size_t sizeentryatts(const struct entryattlist *l)
{
size_t sz = 0;
struct attvaluelist *av;
for ( ; l ; l = l->next)
for (av = l->attvalues ; av ; av = av->next)
sz += av->value.len;
return sz;
}
/*
* Free the entryattlist 'l'
*/
void freeentryatts(struct entryattlist *l)
{
struct entryattlist *n;
while (l) {
n = l->next;
free(l->entry);
freeattvalues(l->attvalues);
free(l);
l = n;
}
}
/* must be called after cyrus_init */
void annotate_init(int (*fetch_func)(const char *, const char *,
const strarray_t *, const strarray_t *),
int (*store_func)(const char *, const char *,
struct entryattlist *))
{
if (fetch_func) {
proxy_fetch_func = fetch_func;
}
if (store_func) {
proxy_store_func = store_func;
}
init_annotation_definitions();
}
/* detach the db_t from the global list */
static void detach_db(annotate_db_t *prev, annotate_db_t *d)
{
if (prev)
prev->next = d->next;
else
all_dbs_head = d->next;
if (all_dbs_tail == d)
all_dbs_tail = prev;
}
/* append the db_t to the global list */
static void append_db(annotate_db_t *d)
{
if (all_dbs_tail)
all_dbs_tail->next = d;
else
all_dbs_head = d;
all_dbs_tail = d;
d->next = NULL;
}
/*
* Generate a new string containing the db filename
* for the given @mboxname, (or the global db if
* @mboxname is NULL). Returns the new string in
* *@fnamep. Returns an error code.
*/
static int annotate_dbname_mbentry(const struct mboxlist_entry *mbentry,
char **fnamep)
{
const char *conf_fname;
if (mbentry) {
/* per-mbox database */
conf_fname = mboxname_metapath(mbentry->partition, mbentry->name,
META_ANNOTATIONS, /*isnew*/0);
if (!conf_fname)
return IMAP_MAILBOX_BADNAME;
*fnamep = xstrdup(conf_fname);
}
else {
/* global database */
conf_fname = config_getstring(IMAPOPT_ANNOTATION_DB_PATH);
if (conf_fname)
*fnamep = xstrdup(conf_fname);
else
*fnamep = strconcat(config_dir, FNAME_GLOBALANNOTATIONS, (char *)NULL);
}
return 0;
}
static int annotate_dbname_mailbox(struct mailbox *mailbox, char **fnamep)
{
const char *conf_fname;
if (!mailbox) return annotate_dbname_mbentry(NULL, fnamep);
conf_fname = mboxname_metapath(mailbox->part, mailbox->name,
META_ANNOTATIONS, /*isnew*/0);
if (!conf_fname) return IMAP_MAILBOX_BADNAME;
*fnamep = xstrdup(conf_fname);
return 0;
}
static int annotate_dbname(const char *mboxname, char **fnamep)
{
int r = 0;
struct mboxlist_entry *mbentry = NULL;
if (mboxname) {
r = mboxlist_lookup(mboxname, &mbentry, NULL);
if (r) goto out;
}
r = annotate_dbname_mbentry(mbentry, fnamep);
out:
mboxlist_entry_free(&mbentry);
return r;
}
static int _annotate_getdb(const char *mboxname,
unsigned int uid,
int dbflags,
annotate_db_t **dbp)
{
annotate_db_t *d, *prev = NULL;
char *fname = NULL;
struct db *db;
int r;
*dbp = NULL;
/*
* The incoming (mboxname,uid) tuple tells us which scope we
* need a database for. Translate into the mboxname used to
* key annotate_db_t's, which is slightly different: message
* scope goes into a per-mailbox db, others in the global db.
*/
if (!strcmpsafe(mboxname, NULL) /*server scope*/ ||
!uid /* mailbox scope*/)
mboxname = NULL;
/* try to find an existing db for the mbox */
for (d = all_dbs_head ; d ; prev = d, d = d->next) {
if (!strcmpsafe(mboxname, d->mboxname)) {
/* found it, bump the refcount */
d->refcount++;
*dbp = d;
/*
* Splay the db_t to the end of the global list.
* This ensures the list remains in getdb() call
* order, and in particular that the dbs are
* committed in getdb() call order. This is
* necessary to ensure safety should a commit fail
* while moving annotations between per-mailbox dbs
*/
detach_db(prev, d);
append_db(d);
return 0;
}
}
/* not found, open/create a new one */
r = annotate_dbname(mboxname, &fname);
if (r)
goto error;
#if DEBUG
syslog(LOG_ERR, "Opening annotations db %s\n", fname);
#endif
r = cyrusdb_open(DB, fname, dbflags, &db);
if (r != 0) {
if (!(dbflags & CYRUSDB_CREATE) && r == CYRUSDB_NOTFOUND)
goto error;
syslog(LOG_ERR, "DBERROR: opening %s: %s",
fname, cyrusdb_strerror(r));
goto error;
}
/* record all the above */
d = xzmalloc(sizeof(*d));
d->refcount = 1;
d->mboxname = xstrdupnull(mboxname);
d->filename = fname;
d->db = db;
append_db(d);
*dbp = d;
return 0;
error:
free(fname);
*dbp = NULL;
return r;
}
int annotate_getdb(const char *mboxname, annotate_db_t **dbp)
{
if (!mboxname || !*mboxname)
return IMAP_INTERNAL; /* we don't return the global db */
/* synthetic UID '1' forces per-mailbox mode */
return _annotate_getdb(mboxname, 1, CYRUSDB_CREATE, dbp);
}
static void annotate_closedb(annotate_db_t *d)
{
annotate_db_t *dx, *prev = NULL;
int r;
/* detach from the global list */
for (dx = all_dbs_head ; dx && dx != d ; prev = dx, dx = dx->next)
;
assert(dx);
assert(d == dx);
detach_db(prev, d);
#if DEBUG
syslog(LOG_ERR, "Closing annotations db %s\n", d->filename);
#endif
r = cyrusdb_close(d->db);
if (r)
syslog(LOG_ERR, "DBERROR: error closing annotations %s: %s",
d->filename, cyrusdb_strerror(r));
free(d->filename);
free(d->mboxname);
memset(d, 0, sizeof(*d)); /* JIC */
free(d);
}
void annotate_putdb(annotate_db_t **dbp)
{
annotate_db_t *d;
if (!dbp || !(d = *dbp))
return;
assert(d->refcount > 0);
if (--d->refcount == 0) {
if (d->in_txn && d->txn) {
syslog(LOG_ERR, "IOERROR: dropped last reference on "
"database %s with uncommitted updates, "
"aborting - DATA LOST!",
d->filename);
annotate_abort(d);
}
assert(!d->in_txn);
annotate_closedb(d);
}
*dbp = NULL;
}
void annotatemore_open(void)
{
int r;
annotate_db_t *d = NULL;
/* force opening the global annotations db */
r = _annotate_getdb(NULL, 0, CYRUSDB_CREATE, &d);
if (r)
fatal("can't open global annotations database", EC_TEMPFAIL);
}
void annotatemore_close(void)
{
/* close all the open databases */
while (all_dbs_head)
annotate_closedb(all_dbs_head);
}
/* Begin a txn if one is not already started. Can be called multiple
* times */
static void annotate_begin(annotate_db_t *d)
{
if (d)
d->in_txn = 1;
}
static void annotate_abort(annotate_db_t *d)
{
/* don't double-abort */
if (!d || !d->in_txn) return;
if (d->txn) {
#if DEBUG
syslog(LOG_ERR, "Aborting annotations db %s\n", d->filename);
#endif
cyrusdb_abort(d->db, d->txn);
}
d->txn = NULL;
d->in_txn = 0;
}
static int annotate_commit(annotate_db_t *d)
{
int r = 0;
/* silently succeed if not in a txn */
if (!d || !d->in_txn) return 0;
if (d->txn) {
#if DEBUG
syslog(LOG_ERR, "Committing annotations db %s\n", d->filename);
#endif
r = cyrusdb_commit(d->db, d->txn);
if (r)
r = IMAP_IOERROR;
d->txn = NULL;
}
d->in_txn = 0;
return r;
}
void annotate_done(void)
{
/* DB->done() handled by cyrus_done() */
}
static int make_key(const char *mboxname,
unsigned int uid,
const char *entry,
const char *userid,
char *key, size_t keysize)
{
int keylen = 0;
if (!uid) {
strlcpy(key+keylen, mboxname, keysize-keylen);
keylen += strlen(mboxname) + 1;
}
else if (uid == ANNOTATE_ANY_UID) {
strlcpy(key+keylen, "*", keysize-keylen);
keylen += strlen(key+keylen) + 1;
}
else {
snprintf(key+keylen, keysize-keylen, "%u", uid);
keylen += strlen(key+keylen) + 1;
}
strlcpy(key+keylen, entry, keysize-keylen);
keylen += strlen(entry);
/* if we don't have a userid, we're doing a foreach() */
if (userid) {
keylen++;
strlcpy(key+keylen, userid, keysize-keylen);
keylen += strlen(userid) + 1;
}
return keylen;
}
static int split_key(const annotate_db_t *d,
const char *key, int keysize,
const char **mboxnamep,
unsigned int *uidp,
const char **entryp,
const char **useridp)
{
#define NFIELDS 3
const char *fields[NFIELDS];
int nfields = 0;
const char *p;
unsigned int uid = 0;
const char *mboxname = "";
/* paranoia: ensure the last character in the key is
* a NUL, which it should be because of the way we
* always build keys */
if (key[keysize-1])
return IMAP_ANNOTATION_BADENTRY;
keysize--;
/*
* paranoia: split the key into fields on NUL characters.
* We would use strarray_nsplit() for this, except that
* by design that function cannot split on NULs and does
* not handle embedded NULs.
*/
fields[nfields++] = key;
for (p = key ; (p-key) < keysize ; p++) {
if (!*p) {
if (nfields == NFIELDS)
return IMAP_ANNOTATION_BADENTRY;
fields[nfields++] = p+1;
}
}
if (nfields != NFIELDS)
return IMAP_ANNOTATION_BADENTRY;
if (d->mboxname) {
/* per-folder db for message scope annotations */
char *end = NULL;
uid = strtoul(fields[0], &end, 10);
if (uid == 0 || end == NULL || *end)
return IMAP_ANNOTATION_BADENTRY;
mboxname = d->mboxname;
}
else {
/* global db for mailnbox & server scope annotations */
uid = 0;
mboxname = fields[0];
}
if (mboxnamep) *mboxnamep = mboxname;
if (uidp) *uidp = uid;
if (entryp) *entryp = fields[1];
if (useridp) *useridp = fields[2];
return 0;
#undef NFIELDS
}
#if DEBUG
static const char *key_as_string(const annotate_db_t *d,
const char *key, int keylen)
{
const char *mboxname, *entry, *userid;
unsigned int uid;
int r;
static struct buf buf = BUF_INITIALIZER;
buf_reset(&buf);
r = split_key(d, key, keylen, &mboxname, &uid, &entry, &userid);
if (r)
buf_appendcstr(&buf, "invalid");
else
buf_printf(&buf, "{ mboxname=\"%s\" uid=%u entry=\"%s\" userid=\"%s\" }",
mboxname, uid, entry, userid);
return buf_cstring(&buf);
}
#endif
static int split_attribs(const char *data, int datalen __attribute__((unused)),
struct buf *value)
{
unsigned long tmp; /* for alignment */
/* xxx use datalen? */
/* xxx sanity check the data? */
/*
* Sigh...this is dumb. We take care to be machine independent by
* storing the length in network byte order...but the size of the
* length field depends on whether we're running on a 32b or 64b
* platform.
*/
memcpy(&tmp, data, sizeof(unsigned long));
data += sizeof(unsigned long); /* skip to value */
buf_init_ro(value, data, ntohl(tmp));
/*
* In records written by older versions of Cyrus, there will be
* binary encoded content-type and modifiedsince values after the
* data. We don't care about those anymore, so we just ignore them.
*/
return 0;
}
struct find_rock {
struct glob *mglob;
struct glob *eglob;
unsigned int uid;
annotate_db_t *d;
annotatemore_find_proc_t proc;
void *rock;
};
static int find_p(void *rock, const char *key, size_t keylen,
const char *data __attribute__((unused)),
size_t datalen __attribute__((unused)))
{
struct find_rock *frock = (struct find_rock *) rock;
const char *mboxname, *entry, *userid;
unsigned int uid;
int r;
r = split_key(frock->d, key, keylen, &mboxname,
&uid, &entry, &userid);
if (r < 0)
return 0;
if (frock->uid &&
frock->uid != ANNOTATE_ANY_UID &&
frock->uid != uid)
return 0;
if (GLOB_TEST(frock->mglob, mboxname) == -1)
return 0;
if (GLOB_TEST(frock->eglob, entry) == -1)
return 0;
return 1;
}
static int find_cb(void *rock, const char *key, size_t keylen,
const char *data, size_t datalen)
{
struct find_rock *frock = (struct find_rock *) rock;
const char *mboxname, *entry, *userid;
unsigned int uid;
struct buf value = BUF_INITIALIZER;
char keycopy[MAX_MAILBOX_PATH+1];
int r;
assert(keylen < MAX_MAILBOX_PATH);
/* take a copy, we may be deleting this record, so the key
* pointer will no longer be valid */
memcpy(keycopy, key, keylen);
#if DEBUG
syslog(LOG_ERR, "find_cb: found key %s in %s",
key_as_string(frock->d, keycopy, keylen), frock->d->filename);
#endif
r = split_key(frock->d, keycopy, keylen, &mboxname,
&uid, &entry, &userid);
if (r)
return r;
r = split_attribs(data, datalen, &value);
if (!r) r = frock->proc(mboxname, uid, entry, userid, &value, frock->rock);
return r;
}
int annotatemore_findall(const char *mboxname, /* internal */
unsigned int uid,
const char *entry,
annotatemore_find_proc_t proc,
void *rock)
{
char key[MAX_MAILBOX_PATH+1], *p;
size_t keylen;
int r;
struct find_rock frock;
assert(mboxname);
assert(entry);
frock.mglob = glob_init(mboxname, GLOB_HIERARCHY);
frock.eglob = glob_init(entry, GLOB_HIERARCHY);
GLOB_SET_SEPARATOR(frock.eglob, '/');
frock.uid = uid;
frock.proc = proc;
frock.rock = rock;
r = _annotate_getdb(mboxname, uid, 0, &frock.d);
if (r) {
if (r == CYRUSDB_NOTFOUND)
r = 0;
goto out;
}
/* Find fixed-string pattern prefix */
keylen = make_key(mboxname, uid,
entry, NULL, key, sizeof(key));
for (p = key; keylen; p++, keylen--) {
if (*p == '*' || *p == '%') break;
}
keylen = p - key;
r = cyrusdb_foreach(frock.d->db, key, keylen, &find_p, &find_cb,
&frock, tid(frock.d));
out:
glob_free(&frock.mglob);
glob_free(&frock.eglob);
annotate_putdb(&frock.d);
return r;
}
/*************************** Annotate State Management ***************************/
annotate_state_t *annotate_state_new(void)
{
annotate_state_t *state;
state = xzmalloc(sizeof(*state));
state->which = ANNOTATION_SCOPE_UNKNOWN;
return state;
}
static void annotate_state_start(annotate_state_t *state)
{
/* xxx better way to determine a size for this table? */
construct_hash_table(&state->entry_table, 100, 1);
construct_hash_table(&state->server_table, 10, 1);
}
static void annotate_state_finish(annotate_state_t *state)
{
/* Free the entry list */
while (state->entry_list) {
struct annotate_entry_list *ee = state->entry_list;
state->entry_list = ee->next;
buf_free(&ee->shared);
buf_free(&ee->priv);
free(ee->name);
free(ee);
}
free_hash_table(&state->entry_table, NULL);
free_hash_table(&state->server_table, NULL);
}
static void annotate_state_free(annotate_state_t **statep)
{
annotate_state_t *state = *statep;
if (!state)
return;
annotate_state_finish(state);
annotate_state_unset_scope(state);
free(state);
*statep = NULL;
}
void annotate_state_abort(annotate_state_t **statep)
{
if (*statep)
annotate_abort((*statep)->d);
annotate_state_free(statep);
}
int annotate_state_commit(annotate_state_t **statep)
{
int r = 0;
if (*statep)
r = annotate_commit((*statep)->d);
annotate_state_free(statep);
return r;
}
static struct annotate_entry_list *
_annotate_state_add_entry(annotate_state_t *state,
const annotate_entrydesc_t *desc,
const char *name)
{
struct annotate_entry_list *ee;
ee = xzmalloc(sizeof(*ee));
ee->name = xstrdup(name);
ee->desc = desc;
ee->next = state->entry_list;
state->entry_list = ee;
return ee;
}
void annotate_state_set_auth(annotate_state_t *state,
int isadmin, const char *userid,
struct auth_state *auth_state)
{
/* Note: lmtpd sometimes calls through the append code with
* auth_state=NULL, so we cannot rely on it being non-NULL */
state->userid = userid;
state->isadmin = isadmin;
state->auth_state = auth_state;
}
int annotate_state_set_server(annotate_state_t *state)
{
return annotate_state_set_scope(state, NULL, NULL, 0);
}
int annotate_state_set_mailbox(annotate_state_t *state,
struct mailbox *mailbox)
{
return annotate_state_set_scope(state, NULL, mailbox, 0);
}
int annotate_state_set_mailbox_mbe(annotate_state_t *state,
struct mboxlist_entry *mbentry)
{
return annotate_state_set_scope(state, mbentry, NULL, 0);
}
int annotate_state_set_message(annotate_state_t *state,
struct mailbox *mailbox,
unsigned int uid)
{
return annotate_state_set_scope(state, NULL, mailbox, uid);
}
/* unset any state from a previous scope */
static void annotate_state_unset_scope(annotate_state_t *state)
{
if (state->mailbox && state->mailbox_is_ours)
mailbox_close(&state->mailbox);
state->mailbox = NULL;
state->mailbox_is_ours = 0;
if (state->mbentry && state->mbentry_is_ours)
mboxlist_entry_free(&state->mbentry);
state->mbentry = NULL;
state->mbentry_is_ours = 0;
state->uid = 0;
state->which = ANNOTATION_SCOPE_UNKNOWN;
annotate_putdb(&state->d);
}
static int annotate_state_set_scope(annotate_state_t *state,
struct mboxlist_entry *mbentry,
struct mailbox *mailbox,
unsigned int uid)
{
int r = 0;
annotate_db_t *oldd = NULL;
int oldwhich = state->which;
/* Carefully preserve the reference on the old DB just in case it
* turns out to be the same as the new DB, so we avoid the overhead
* of an unnecessary cyrusdb_open/close pair. */
oldd = state->d;
state->d = NULL;
annotate_state_unset_scope(state);
if (mbentry) {
if (!mailbox && !mbentry->server) {
/* local mailbox */
r = mailbox_open_iwl(mbentry->name, &mailbox);
if (r)
goto out;
state->mailbox_is_ours = 1;
}
assert(mailbox);
state->mbentry = mbentry;
}
if (uid) {
assert(mailbox);
state->which = ANNOTATION_SCOPE_MESSAGE;
}
else if (mailbox) {
assert(!uid);
state->which = ANNOTATION_SCOPE_MAILBOX;
}
else {
assert(!mailbox);
assert(!uid);
state->which = ANNOTATION_SCOPE_SERVER;
}
assert(oldwhich == ANNOTATION_SCOPE_UNKNOWN ||
oldwhich == state->which);
state->mailbox = mailbox;
state->uid = uid;
r = _annotate_getdb(mailbox ? mailbox->name : NULL, uid,
CYRUSDB_CREATE, &state->d);
out:
annotate_putdb(&oldd);
return r;
}
static int annotate_state_need_mbentry(annotate_state_t *state)
{
int r = 0;
if (!state->mbentry && state->mailbox) {
r = mboxlist_lookup(state->mailbox->name, &state->mbentry, NULL);
if (r) {
syslog(LOG_ERR, "Failed to lookup mbentry for %s: %s",
state->mailbox->name, error_message(r));
goto out;
}
state->mbentry_is_ours = 1;
}
out:
return r;
}
/*************************** Annotation Fetching ***************************/
static void flush_entryatt(annotate_state_t *state)
{
if (!state->attvalues)
return; /* nothing to flush */
state->callback(state->lastname,
state->lastuid,
state->lastentry,
state->attvalues,
state->callback_rock);
freeattvalues(state->attvalues);
state->attvalues = NULL;
}
/* Output a single entry and attributes for a single mailbox.
* Shared and private annotations are output together by caching
* the attributes until the mailbox and/or entry changes.
*
* The cache is reset by calling with a NULL mboxname or entry.
* The last entry is flushed by calling with a NULL attrib.
*/
static void output_entryatt(annotate_state_t *state, const char *entry,
const char *userid, const struct buf *value)
{
const char *mboxname;
char key[MAX_MAILBOX_BUFFER]; /* XXX MAX_MAILBOX_NAME + entry + userid */
struct buf buf = BUF_INITIALIZER;
int vallen;
/* We don't put any funny interpretations on NULL values for
* some of these anymore, now that the dirty hacks are gone. */
assert(state);
assert(entry);
assert(userid);
assert(value);
if (state->mailbox)
mboxname = state->mailbox->name;
else
mboxname = "";
/* @mboxname is now an internal mailbox name */
/* Check if this is a new entry.
* If so, flush our current entry.
*/
if (state->uid != state->lastuid ||
strcmp(mboxname, state->lastname) ||
strcmp(entry, state->lastentry))
flush_entryatt(state);
strlcpy(state->lastname, mboxname, sizeof(state->lastname));
strlcpy(state->lastentry, entry, sizeof(state->lastentry));
state->lastuid = state->uid;
/* check if we already returned this entry */
strlcpy(key, mboxname, sizeof(key));
if (state->uid) {
char uidbuf[32];
snprintf(uidbuf, sizeof(uidbuf), "/UID%u/", state->uid);
strlcat(key, uidbuf, sizeof(key));
}
strlcat(key, entry, sizeof(key));
strlcat(key, userid, sizeof(key));
if (hash_lookup(key, &state->entry_table)) return;
hash_insert(key, (void *)0xDEADBEEF, &state->entry_table);
vallen = value->len;
if (state->sizeptr && state->maxsize < vallen) {
/* too big - track the size of the largest */
int *sp = state->sizeptr;
if (*sp < vallen) *sp = vallen;
return;
}
if (!userid[0]) { /* shared annotation */
if ((state->attribs & ATTRIB_VALUE_SHARED)) {
appendattvalue(&state->attvalues, "value.shared", value);
state->found |= ATTRIB_VALUE_SHARED;
}
if ((state->attribs & ATTRIB_SIZE_SHARED)) {
buf_reset(&buf);
buf_printf(&buf, "%u", value->len);
appendattvalue(&state->attvalues, "size.shared", &buf);
state->found |= ATTRIB_SIZE_SHARED;
}
}
else { /* private annotation */
if ((state->attribs & ATTRIB_VALUE_PRIV)) {
appendattvalue(&state->attvalues, "value.priv", value);
state->found |= ATTRIB_VALUE_PRIV;
}
if ((state->attribs & ATTRIB_SIZE_PRIV)) {
buf_reset(&buf);
buf_printf(&buf, "%u", value->len);
appendattvalue(&state->attvalues, "size.priv", &buf);
state->found |= ATTRIB_SIZE_PRIV;
}
}
buf_free(&buf);
}
/* Note that unlike store access control, fetch access control
* is identical between shared and private annotations */
static int _annotate_may_fetch(annotate_state_t *state,
const annotate_entrydesc_t *desc)
{
unsigned int my_rights;
unsigned int needed = 0;
const char *acl = NULL;
/* Admins can do anything */
if (state->isadmin)
return 1;
/* Some entries need to do their own access control */
if ((desc->type & ATTRIB_NO_FETCH_ACL_CHECK))
return 1;
if (state->which == ANNOTATION_SCOPE_SERVER) {
/* RFC5464 doesn't mention access control for server
* annotations, but this seems a sensible practice and is
* consistent with past Cyrus behaviour */
return 1;
}
else if (state->which == ANNOTATION_SCOPE_MAILBOX) {
assert(state->mailbox);
/* Make sure its a local mailbox annotation */
if (state->mbentry && state->mbentry->server)
return 0;
acl = state->mailbox->acl;
/* RFC5464 is a trifle vague about access control for mailbox
* annotations but this seems to be compliant */
needed = ACL_LOOKUP|ACL_READ;
/* fall through to ACL check */
}
else if (state->which == ANNOTATION_SCOPE_MESSAGE) {
assert(state->mailbox);
acl = state->mailbox->acl;
/* RFC5257: reading from a private annotation needs 'r'.
* Reading from a shared annotation needs 'r' */
needed = ACL_READ;
/* fall through to ACL check */
}
if (!acl)
return 0;
my_rights = cyrus_acl_myrights(state->auth_state, acl);
return ((my_rights & needed) == needed);
}
static void annotation_get_fromfile(annotate_state_t *state,
struct annotate_entry_list *entry)
{
const char *filename = (const char *) entry->desc->rock;
char path[MAX_MAILBOX_PATH+1];
struct buf value = BUF_INITIALIZER;
FILE *f;
snprintf(path, sizeof(path), "%s/msg/%s", config_dir, filename);
if ((f = fopen(path, "r")) && buf_getline(&value, f)) {
/* TODO: we need a buf_chomp() */
if (value.s[value.len-1] == '\r')
buf_truncate(&value, value.len-1);
}
if (f) fclose(f);
output_entryatt(state, entry->name, "", &value);
buf_free(&value);
}
static void annotation_get_freespace(annotate_state_t *state,
struct annotate_entry_list *entry)
{
unsigned long tavail;
struct buf value = BUF_INITIALIZER;
(void) find_free_partition(&tavail);
buf_printf(&value, "%lu", tavail);
output_entryatt(state, entry->name, "", &value);
buf_free(&value);
}
static void annotation_get_server(annotate_state_t *state,
struct annotate_entry_list *entry)
{
struct buf value = BUF_INITIALIZER;
int r;
assert(state);
assert(state->which == ANNOTATION_SCOPE_MAILBOX);
r = annotate_state_need_mbentry(state);
assert(r == 0);
/* Check ACL */
/* Note that we use a weaker form of access control than
* normal - we only check for ACL_LOOKUP and we don't refuse
* access if the mailbox is not local */
if (!state->mbentry->acl ||
!(cyrus_acl_myrights(state->auth_state, state->mbentry->acl) & ACL_LOOKUP))
goto out;
if (state->mbentry->server)
buf_appendcstr(&value, state->mbentry->server);
output_entryatt(state, entry->name, "", &value);
out:
buf_free(&value);
}
static void annotation_get_partition(annotate_state_t *state,
struct annotate_entry_list *entry)
{
struct buf value = BUF_INITIALIZER;
int r;
assert(state);
assert(state->which == ANNOTATION_SCOPE_MAILBOX);
r = annotate_state_need_mbentry(state);
assert(r == 0);
/* Check ACL */
if (!state->mbentry->acl ||
!(cyrus_acl_myrights(state->auth_state, state->mbentry->acl) & ACL_LOOKUP))
goto out;
if (!state->mbentry->server)
buf_appendcstr(&value, state->mbentry->partition);
output_entryatt(state, entry->name, "", &value);
out:
buf_free(&value);
}
static void annotation_get_size(annotate_state_t *state,
struct annotate_entry_list *entry)
{
struct mailbox *mailbox = state->mailbox;
struct buf value = BUF_INITIALIZER;
assert(mailbox);
buf_printf(&value, QUOTA_T_FMT, mailbox->i.quota_mailbox_used);
output_entryatt(state, entry->name, "", &value);
buf_free(&value);
}
static void annotation_get_lastupdate(annotate_state_t *state,
struct annotate_entry_list *entry)
{
struct stat sbuf;
char valuebuf[RFC3501_DATETIME_MAX+1];
struct buf value = BUF_INITIALIZER;
char *fname;
int r;
r = annotate_state_need_mbentry(state);
if (r)
goto out;
fname = mboxname_metapath(state->mbentry->partition,
state->mbentry->name, META_INDEX, 0);
if (!fname)
goto out;
if (stat(fname, &sbuf) == -1)
goto out;
time_to_rfc3501(sbuf.st_mtime, valuebuf, sizeof(valuebuf));
buf_appendcstr(&value, valuebuf);
output_entryatt(state, entry->name, "", &value);
out:
buf_free(&value);
}
static void annotation_get_lastpop(annotate_state_t *state,
struct annotate_entry_list *entry)
{
struct mailbox *mailbox = state->mailbox;
char valuebuf[RFC3501_DATETIME_MAX+1];
struct buf value = BUF_INITIALIZER;
assert(mailbox);
if (mailbox->i.pop3_last_login) {
time_to_rfc3501(mailbox->i.pop3_last_login, valuebuf,
sizeof(valuebuf));
buf_appendcstr(&value, valuebuf);
}
output_entryatt(state, entry->name, "", &value);
buf_free(&value);
}
static void annotation_get_mailboxopt(annotate_state_t *state,
struct annotate_entry_list *entry)
{
struct mailbox *mailbox = state->mailbox;
uint32_t flag = (unsigned long)entry->desc->rock;
struct buf value = BUF_INITIALIZER;
assert(mailbox);
buf_appendcstr(&value,
(mailbox->i.options & flag ? "true" : "false"));
output_entryatt(state, entry->name, "", &value);
buf_free(&value);
}
static void annotation_get_pop3showafter(annotate_state_t *state,
struct annotate_entry_list *entry)
{
struct mailbox *mailbox = state->mailbox;
char valuebuf[RFC3501_DATETIME_MAX+1];
struct buf value = BUF_INITIALIZER;
assert(mailbox);
if (mailbox->i.pop3_show_after)
{
time_to_rfc3501(mailbox->i.pop3_show_after, valuebuf, sizeof(valuebuf));
buf_appendcstr(&value, valuebuf);
}
output_entryatt(state, entry->name, "", &value);
buf_free(&value);
}
static void annotation_get_specialuse(annotate_state_t *state,
struct annotate_entry_list *entry)
{
struct buf value = BUF_INITIALIZER;
assert(state->mailbox);
if (state->mailbox->specialuse)
buf_appendcstr(&value, state->mailbox->specialuse);
output_entryatt(state, entry->name, state->userid, &value);
buf_free(&value);
}
static void annotation_get_uniqueid(annotate_state_t *state,
struct annotate_entry_list *entry)
{
struct buf value = BUF_INITIALIZER;
assert(state->mailbox);
if (state->mailbox->uniqueid)
buf_appendcstr(&value, state->mailbox->uniqueid);
output_entryatt(state, entry->name, "", &value);
buf_free(&value);
}
static int rw_cb(const char *mailbox __attribute__((unused)),
uint32_t uid __attribute__((unused)),
const char *entry, const char *userid,
const struct buf *value, void *rock)
{
annotate_state_t *state = (annotate_state_t *)rock;
if (!userid[0] || !strcmp(userid, state->userid)) {
output_entryatt(state, entry, userid, value);
}
return 0;
}
static void annotation_get_fromdb(annotate_state_t *state,
struct annotate_entry_list *entry)
{
const char *mboxname = (state->mailbox ? state->mailbox->name : "");
state->found = 0;
annotatemore_findall(mboxname, state->uid, entry->name, &rw_cb, state);
if (state->found != state->attribs &&
(!strchr(entry->name, '%') && !strchr(entry->name, '*'))) {
/* some results not found for an explicitly specified entry,
* make sure we emit explicit NILs */
struct buf empty = BUF_INITIALIZER;
if (!(state->found & (ATTRIB_VALUE_PRIV|ATTRIB_SIZE_PRIV)) &&
(state->attribs & (ATTRIB_VALUE_PRIV|ATTRIB_SIZE_PRIV))) {
/* store up value.priv and/or size.priv */
output_entryatt(state, entry->name, state->userid, &empty);
}
if (!(state->found & (ATTRIB_VALUE_SHARED|ATTRIB_SIZE_SHARED)) &&
(state->attribs & (ATTRIB_VALUE_SHARED|ATTRIB_SIZE_SHARED))) {
/* store up value.shared and/or size.shared */
output_entryatt(state, entry->name, "", &empty);
}
/* flush any stored attribute-value pairs */
flush_entryatt(state);
}
}
/* TODO: need to handle /<section-part>/ somehow */
static const annotate_entrydesc_t message_builtin_entries[] =
{
{
/* RFC5257 defines /altsubject with both .shared & .priv */
"/altsubject",
ATTRIB_TYPE_STRING,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED | ATTRIB_VALUE_PRIV,
0,
annotation_get_fromdb,
annotation_set_todb,
NULL
},{
/* RFC5257 defines /comment with both .shared & .priv */
"/comment",
ATTRIB_TYPE_STRING,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED | ATTRIB_VALUE_PRIV,
0,
annotation_get_fromdb,
annotation_set_todb,
NULL
},{ NULL, 0, ANNOTATION_PROXY_T_INVALID, 0, 0, NULL, NULL, NULL }
};
static const annotate_entrydesc_t message_db_entry =
{
NULL,
ATTRIB_TYPE_STRING,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED | ATTRIB_VALUE_PRIV,
0,
annotation_get_fromdb,
annotation_set_todb,
NULL
};
static const annotate_entrydesc_t mailbox_builtin_entries[] =
{
{
/*
* This entry was defined in the early ANNOTATMORE drafts but
* disappeared as of draft 13 and didn't make it into the final
* RFC. We keep it around because it's not too hard to
* implement.
*/
"/check",
ATTRIB_TYPE_BOOLEAN,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED | ATTRIB_VALUE_PRIV,
0,
annotation_get_fromdb,
annotation_set_todb,
NULL
},{
/*
* This entry was defined in the early ANNOTATMORE drafts but
* disappeared as of draft 13 and didn't make it into the final
* RFC. We keep it around because it's not too hard to
* implement.
*/
"/checkperiod",
ATTRIB_TYPE_UINT,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED | ATTRIB_VALUE_PRIV,
0,
annotation_get_fromdb,
annotation_set_todb,
NULL
},{
/* RFC5464 defines /shared/comment and /private/comment */
"/comment",
ATTRIB_TYPE_STRING,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED | ATTRIB_VALUE_PRIV,
0,
annotation_get_fromdb,
annotation_set_todb,
NULL
},{
/*
* This entry was defined in the early ANNOTATMORE drafts but
* disappeared as of draft 13 and didn't make it into the final
* RFC. We keep it around because it's not too hard to
* implement, even though we don't check the format.
*/
"/sort",
ATTRIB_TYPE_STRING,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED | ATTRIB_VALUE_PRIV,
0,
annotation_get_fromdb,
annotation_set_todb,
NULL
},{
/*
* RFC6154 defines /private/specialuse. We incorrectly
* implement /shared semantics, as defined in the drafts but not
* the final RFC, by historical accident.
*/
"/specialuse",
ATTRIB_TYPE_STRING,
BACKEND_ONLY,
ATTRIB_VALUE_PRIV,
0,
annotation_get_specialuse,
annotation_set_specialuse,
NULL
},{
/*
* This entry was defined in the early ANNOTATMORE drafts but
* disappeared as of draft 13 and didn't make it into the final
* RFC. We keep it around because it's not too hard to
* implement, even though we don't check the format.
*/
"/thread",
ATTRIB_TYPE_STRING,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED | ATTRIB_VALUE_PRIV,
0,
annotation_get_fromdb,
annotation_set_todb,
NULL
},{
"/vendor/cmu/cyrus-imapd/duplicatedeliver",
ATTRIB_TYPE_BOOLEAN,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED,
0,
annotation_get_mailboxopt,
annotation_set_mailboxopt,
(void *)OPT_IMAP_DUPDELIVER
},{
"/vendor/cmu/cyrus-imapd/expire",
ATTRIB_TYPE_UINT,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED,
ACL_ADMIN,
annotation_get_fromdb,
annotation_set_todb,
NULL
},{
"/vendor/cmu/cyrus-imapd/lastpop",
ATTRIB_TYPE_STRING,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED,
0,
annotation_get_lastpop,
/*set*/NULL,
NULL
},{
"/vendor/cmu/cyrus-imapd/lastupdate",
ATTRIB_TYPE_STRING,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED,
0,
annotation_get_lastupdate,
/*set*/NULL,
NULL
},{
"/vendor/cmu/cyrus-imapd/news2mail",
ATTRIB_TYPE_STRING,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED,
ACL_ADMIN,
annotation_get_fromdb,
annotation_set_todb,
NULL
},{
"/vendor/cmu/cyrus-imapd/partition",
/* _get_partition does its own access control check */
ATTRIB_TYPE_STRING | ATTRIB_NO_FETCH_ACL_CHECK,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED,
0,
annotation_get_partition,
/*set*/NULL,
NULL
},{
"/vendor/cmu/cyrus-imapd/pop3newuidl",
ATTRIB_TYPE_BOOLEAN,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED,
0,
annotation_get_mailboxopt,
annotation_set_mailboxopt,
(void *)OPT_POP3_NEW_UIDL
},{
"/vendor/cmu/cyrus-imapd/pop3showafter",
ATTRIB_TYPE_STRING,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED,
0,
annotation_get_pop3showafter,
annotation_set_pop3showafter,
NULL
},{
"/vendor/cmu/cyrus-imapd/server",
/* _get_server does its own access control check */
ATTRIB_TYPE_STRING | ATTRIB_NO_FETCH_ACL_CHECK,
PROXY_ONLY,
ATTRIB_VALUE_SHARED,
0,
annotation_get_server,
/*set*/NULL,
NULL
},{
"/vendor/cmu/cyrus-imapd/sharedseen",
ATTRIB_TYPE_BOOLEAN,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED,
0,
annotation_get_mailboxopt,
annotation_set_mailboxopt,
(void *)OPT_IMAP_SHAREDSEEN
},{
"/vendor/cmu/cyrus-imapd/sieve",
ATTRIB_TYPE_STRING,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED,
ACL_ADMIN,
annotation_get_fromdb,
annotation_set_todb,
NULL
},{
"/vendor/cmu/cyrus-imapd/size",
ATTRIB_TYPE_STRING,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED,
0,
annotation_get_size,
/*set*/NULL,
NULL
},{
"/vendor/cmu/cyrus-imapd/squat",
ATTRIB_TYPE_BOOLEAN,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED,
ACL_ADMIN,
annotation_get_fromdb,
annotation_set_todb,
NULL
},{
"/vendor/cmu/cyrus-imapd/uniqueid",
ATTRIB_TYPE_STRING,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED,
0,
annotation_get_uniqueid,
NULL,
NULL
},{ NULL, 0, ANNOTATION_PROXY_T_INVALID, 0, 0, NULL, NULL, NULL }
};
static const annotate_entrydesc_t mailbox_db_entry =
{
NULL,
ATTRIB_TYPE_STRING,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED | ATTRIB_VALUE_PRIV,
0,
annotation_get_fromdb,
annotation_set_todb,
NULL
};
static const annotate_entrydesc_t server_builtin_entries[] =
{
{
/* RFC5464 defines /shared/admin. */
"/admin",
ATTRIB_TYPE_STRING,
PROXY_AND_BACKEND,
ATTRIB_VALUE_SHARED,
ACL_ADMIN,
annotation_get_fromdb,
annotation_set_todb,
NULL
},{
/* RFC5464 defines /shared/comment. */
"/comment",
ATTRIB_TYPE_STRING,
PROXY_AND_BACKEND,
ATTRIB_VALUE_SHARED | ATTRIB_VALUE_PRIV,
ACL_ADMIN,
annotation_get_fromdb,
annotation_set_todb,
NULL
},{
/*
* This entry was defined in the early ANNOTATMORE drafts but
* disappeared as of draft 13 and didn't make it into the final
* RFC. We keep it around because it's not too hard to
* implement.
*/
"/motd",
ATTRIB_TYPE_STRING,
PROXY_AND_BACKEND,
ATTRIB_VALUE_SHARED,
0,
annotation_get_fromfile,
annotation_set_tofile,
(void *)"motd"
},{
"/vendor/cmu/cyrus-imapd/expire",
ATTRIB_TYPE_UINT,
PROXY_AND_BACKEND,
ATTRIB_VALUE_SHARED,
ACL_ADMIN,
annotation_get_fromdb,
annotation_set_todb,
NULL
},{
"/vendor/cmu/cyrus-imapd/freespace",
ATTRIB_TYPE_STRING,
BACKEND_ONLY,
ATTRIB_VALUE_SHARED,
0,
annotation_get_freespace,
/*set*/NULL,
NULL
},{
"/vendor/cmu/cyrus-imapd/shutdown",
ATTRIB_TYPE_STRING,
PROXY_AND_BACKEND,
ATTRIB_VALUE_SHARED,
0,
annotation_get_fromfile,
annotation_set_tofile,
(void *)"shutdown"
},{
"/vendor/cmu/cyrus-imapd/squat",
ATTRIB_TYPE_BOOLEAN,
PROXY_AND_BACKEND,
ATTRIB_VALUE_SHARED,
ACL_ADMIN,
annotation_get_fromdb,
annotation_set_todb,
NULL
},{ NULL, 0, ANNOTATION_PROXY_T_INVALID,
0, 0, NULL, NULL, NULL }
};
static const annotate_entrydesc_t server_db_entry =
{
NULL,
ATTRIB_TYPE_STRING,
PROXY_AND_BACKEND,
ATTRIB_VALUE_SHARED | ATTRIB_VALUE_PRIV,
0,
annotation_get_fromdb,
annotation_set_todb,
NULL
};
/* Annotation attributes and their flags */
struct annotate_attrib
{
const char *name;
int entry;
};
const struct annotate_attrib annotation_attributes[] =
{
{ "value", ATTRIB_VALUE_SHARED | ATTRIB_VALUE_PRIV },
{ "value.shared", ATTRIB_VALUE_SHARED },
{ "value.priv", ATTRIB_VALUE_PRIV },
{ "size", ATTRIB_SIZE_SHARED | ATTRIB_SIZE_PRIV },
{ "size.shared", ATTRIB_SIZE_SHARED },
{ "size.priv", ATTRIB_SIZE_PRIV },
/*
* The following attribute names appeared in the first drafts of the
* ANNOTATEMORE extension but did not make it to the final RFC, or
* even to draft 11 which we also officially support. They might
* appear in old annotation definition files, so we map them to
* ATTRIB_DEPRECATED and issue a warning rather then remove them
* entirely.
*/
{ "modifiedsince", ATTRIB_DEPRECATED },
{ "modifiedsince.shared", ATTRIB_DEPRECATED },
{ "modifiedsince.priv", ATTRIB_DEPRECATED },
{ "content-type", ATTRIB_DEPRECATED },
{ "content-type.shared", ATTRIB_DEPRECATED },
{ "content-type.priv", ATTRIB_DEPRECATED },
{ NULL, 0 }
};
static void _annotate_fetch_entries(annotate_state_t *state,
int proxy_check)
{
struct annotate_entry_list *ee;
/* Loop through the list of provided entries to get */
for (ee = state->entry_list; ee; ee = ee->next) {
if (proxy_check) {
if (ee->desc->proxytype == BACKEND_ONLY &&
proxy_fetch_func &&
!config_getstring(IMAPOPT_PROXYSERVERS))
continue;
}
if (!_annotate_may_fetch(state, ee->desc))
continue;
ee->desc->get(state, ee);
}
}
int annotate_state_fetch(annotate_state_t *state,
const strarray_t *entries, const strarray_t *attribs,
annotate_fetch_cb_t callback, void *rock,
int *maxsizeptr)
{
int i;
struct glob *g;
const ptrarray_t *non_db_entries;
const annotate_entrydesc_t *db_entry;
int r = 0;
annotate_state_start(state);
state->callback = callback;
state->callback_rock = rock;
if (maxsizeptr) {
state->maxsize = *maxsizeptr; /* copy to check against */
state->sizeptr = maxsizeptr; /* pointer to push largest back */
}
/* Build list of attributes to fetch */
for (i = 0 ; i < attribs->count ; i++)
{
const char *s = attribs->data[i];
int attribcount;
/*
* TODO: this is bogus. The * and % wildcard characters applied
* to attributes in the early drafts of the ANNOTATEMORE
* extension, but not in later drafts where those characters are
* actually illegal in attribute names.
*/
g = glob_init(s, GLOB_HIERARCHY);
for (attribcount = 0;
annotation_attributes[attribcount].name;
attribcount++) {
if (GLOB_TEST(g, annotation_attributes[attribcount].name) != -1) {
if (annotation_attributes[attribcount].entry & ATTRIB_DEPRECATED) {
if (strcmp(s, "*"))
syslog(LOG_WARNING, "annotatemore_fetch: client used "
"deprecated attribute \"%s\", ignoring",
annotation_attributes[attribcount].name);
}
else
state->attribs |= annotation_attributes[attribcount].entry;
}
}
glob_free(&g);
}
if (!state->attribs)
goto out;
if (state->which == ANNOTATION_SCOPE_SERVER) {
non_db_entries = &server_entries;
db_entry = &server_db_entry;
}
else if (state->which == ANNOTATION_SCOPE_MAILBOX) {
non_db_entries = &mailbox_entries;
db_entry = &mailbox_db_entry;
}
else if (state->which == ANNOTATION_SCOPE_MESSAGE) {
non_db_entries = &message_entries;
db_entry = &message_db_entry;
}
else {
r = IMAP_INTERNAL;
goto out;
}
/* Build a list of callbacks for fetching the annotations */
for (i = 0 ; i < entries->count ; i++)
{
const char *s = entries->data[i];
int j;
int check_db = 0; /* should we check the db for this entry? */
g = glob_init(s, GLOB_HIERARCHY);
GLOB_SET_SEPARATOR(g, '/');
for (j = 0 ; j < non_db_entries->count ; j++) {
const annotate_entrydesc_t *desc = non_db_entries->data[j];
if (!desc->get)
continue;
if (GLOB_TEST(g, desc->name) != -1) {
/* Add this entry to our list only if it
applies to our particular server type */
if ((desc->proxytype != PROXY_ONLY)
|| proxy_fetch_func)
_annotate_state_add_entry(state, desc, desc->name);
}
if (!strcmp(s, desc->name)) {
/* exact match */
if (desc->proxytype != PROXY_ONLY) {
state->orig_entry = entries; /* proxy it */
}
break;
}
}
if (j == non_db_entries->count) {
/* no [exact] match */
state->orig_entry = entries; /* proxy it */
check_db = 1;
}
/* Add the db entry to our list if only if it
applies to our particular server type */
if (check_db &&
((db_entry->proxytype != PROXY_ONLY) || proxy_fetch_func)) {
/* Add the db entry to our list */
_annotate_state_add_entry(state, db_entry, s);
}
glob_free(&g);
}
if (state->which == ANNOTATION_SCOPE_SERVER) {
_annotate_fetch_entries(state, /*proxy_check*/1);
}
else if (state->which == ANNOTATION_SCOPE_MAILBOX) {
if (state->entry_list || proxy_fetch_func) {
if (proxy_fetch_func) {
r = annotate_state_need_mbentry(state);
if (r)
goto out;
assert(state->mbentry);
}
if (proxy_fetch_func && state->orig_entry) {
state->orig_mailbox = state->mbentry->name;
state->orig_attribute = attribs;
}
_annotate_fetch_entries(state, /*proxy_check*/0);
if (proxy_fetch_func && state->orig_entry && state->mbentry->server &&
!hash_lookup(state->mbentry->server, &state->server_table)) {
/* xxx ignoring result */
proxy_fetch_func(state->mbentry->server, state->orig_mailbox,
state->orig_entry, state->orig_attribute);
hash_insert(state->mbentry->server, (void *)0xDEADBEEF, &state->server_table);
}
}
}
else if (state->which == ANNOTATION_SCOPE_MESSAGE) {
_annotate_fetch_entries(state, /*proxy_check*/0);
}
/* Flush last cached entry in output_entryatt() */
flush_entryatt(state);
out:
annotate_state_finish(state);
return r;
}
/************************** Annotation Storing *****************************/
int annotatemore_lookup(const char *mboxname, const char *entry,
const char *userid, struct buf *value)
{
return annotatemore_msg_lookup(mboxname, /*uid*/0, entry, userid, value);
}
int annotatemore_msg_lookup(const char *mboxname, uint32_t uid, const char *entry,
const char *userid, struct buf *value)
{
char key[MAX_MAILBOX_PATH+1];
size_t keylen, datalen;
int r;
const char *data;
annotate_db_t *d = NULL;
r = _annotate_getdb(mboxname, uid, 0, &d);
if (r)
return (r == CYRUSDB_NOTFOUND ? 0 : r);
keylen = make_key(mboxname, uid, entry, userid, key, sizeof(key));
do {
r = cyrusdb_fetch(d->db, key, keylen, &data, &datalen, tid(d));
} while (r == CYRUSDB_AGAIN);
if (!r && data) {
r = split_attribs(data, datalen, value);
if (!r) {
/* Force a copy, in case the putdb() call destroys
* the per-db data area that @data points to. */
buf_cstring(value);
}
}
else if (r == CYRUSDB_NOTFOUND) r = 0;
annotate_putdb(&d);
return r;
}
static int count_old_storage(annotate_db_t *d,
const char *key, int keylen,
quota_t *oldlenp)
{
int r;
size_t datalen;
const char *data;
struct buf val = BUF_INITIALIZER;
*oldlenp = 0;
do {
r = cyrusdb_fetch(d->db, key, keylen, &data, &datalen, tid(d));
} while (r == CYRUSDB_AGAIN);
if (r == CYRUSDB_NOTFOUND) {
r = 0;
goto out;
}
if (r || !data)
goto out;
r = split_attribs(data, datalen, &val);
if (r)
goto out;
*oldlenp = val.len;
out:
buf_free(&val);
return r;
}
static int write_entry(struct mailbox *mailbox,
unsigned int uid,
const char *entry,
const char *userid,
const struct buf *value,
int ignorequota)
{
char key[MAX_MAILBOX_PATH+1];
int keylen, r;
annotate_db_t *d = NULL;
quota_t oldlen = 0;
const char *mboxname = mailbox ? mailbox->name : "";
r = _annotate_getdb(mboxname, uid, CYRUSDB_CREATE, &d);
if (r)
return r;
/* must be in a transaction to modify the db */
annotate_begin(d);
keylen = make_key(mboxname, uid, entry, userid, key, sizeof(key));
if (mailbox) {
r = count_old_storage(d, key, keylen, &oldlen);
if (r)
goto out;
}
if (!ignorequota && mailbox) {
quota_t qdiffs[QUOTA_NUMRESOURCES] =
QUOTA_DIFFS_DONTCARE_INITIALIZER;
qdiffs[QUOTA_ANNOTSTORAGE] = value->len - oldlen;
r = mailbox_quota_check(mailbox, qdiffs);
if (r)
goto out;
}
if (value->s == NULL) {
#if DEBUG
syslog(LOG_ERR, "write_entry: deleting key %s from %s",
key_as_string(d, key, keylen), d->filename);
#endif
do {
r = cyrusdb_delete(d->db, key, keylen, tid(d), /*force*/1);
} while (r == CYRUSDB_AGAIN);
}
else {
struct buf data = BUF_INITIALIZER;
unsigned long l;
static const char contenttype[] = "text/plain"; /* fake */
l = htonl(value->len);
buf_appendmap(&data, (const char *)&l, sizeof(l));
buf_appendmap(&data, value->s, value->len);
buf_putc(&data, '\0');
/*
* Older versions of Cyrus expected content-type and
* modifiedsince fields after the value. We don't support those
* but we write out default values just in case the database
* needs to be read by older versions of Cyrus
*/
buf_appendcstr(&data, contenttype);
buf_putc(&data, '\0');
l = 0; /* fake modifiedsince */
buf_appendmap(&data, (const char *)&l, sizeof(l));
#if DEBUG
syslog(LOG_ERR, "write_entry: storing key %s to %s",
key_as_string(d, key, keylen), d->filename);
#endif
do {
r = cyrusdb_store(d->db, key, keylen, data.s, data.len, tid(d));
} while (r == CYRUSDB_AGAIN);
buf_free(&data);
}
if (mailbox)
mailbox_use_annot_quota(mailbox, value->len - oldlen);
else
sync_log_annotation("");
out:
annotate_putdb(&d);
return r;
}
int annotate_state_write(annotate_state_t *state,
const char *entry,
const char *userid,
const struct buf *value)
{
return write_entry(state->mailbox, state->uid,
entry, userid, value, /*ignorequota*/1);
}
static int annotate_canon_value(struct buf *value, int type)
{
char *p = NULL;
unsigned long ignoredul;
long ignoredl;
/* check for NIL */
if (value->s == NULL)
return 0;
switch (type) {
case ATTRIB_TYPE_STRING:
/* free form */
break;
case ATTRIB_TYPE_BOOLEAN:
/* make sure its "true" or "false" */
if (value->len == 4 && !strncasecmp(value->s, "true", 4)) {
buf_reset(value);
buf_appendcstr(value, "true");
buf_cstring(value);
}
else if (value->len == 5 && !strncasecmp(value->s, "false", 5)) {
buf_reset(value);
buf_appendcstr(value, "false");
buf_cstring(value);
}
else return IMAP_ANNOTATION_BADVALUE;
break;
case ATTRIB_TYPE_UINT:
/* make sure its a valid ulong ( >= 0 ) */
errno = 0;
ignoredul = strtoul(value->s, &p, 10);
if ((p == value->s) /* no value */
|| (*p != '\0') /* illegal char */
|| (unsigned)(p - value->s) != value->len
/* embedded NUL */
|| errno /* overflow */
|| strchr(value->s, '-')) { /* negative number */
return IMAP_ANNOTATION_BADVALUE;
}
break;
case ATTRIB_TYPE_INT:
/* make sure its a valid long */
errno = 0;
ignoredl = strtol(value->s, &p, 10);
if ((p == value->s) /* no value */
|| (*p != '\0') /* illegal char */
|| (unsigned)(p - value->s) != value->len
/* embedded NUL */
|| errno) { /* underflow/overflow */
return IMAP_ANNOTATION_BADVALUE;
}
break;
default:
/* unknown type */
return IMAP_ANNOTATION_BADVALUE;
}
return 0;
}
static int _annotate_store_entries(annotate_state_t *state)
{
struct annotate_entry_list *ee;
int r;
/* Loop through the list of provided entries to get */
for (ee = state->entry_list ; ee ; ee = ee->next) {
if (ee->have_shared &&
!_annotate_may_store(state, /*shared*/1, ee->desc))
return IMAP_PERMISSION_DENIED;
if (ee->have_priv &&
!_annotate_may_store(state, /*shared*/0, ee->desc))
return IMAP_PERMISSION_DENIED;
r = ee->desc->set(state, ee);
if (r)
return r;
}
return 0;
}
struct proxy_rock {
const char *mbox_pat;
struct entryattlist *entryatts;
};
static void store_proxy(const char *server, void *data __attribute__((unused)),
void *rock)
{
struct proxy_rock *prock = (struct proxy_rock *) rock;
proxy_store_func(server, prock->mbox_pat, prock->entryatts);
}
static int _annotate_may_store(annotate_state_t *state,
int is_shared,
const annotate_entrydesc_t *desc)
{
unsigned int my_rights;
unsigned int needed = 0;
const char *acl = NULL;
/* Admins can do anything */
if (state->isadmin)
return 1;
if (state->which == ANNOTATION_SCOPE_SERVER) {
/* RFC5464 doesn't mention access control for server
* annotations, but this seems a sensible practice and is
* consistent with past Cyrus behaviour */
return !is_shared;
}
else if (state->which == ANNOTATION_SCOPE_MAILBOX) {
assert(state->mailbox);
/* Make sure its a local mailbox annotation */
if (state->mbentry && state->mbentry->server)
return 0;
acl = state->mailbox->acl;
/* RFC5464 is a trifle vague about access control for mailbox
* annotations but this seems to be compliant */
needed = ACL_LOOKUP;
if (is_shared)
needed |= ACL_READ|ACL_WRITE|desc->extra_rights;
/* fall through to ACL check */
}
else if (state->which == ANNOTATION_SCOPE_MESSAGE) {
assert(state->mailbox);
acl = state->mailbox->acl;
/* RFC5257: writing to a private annotation needs 'r'.
* Writing to a shared annotation needs 'n' */
needed = (is_shared ? ACL_ANNOTATEMSG : ACL_READ);
/* fall through to ACL check */
}
if (!acl)
return 0;
my_rights = cyrus_acl_myrights(state->auth_state, acl);
return ((my_rights & needed) == needed);
}
static int annotation_set_tofile(annotate_state_t *state
__attribute__((unused)),
struct annotate_entry_list *entry)
{
const char *filename = (const char *)entry->desc->rock;
char path[MAX_MAILBOX_PATH+1];
int r;
FILE *f;
snprintf(path, sizeof(path), "%s/msg/%s", config_dir, filename);
/* XXX how do we do this atomically with other annotations? */
if (entry->shared.s == NULL)
return unlink(path);
else {
r = cyrus_mkdir(path, 0755);
if (r)
return r;
f = fopen(path, "w");
if (!f) {
syslog(LOG_ERR, "cannot open %s for writing: %m", path);
return IMAP_IOERROR;
}
fwrite(entry->shared.s, 1, entry->shared.len, f);
fputc('\n', f);
return fclose(f);
}
return IMAP_IOERROR;
}
static int annotation_set_todb(annotate_state_t *state,
struct annotate_entry_list *entry)
{
int r = 0;
if (entry->have_shared)
r = write_entry(state->mailbox, state->uid,
entry->name, "",
&entry->shared, 0);
if (!r && entry->have_priv)
r = write_entry(state->mailbox, state->uid,
entry->name, state->userid,
&entry->priv, 0);
return r;
}
static int annotation_set_mailboxopt(annotate_state_t *state,
struct annotate_entry_list *entry)
{
struct mailbox *mailbox = state->mailbox;
uint32_t flag = (unsigned long)entry->desc->rock;
unsigned long newopts;
assert(mailbox);
newopts = mailbox->i.options;
if (entry->shared.s &&
!strcmp(entry->shared.s, "true")) {
newopts |= flag;
} else {
newopts &= ~flag;
}
/* only mark dirty if there's been a change */
if (mailbox->i.options != newopts) {
mailbox_index_dirty(mailbox);
mailbox->i.options = newopts;
}
return 0;
}
static int annotation_set_pop3showafter(annotate_state_t *state,
struct annotate_entry_list *entry)
{
struct mailbox *mailbox = state->mailbox;
int r = 0;
time_t date;
assert(mailbox);
if (entry->shared.s == NULL) {
/* Effectively removes the annotation */
date = 0;
}
else {
r = time_from_rfc3501(buf_cstring(&entry->shared), &date);
if (r < 0)
return IMAP_PROTOCOL_BAD_PARAMETERS;
}
if (date != mailbox->i.pop3_show_after) {
mailbox->i.pop3_show_after = date;
mailbox_index_dirty(mailbox);
}
return 0;
}
static int annotation_set_specialuse(annotate_state_t *state,
struct annotate_entry_list *entry)
{
int r = 0;
const char *val;
int i;
strarray_t * specialuse_extra = 0;
static const char * const valid_specialuse[] = {
/* "\\All", -- we don't support virtual folders right now */
"\\Archive",
"\\Drafts",
/* "\\Flagged", -- we don't support virtual folders right now */
"\\Junk",
"\\Sent",
"\\Trash",
NULL
};
assert(state->mailbox);
/* can only set specialuse on your own mailboxes */
if (!mboxname_userownsmailbox(state->userid, state->mailbox->name))
return IMAP_PERMISSION_DENIED;
if (entry->priv.s == NULL) {
/* Effectively removes the annotation */
val = NULL;
}
else {
for (i = 0; valid_specialuse[i]; i++) {
if (!strcasecmp(valid_specialuse[i], buf_cstring(&entry->priv)))
break;
/* or without the leading '\' */
if (!strcasecmp(valid_specialuse[i]+1, buf_cstring(&entry->priv)))
break;
}
val = valid_specialuse[i];
/* If not a built in one, check specialuse_extra option */
if (!val) {
const char * specialuse_extra_opt = config_getstring(IMAPOPT_SPECIALUSE_EXTRA);
if (specialuse_extra_opt) {
specialuse_extra = strarray_split(specialuse_extra_opt, NULL, 0);
for (i = 0; i < specialuse_extra->count; i++) {
const char * extra_val = strarray_nth(specialuse_extra, i);
if (!strcasecmp(extra_val, buf_cstring(&entry->priv))) {
/* strarray owns string, keep specialuse_extra until after set call */
val = extra_val;
break;
}
}
}
}
if (!val) {
r = IMAP_ANNOTATION_BADVALUE;
goto done;
}
}
r = mboxlist_setspecialuse(state->mailbox, val);
done:
strarray_free(specialuse_extra);
return r;
}
static int find_desc_store(int scope,
const char *name,
const annotate_entrydesc_t **descp)
{
const ptrarray_t *descs;
const annotate_entrydesc_t *db_entry;
annotate_entrydesc_t *desc;
int i;
if (scope == ANNOTATION_SCOPE_SERVER) {
descs = &server_entries;
db_entry = &server_db_entry;
}
else if (scope == ANNOTATION_SCOPE_MAILBOX) {
descs = &mailbox_entries;
db_entry = &mailbox_db_entry;
}
else if (scope == ANNOTATION_SCOPE_MESSAGE) {
descs = &message_entries;
db_entry = &message_db_entry;
}
else
return IMAP_INTERNAL;
for (i = 0 ; i < descs->count ; i++) {
desc = descs->data[i];
if (strcmp(name, desc->name))
continue;
if (!desc->set) {
/* read-only annotation */
return IMAP_PERMISSION_DENIED;
}
*descp = desc;
return 0;
}
/* unknown annotation */
if (!config_getswitch(IMAPOPT_ANNOTATION_ALLOW_UNDEFINED))
return IMAP_PERMISSION_DENIED;
/* check for /flags and /vendor/cyrus */
if (scope == ANNOTATION_SCOPE_MESSAGE &&
!strncmp(name, "/flags/", 7))
return IMAP_PERMISSION_DENIED;
if (!strncmp(name, "/vendor/cmu/cyrus-imapd/", 24))
return IMAP_PERMISSION_DENIED;
*descp = db_entry;
return 0;
}
int annotate_state_store(annotate_state_t *state, struct entryattlist *l)
{
int r = 0;
struct entryattlist *e = l;
struct attvaluelist *av;
annotate_state_start(state);
/* Build a list of callbacks for storing the annotations */
while (e) {
int attribs;
const annotate_entrydesc_t *desc = NULL;
struct annotate_entry_list *nentry = NULL;
/* See if we support this entry */
r = find_desc_store(state->which, e->entry, &desc);
if (r)
goto cleanup;
/* Add this entry to our list only if it
applies to our particular server type */
if ((desc->proxytype != PROXY_ONLY)
|| proxy_store_func)
nentry = _annotate_state_add_entry(state, desc, e->entry);
/* See if we are allowed to set the given attributes. */
attribs = desc->attribs;
av = e->attvalues;
while (av) {
if (!strcmp(av->attrib, "value.shared")) {
if (!(attribs & ATTRIB_VALUE_SHARED)) {
r = IMAP_PERMISSION_DENIED;
goto cleanup;
}
r = annotate_canon_value(&av->value,
desc->type);
if (r)
goto cleanup;
if (nentry) {
buf_init_ro(&nentry->shared, av->value.s, av->value.len);
nentry->have_shared = 1;
}
}
else if (!strcmp(av->attrib, "content-type.shared") ||
!strcmp(av->attrib, "content-type.priv")) {
syslog(LOG_WARNING, "annotatemore_store: client used "
"deprecated attribute \"%s\", ignoring",
av->attrib);
}
else if (!strcmp(av->attrib, "value.priv")) {
if (!(attribs & ATTRIB_VALUE_PRIV)) {
r = IMAP_PERMISSION_DENIED;
goto cleanup;
}
r = annotate_canon_value(&av->value,
desc->type);
if (r)
goto cleanup;
if (nentry) {
buf_init_ro(&nentry->priv, av->value.s, av->value.len);
nentry->have_priv = 1;
}
}
else {
r = IMAP_PERMISSION_DENIED;
goto cleanup;
}
av = av->next;
}
e = e->next;
}
if (state->which == ANNOTATION_SCOPE_SERVER) {
r = _annotate_store_entries(state);
}
else if (state->which == ANNOTATION_SCOPE_MAILBOX) {
if (proxy_store_func) {
r = annotate_state_need_mbentry(state);
if (r)
goto cleanup;
assert(state->mbentry);
}
assert(state->mailbox);
r = _annotate_store_entries(state);
if (r)
goto cleanup;
state->count++;
if (proxy_store_func && state->mbentry->server &&
!hash_lookup(state->mbentry->server, &state->server_table)) {
hash_insert(state->mbentry->server, (void *)0xDEADBEEF, &state->server_table);
}
if (!r && !state->count) r = IMAP_MAILBOX_NONEXISTENT;
if (proxy_store_func) {
if (!r) {
/* proxy command to backends */
struct proxy_rock prock = { NULL, NULL };
prock.mbox_pat = state->mbentry->name;
prock.entryatts = l;
hash_enumerate(&state->server_table, store_proxy, &prock);
}
}
}
else if (state->which == ANNOTATION_SCOPE_MESSAGE) {
r = _annotate_store_entries(state);
if (r)
goto cleanup;
}
cleanup:
annotate_state_finish(state);
return r;
}
struct rename_rock {
struct mailbox *oldmailbox;
struct mailbox *newmailbox;
const char *olduserid;
const char *newuserid;
uint32_t olduid;
uint32_t newuid;
int copy;
};
static int rename_cb(const char *mboxname __attribute__((unused)),
uint32_t uid,
const char *entry,
const char *userid, const struct buf *value,
void *rock)
{
struct rename_rock *rrock = (struct rename_rock *) rock;
int r = 0;
if (rrock->newmailbox) {
/* create newly renamed entry */
const char *newuserid = userid;
if (rrock->olduserid && rrock->newuserid &&
!strcmp(rrock->olduserid, userid)) {
/* renaming a user, so change the userid for priv annots */
newuserid = rrock->newuserid;
}
r = write_entry(rrock->newmailbox, rrock->newuid, entry, newuserid, value, 0);
}
if (!rrock->copy && !r) {
/* delete existing entry */
struct buf dattrib = BUF_INITIALIZER;
r = write_entry(rrock->oldmailbox, uid, entry, userid, &dattrib, 0);
}
return r;
}
int annotate_rename_mailbox(struct mailbox *oldmailbox,
struct mailbox *newmailbox)
{
/* rename one mailbox */
char *olduserid = xstrdupnull(mboxname_to_userid(oldmailbox->name));
char *newuserid = xstrdupnull(mboxname_to_userid(newmailbox->name));
annotate_db_t *d = NULL;
int r = 0;
/* rewrite any per-folder annotations from the global db */
r = _annotate_getdb(NULL, 0, /*don't create*/0, &d);
if (r == CYRUSDB_NOTFOUND) {
/* no global database, must not be anything to rename */
r = 0;
goto done;
}
if (r) goto done;
annotate_begin(d);
/* copy here - delete will dispose of old records later */
r = _annotate_rewrite(oldmailbox, 0, olduserid,
newmailbox, 0, newuserid,
/*copy*/1);
if (r) goto done;
r = annotate_commit(d);
if (r) goto done;
/*
* The per-folder database got moved or linked by mailbox_copy_files().
*/
done:
annotate_putdb(&d);
free(olduserid);
free(newuserid);
return r;
}
/*
* Perform a scan-and-rewrite through the database(s) for
* a given set of criteria; common code for several higher
* level operations.
*/
static int _annotate_rewrite(struct mailbox *oldmailbox,
uint32_t olduid,
const char *olduserid,
struct mailbox *newmailbox,
uint32_t newuid,
const char *newuserid,
int copy)
{
struct rename_rock rrock;
rrock.oldmailbox = oldmailbox;
rrock.newmailbox = newmailbox;
rrock.olduserid = olduserid;
rrock.newuserid = newuserid;
rrock.olduid = olduid;
rrock.newuid = newuid;
rrock.copy = copy;
return annotatemore_findall(oldmailbox->name, olduid, "*", &rename_cb, &rrock);
}
int annotate_delete_mailbox(struct mailbox *mailbox)
{
int r = 0;
char *fname = NULL;
annotate_db_t *d = NULL;
assert(mailbox);
/* remove any per-folder annotations from the global db */
r = _annotate_getdb(NULL, 0, /*don't create*/0, &d);
if (r == CYRUSDB_NOTFOUND) {
/* no global database, must not be anything to rename */
r = 0;
goto out;
}
if (r) goto out;
annotate_begin(d);
r = _annotate_rewrite(mailbox,
/*olduid*/0, /*olduserid*/NULL,
/*newmailbox*/NULL,
/*newuid*/0, /*newuserid*/NULL,
/*copy*/0);
if (r) goto out;
/* remove the entire per-folder database */
r = annotate_dbname_mailbox(mailbox, &fname);
if (r) goto out;
/* (gnb)TODO: do we even need to do this?? */
if (unlink(fname) < 0 && errno != ENOENT) {
syslog(LOG_ERR, "cannot unlink %s: %m", fname);
}
r = annotate_commit(d);
out:
annotate_putdb(&d);
free(fname);
return r;
}
int annotate_msg_copy(struct mailbox *oldmailbox, uint32_t olduid,
struct mailbox *newmailbox, uint32_t newuid,
const char *userid)
{
annotate_db_t *d = NULL;
int r;
r = _annotate_getdb(newmailbox->name, newuid, CYRUSDB_CREATE, &d);
if (r) return r;
annotate_begin(d);
/* If these are not true, nobody will ever commit the data we're
* about to copy, and that would be sad */
assert(newmailbox->annot_state != NULL);
assert(newmailbox->annot_state->d == d);
r = _annotate_rewrite(oldmailbox, olduid, userid,
newmailbox, newuid, userid,
/*copy*/1);
annotate_putdb(&d);
return r;
}
int annotate_msg_expunge(annotate_state_t *state)
{
if (state->which != ANNOTATION_SCOPE_MESSAGE) return IMAP_INTERNAL;
annotate_begin(state->d);
return _annotate_rewrite(state->mailbox, state->uid, /*userid*/NULL,
/*newmbox*/NULL,
/*newuid*/0, /*newuserid*/NULL,
/*copy*/0);
}
/************************* Annotation Recalc ************************/
static int calc_usage_cb(const char *mailbox __attribute__((unused)),
uint32_t uid,
const char *entry __attribute__((unused)),
const char *userid __attribute__((unused)),
const struct buf *value,
void *rock)
{
annotate_recalc_state_t *ars = (annotate_recalc_state_t *)rock;
uint32_t *cp;
char uidkey[32];
snprintf(uidkey, sizeof(uidkey), "%u", uid);
cp = hash_lookup(uidkey, &ars->counts_by_uid);
if (!cp) {
cp = xzmalloc(sizeof(uint32_t));
hash_insert(uidkey, cp, &ars->counts_by_uid);
ars->num_uids++;
}
*cp += value->len;
return 0;
}
/*
* Begin scanning the annotations dbs for per-message and
* per-mailbox annotations for this mailbox, to calculate
* mailbox->i.quota_annot_used.
*/
int annotate_recalc_begin(struct mailbox *mailbox,
annotate_recalc_state_t **arsp,
int reconstruct)
{
int r;
annotate_recalc_state_t *ars;
assert(arsp);
ars = xzmalloc(sizeof(*ars));
ars->mailbox = mailbox;
ars->reconstruct = reconstruct;
construct_hash_table(&ars->counts_by_uid, 1024, 0);
/* scan for per-mailbox annotations */
r = annotatemore_findall(mailbox->name, 0, "*",
calc_usage_cb, ars);
if (r) {
syslog(LOG_ERR, "annotation recalc: cannot scan for "
"per-mailbox annotations: %s",
error_message(r));
}
/* grab a reference to the per-mailbox db, to avoid
* multiple closes and opens of the db */
r = annotate_getdb(mailbox->name, &ars->d);
if (r == CYRUSDB_NOTFOUND)
r = 0; /* no db, not a problem, nothing to scan */
else if (r)
r = IMAP_IOERROR;
else {
/* scan for per-message annotations */
r = annotatemore_findall(mailbox->name, ANNOTATE_ANY_UID, "*",
calc_usage_cb, ars);
}
if (r) {
syslog(LOG_ERR, "annotation recalc: cannot scan for "
"per-message annotations: %s",
error_message(r));
}
/* account for per-mailbox annotations */
annotate_recalc_add(ars, 0);
/* there may have been errors above but it doesn't
* matter for our purposes */
*arsp = ars;
return 0;
}
static void annotate_recalc_end(annotate_recalc_state_t *ars)
{
assert(ars);
annotate_putdb(&ars->d);
free_hash_table(&ars->counts_by_uid, free);
free(ars);
}
void annotate_recalc_add(annotate_recalc_state_t *ars,
uint32_t uid)
{
uint32_t *cp;
char uidkey[32];
if (!ars)
return;
/* account for per-message annotations for this uid
* (or zero for per-mailbox annotations) */
snprintf(uidkey, sizeof(uidkey), "%u", uid);
cp = hash_del(uidkey, &ars->counts_by_uid);
if (cp) {
mailbox_use_annot_quota(ars->mailbox, *cp);
ars->num_uids--;
free(cp);
}
}
static int recalc_prune_cb(void *rock, const char *key, size_t keylen,
const char *data __attribute__((unused)),
size_t datalen __attribute__((unused)))
{
annotate_recalc_state_t *ars = (annotate_recalc_state_t *)rock;
int r;
const char *entry = NULL;
const char *userid = NULL;
unsigned int uid;
r = split_key(ars->d, key, keylen,
/*mboxnamep*/NULL, &uid, &entry, &userid);
if (r) {
if (ars->reconstruct) {
printf("%s: deleting annotation with bad key\n",
ars->mailbox->name);
}
syslog(LOG_ERR, "%s: deleting annotation with bad key",
ars->mailbox->name);
}
else {
char uidkey[32];
snprintf(uidkey, sizeof(uidkey), "%u", uid);
if (!hash_lookup(uidkey, &ars->counts_by_uid))
return 0;
/* this annotation is for a leftover uid */
if (!userid)
userid = "";
if (!entry)
entry = "";
if (ars->reconstruct) {
printf("%s: deleting orphan annotation "
"entry=\"%s\", uid=%u, userid=\"%s\"\n",
ars->mailbox->name, entry, uid, userid);
}
syslog(LOG_ERR, "%s: deleting orphan annotation "
"entry=\"%s\", uid=%u, userid=\"%s\"",
ars->mailbox->name, entry, uid, userid);
}
/* delete this entry */
do {
r = cyrusdb_delete(ars->d->db, key, keylen, tid(ars->d), 0);
} while (r == CYRUSDB_AGAIN);
return r;
}
int annotate_recalc_commit(annotate_recalc_state_t *ars)
{
int r = 0;
if (!ars)
return 0;
/*
* We got to the end of the mailbox uid scan, and have
* seen all the valid uids, so any leftover entries in
* counts_by_uid are for expunged or bogus messages, and
* need to be removed from the annotations db.
*/
if (!ars->num_uids)
goto out; /* easy case: no leftovers */
/*
* Start a transaction, and make a pass through the
* database deleting any annotations for leftover uids.
*/
annotate_begin(ars->d);
r = cyrusdb_foreach(ars->d->db, NULL, 0, NULL,
recalc_prune_cb, ars, tid(ars->d));
if (!r)
r = annotate_commit(ars->d);
else
annotate_abort(ars->d);
if (r) {
syslog(LOG_ERR, "annotation recalc: could not prune "
"orphan annotations: %s",
error_message(r));
}
out:
annotate_recalc_end(ars);
return r;
}
void annotate_recalc_abort(annotate_recalc_state_t *ars)
{
if (ars)
annotate_recalc_end(ars);
}
/************************* Annotation Initialization ************************/
/* The following code is courtesy of Thomas Viehmann <tv@beamnet.de> */
const struct annotate_attrib annotation_scope_names[] =
{
{ "server", ANNOTATION_SCOPE_SERVER },
{ "mailbox", ANNOTATION_SCOPE_MAILBOX },
{ "message", ANNOTATION_SCOPE_MESSAGE },
{ NULL, 0 }
};
const struct annotate_attrib annotation_proxy_type_names[] =
{
{ "proxy", PROXY_ONLY },
{ "backend", BACKEND_ONLY },
{ "proxy_and_backend", PROXY_AND_BACKEND },
{ NULL, 0 }
};
const struct annotate_attrib attribute_type_names[] =
{
/*
* The "content-type" type was only used for protocol features which
* were dropped before the RFCs became final. We accept it in
* annotation definition files only for backwards compatibility with
* earlier Cyrus versions.
*/
{ "content-type", ATTRIB_TYPE_STRING },
{ "string", ATTRIB_TYPE_STRING },
{ "boolean", ATTRIB_TYPE_BOOLEAN },
{ "uint", ATTRIB_TYPE_UINT },
{ "int", ATTRIB_TYPE_INT },
{ NULL, 0 }
};
#define ANNOT_DEF_MAXLINELEN 1024
#define ANNOT_MAX_ERRORS 64
struct parse_state
{
const char *filename;
const char *context;
unsigned int lineno;
unsigned int nerrors;
tok_t tok;
};
static void parse_error(struct parse_state *state, const char *err)
{
if (++state->nerrors < ANNOT_MAX_ERRORS)
{
struct buf msg = BUF_INITIALIZER;
buf_printf(&msg, "%s:%u:%u:error: %s",
state->filename, state->lineno,
tok_offset(&state->tok), err);
if (state->context && *state->context)
buf_printf(&msg, ", at or near '%s'", state->context);
syslog(LOG_ERR, "%s", buf_cstring(&msg));
buf_free(&msg);
}
state->context = NULL;
}
/* Search in table for the value given by @name and return
* the corresponding enum value, or -1 on error.
* @state and @errmsg is used to hint the user where we failed.
*/
static int table_lookup(const struct annotate_attrib *table,
const char *name)
{
for ( ; table->name ; table++) {
if (!strcasecmp(table->name, name))
return table->entry;
}
return -1;
}
/*
* Parse and return the next token from the line buffer. Tokens are
* separated by comma ',' characters but leading and trailing whitespace
* is trimmed. Tokens are made up of alphanumeric characters (as
* defined by libc's isalnum()) plus additional allowable characters
* defined by @extra.
*
* At start *@state points into the buffer, and will be adjusted to
* point further along in the buffer. Returns the beginning of the
* token or NULL (and whines to syslog) if an error was encountered.
*/
static char *get_token(struct parse_state *state, const char *extra)
{
char *token;
char *p;
token = tok_next(&state->tok);
if (!token) {
parse_error(state, "short line");
return NULL;
}
/* check the token */
if (extra == NULL)
extra = "";
for (p = token ; *p && (isalnum(*p) || strchr(extra, *p)) ; p++)
;
if (*p) {
state->context = p;
parse_error(state, "invalid character");
return NULL;
}
state->context = token;
return token;
}
/* Parses strings of the form value1 [ value2 [ ... ]].
* value1 is mapped via table to ints and the result or'ed.
* Whitespace is allowed between value names and punctuation.
* The field must end in '\0' or ','.
* s is advanced to '\0' or ','.
* On error errmsg is used to identify item to be parsed.
*/
static int parse_table_lookup_bitmask(const struct annotate_attrib *table,
struct parse_state *state)
{
char *token = get_token(state, ".-_/ ");
char *p;
int i;
int result = 0;
tok_t tok;
if (!token)
return -1;
tok_initm(&tok, token, NULL, 0);
while ((p = tok_next(&tok))) {
state->context = p;
i = table_lookup(table, p);
if (i < 0)
return i;
result |= i;
}
return result;
}
static int normalise_attribs(struct parse_state *state, int attribs)
{
int nattribs = 0;
static int deprecated_warnings = 0;
/* always provide size.shared if value.shared specified */
if ((attribs & ATTRIB_VALUE_SHARED))
nattribs |= ATTRIB_VALUE_SHARED|ATTRIB_SIZE_SHARED;
/* likewise size.priv */
if ((attribs & ATTRIB_VALUE_PRIV))
nattribs |= ATTRIB_VALUE_PRIV|ATTRIB_SIZE_PRIV;
/* ignore any other specified attributes */
if ((attribs & ATTRIB_DEPRECATED)) {
if (!deprecated_warnings++)
parse_error(state, "deprecated attribute names such as "
"content-type or modified-since (ignoring)");
}
return nattribs;
}
/* Create array of allowed annotations, both internally & externally defined */
static void init_annotation_definitions(void)
{
char *p;
char aline[ANNOT_DEF_MAXLINELEN];
annotate_entrydesc_t *ae;
int i;
FILE* f;
struct parse_state state;
ptrarray_t *entries;
/* copy static entries into list */
for (i = 0 ; server_builtin_entries[i].name ; i++)
ptrarray_append(&server_entries, (void *)&server_builtin_entries[i]);
/* copy static entries into list */
for (i = 0 ; mailbox_builtin_entries[i].name ; i++)
ptrarray_append(&mailbox_entries, (void *)&mailbox_builtin_entries[i]);
/* copy static entries into list */
for (i = 0 ; message_builtin_entries[i].name ; i++)
ptrarray_append(&message_entries, (void *)&message_builtin_entries[i]);
memset(&state, 0, sizeof(state));
/* parse config file */
state.filename = config_getstring(IMAPOPT_ANNOTATION_DEFINITIONS);
if (!state.filename)
return;
f = fopen(state.filename,"r");
if (!f) {
syslog(LOG_ERR, "%s: could not open annotation definition file: %m",
state.filename);
return;
}
while (fgets(aline, sizeof(aline), f)) {
/* remove leading space, skip blank lines and comments */
state.lineno++;
for (p = aline; *p && isspace(*p); p++);
if (!*p || *p == '#') continue;
tok_initm(&state.tok, aline, ",", TOK_TRIMLEFT|TOK_TRIMRIGHT|TOK_EMPTY);
/* note, we only do the most basic validity checking and may
be more restrictive than neccessary */
ae = xzmalloc(sizeof(*ae));
if (!(p = get_token(&state, ".-_/:"))) goto bad;
/* TV-TODO: should test for empty */
if (!strncmp(p, "/vendor/cmu/cyrus-imapd/", 24)) {
parse_error(&state, "annotation under /vendor/cmu/cyrus-imapd/");
goto bad;
}
ae->name = xstrdup(p);
if (!(p = get_token(&state, ".-_/"))) goto bad;
switch (table_lookup(annotation_scope_names, p)) {
case ANNOTATION_SCOPE_SERVER:
entries = &server_entries;
break;
case ANNOTATION_SCOPE_MAILBOX:
entries = &mailbox_entries;
break;
case ANNOTATION_SCOPE_MESSAGE:
if (!strncmp(ae->name, "/flags/", 7)) {
/* RFC5257 reserves the /flags/ hierarchy for future use */
state.context = ae->name;
parse_error(&state, "message entry under /flags/");
goto bad;
}
entries = &message_entries;
break;
case -1:
parse_error(&state, "invalid annotation scope");
goto bad;
}
if (!(p = get_token(&state, NULL))) goto bad;
i = table_lookup(attribute_type_names, p);
if (i < 0) {
parse_error(&state, "invalid annotation type");
goto bad;
}
ae->type = i;
i = parse_table_lookup_bitmask(annotation_proxy_type_names, &state);
if (i < 0) {
parse_error(&state, "invalid annotation proxy type");
goto bad;
}
ae->proxytype = i;
i = parse_table_lookup_bitmask(annotation_attributes, &state);
if (i < 0) {
parse_error(&state, "invalid annotation attributes");
goto bad;
}
ae->attribs = normalise_attribs(&state, i);
if (!(p = get_token(&state, NULL))) goto bad;
ae->extra_rights = cyrus_acl_strtomask(p);
p = tok_next(&state.tok);
if (p) {
parse_error(&state, "junk at end of line");
goto bad;
}
ae->get = annotation_get_fromdb;
ae->set = annotation_set_todb;
ae->rock = NULL;
ptrarray_append(entries, ae);
continue;
bad:
free((char *)ae->name);
free(ae);
tok_fini(&state.tok);
continue;
}
if (state.nerrors)
syslog(LOG_ERR, "%s: encountered %u errors. Struggling on, but "
"some of your annotation definitions may be "
"ignored. Please fix this file!",
state.filename, state.nerrors);
fclose(f);
}

File Metadata

Mime Type
text/x-c
Expires
Sat, Apr 4, 2:40 AM (6 d, 8 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18822172
Default Alt Text
annotate.c (88 KB)

Event Timeline