diff --git a/imap/annotate.c b/imap/annotate.c index 1145bc0ae..31bace7d3 100644 --- a/imap/annotate.c +++ b/imap/annotate.c @@ -1,4603 +1,4609 @@ /* 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. */ #include #include #include #include #ifdef HAVE_UNISTD_H #include #endif #include #ifdef HAVE_INTTYPES_H # include #elif defined(HAVE_STDINT_H) # include #endif #include #include #include #include #include #include #include #include "acl.h" #include "assert.h" #include "cyrusdb.h" #include "glob.h" #include "hash.h" #include "imapd.h" #include "global.h" #include "times.h" #include "mboxlist.h" #include "partlist.h" #include "util.h" #include "xmalloc.h" #include "ptrarray.h" #include "xstrlcpy.h" #include "xstrlcat.h" #include "tok.h" #include "quota.h" /* generated headers are not necessarily in current directory */ #include "imap/imap_err.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_* */ const mbentry_t *mbentry; /* for _MAILBOX */ mbentry_t *ourmbentry; struct mailbox *mailbox; /* for _MAILBOX, _MESSAGE */ struct mailbox *ourmailbox; unsigned int uid; /* for _MESSAGE */ const char *acl; /* for _MESSAGE */ annotate_db_t *d; /* authentication state */ const char *userid; int isadmin; const 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; /* state for output_entryatt */ struct attvaluelist *attvalues; char lastname[MAX_MAILBOX_PATH+1]; /* internal */ char lastentry[MAX_MAILBOX_PATH+1]; uint32_t lastuid; annotate_fetch_cb_t callback; void *callback_rock; /* * Storing. */ /* number of mailboxes matching the pattern */ unsigned count; /* * Silent. If set, mailboxes aren't dirtied for mailbox and * message annotation writes. */ unsigned silent; }; 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, int maywrite); char *freeme; /* entry and name needs to be freed on cleanup */ void *rock; /* rock passed to get() function */ }; struct annotate_db { annotate_db_t *next; int refcount; char *mboxid; char *filename; struct db *db; struct txn *txn; int in_txn; }; #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) static int (*proxy_fetch_func)(const char *server, const char *mbox_pat, const strarray_t *entry_pat, const strarray_t *attribute_pat) = NULL; static 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, const mbentry_t *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, int maywrite); static int annotation_set_todb(annotate_state_t *state, struct annotate_entry_list *entry, int maywrite); static int annotation_set_mailboxopt(annotate_state_t *state, struct annotate_entry_list *entry, int maywrite); static int annotation_set_pop3showafter(annotate_state_t *state, struct annotate_entry_list *entry, int maywrite); static int annotation_set_fuzzyalways(annotate_state_t *state, struct annotate_entry_list *entry, int maywrite); static int annotation_set_specialuse(annotate_state_t *state, struct annotate_entry_list *entry, int maywrite); 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); static void init_internal(); static int annotate_initialized = 0; static int annotatemore_dbopen = 0; /* String List Management */ /* * Append 's' to the strlist 'l'. */ EXPORTED 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)->next = 0; } /* * Free the strlist 'l' */ EXPORTED void freestrlist(struct strlist *l) { struct strlist *n; while (l) { n = l->next; free(l->s); free((char *)l); l = n; } } /* Attribute Management (also used by the ID command) */ /* * Append the 'attrib'/'value' pair to the attvaluelist 'l'. */ EXPORTED 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' */ EXPORTED 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'. */ EXPORTED 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; } EXPORTED 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); } } EXPORTED char *dumpentryatt(const struct entryattlist *l) { struct buf buf = BUF_INITIALIZER; const struct entryattlist *ee; buf_printf(&buf, "("); const char *sp = ""; const struct attvaluelist *av; for (ee = l ; ee ; ee = ee->next) { buf_printf(&buf, "%s%s (", sp, ee->entry); const char *insp = ""; for (av = ee->attvalues ; av ; av = av->next) { buf_printf(&buf, "%s%s %s", insp, av->attrib, buf_cstring(&av->value)); insp = " "; } buf_printf(&buf, ")"); sp = " "; } buf_printf(&buf, ")"); return buf_release(&buf); } EXPORTED 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' */ EXPORTED 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' */ EXPORTED void freeentryatts(struct entryattlist *l) { struct entryattlist *n; while (l) { n = l->next; free(l->entry); freeattvalues(l->attvalues); free(l); l = n; } } static void done_cb(void*rock __attribute__((unused))) { if (annotatemore_dbopen) { annotatemore_close(); } annotate_done(); } static void init_internal() { if (!annotate_initialized) { annotate_init(NULL, NULL); cyrus_modules_add(done_cb, NULL); } if (!annotatemore_dbopen) { annotatemore_open(); } } /* must be called after cyrus_init */ EXPORTED 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(); annotate_initialized = 1; } /* 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 mbentry_t *mbentry, char **fnamep) { const char *conf_fname; if (mbentry) { /* per-mbox database */ conf_fname = mbentry_metapath(mbentry, 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(const struct mailbox *mailbox, char **fnamep) { const char *conf_fname; if (!mailbox) return annotate_dbname_mbentry(NULL, fnamep); conf_fname = mailbox_meta_fname(mailbox, META_ANNOTATIONS); if (!conf_fname) return IMAP_MAILBOX_BADNAME; *fnamep = xstrdup(conf_fname); return 0; } static int annotate_dbname(const char *mboxid, char **fnamep) { int r = 0; mbentry_t *mbentry = NULL; if (mboxid) { r = mboxlist_lookup_by_uniqueid(mboxid, &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 *mboxid, const struct mailbox *mailbox, 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 (mboxid,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(mboxid, NULL) /*server scope*/ || !uid /* mailbox scope*/) mboxid = NULL; /* try to find an existing db for the mbox */ for (d = all_dbs_head ; d ; prev = d, d = d->next) { if (!strcmpsafe(mboxid, d->mboxid)) { /* 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 */ if (mailbox) r = annotate_dbname_mailbox(mailbox, &fname); else r = annotate_dbname(mboxid, &fname); if (r) goto error; #if DEBUG syslog(LOG_ERR, "Opening annotations db %s", fname); #endif r = cyrusdb_open(DB, fname, dbflags | CYRUSDB_CONVERT, &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->mboxid = xstrdupnull(mboxid); d->filename = fname; d->db = db; append_db(d); *dbp = d; return 0; error: free(fname); *dbp = NULL; return r; } HIDDEN int annotate_getdb(const struct mailbox *mailbox, annotate_db_t **dbp) { if (!mailbox) { syslog(LOG_ERR, "IOERROR: annotate_getdb called with no mailbox"); return IMAP_INTERNAL; /* we don't return the global db */ } /* ANNOTATE_ANY_UID forces UID mode */ return _annotate_getdb(mailbox_uniqueid(mailbox), mailbox, ANNOTATE_ANY_UID, 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", 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->mboxid); memset(d, 0, sizeof(*d)); /* JIC */ free(d); } HIDDEN 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; } EXPORTED void annotatemore_open(void) { int r; annotate_db_t *d = NULL; /* force opening the global annotations db */ r = _annotate_getdb(NULL, NULL, 0, CYRUSDB_CREATE, &d); if (r) fatal("can't open global annotations database", EX_TEMPFAIL); annotatemore_dbopen = 1; } EXPORTED void annotatemore_close(void) { /* close all the open databases */ while (all_dbs_head) annotate_closedb(all_dbs_head); annotatemore_dbopen = 0; } /* 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", 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", d->filename); #endif r = cyrusdb_commit(d->db, d->txn); if (r) r = IMAP_IOERROR; d->txn = NULL; } d->in_txn = 0; return r; } EXPORTED void annotate_done(void) { int i; /* DB->done() handled by cyrus_done() */ if (annotatemore_dbopen) { annotatemore_close(); } for (i = 0; i < ptrarray_size(&message_entries); i++) { annotate_entrydesc_t *ae = ptrarray_nth(&message_entries, i); if (ae->freeme) { free(ae->freeme); free(ae); } } ptrarray_fini(&message_entries); for (i = 0; i < ptrarray_size(&mailbox_entries); i++) { annotate_entrydesc_t *ae = ptrarray_nth(&mailbox_entries, i); if (ae->freeme) { free(ae->freeme); free(ae); } } ptrarray_fini(&mailbox_entries); for (i = 0; i < ptrarray_size(&server_entries); i++) { annotate_entrydesc_t *ae = ptrarray_nth(&server_entries, i); if (ae->freeme) { free(ae->freeme); free(ae); } } ptrarray_fini(&server_entries); annotate_initialized = 0; } #define OWNER_USERID_TOKEN "[.OwNeR.]" static int make_key(const char *mboxname, const char *mboxid, unsigned int uid, const char *entry, const char *userid, char *key, size_t keysize) { int keylen; if (!uid) { strlcpy(key, mboxid, keysize); } else if (uid == ANNOTATE_ANY_UID) { strlcpy(key, "*", keysize); } else { snprintf(key, keysize, "%u", uid); } keylen = strlen(key) + 1; strlcpy(key+keylen, entry, keysize-keylen); keylen += strlen(entry); /* if we don't have a userid, we're doing a foreach() */ if (userid) { if (userid[0] && mboxname && mboxname_userownsmailbox(userid, mboxname)) { /* replace userid of owner with a fixed token */ userid = OWNER_USERID_TOKEN; } 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 **mboxidp, unsigned int *uidp, const char **entryp, const char **useridp) { static struct buf keybuf; const char *p; const char *end; buf_setmap(&keybuf, key, keysize); buf_putc(&keybuf, '\0'); /* safety tricks due to broken FM code */ p = buf_cstring(&keybuf); end = p + 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. */ if (d->mboxid) { *mboxidp = d->mboxid; *uidp = 0; while (*p && p < end) *uidp = (10 * (*uidp)) + (*p++ - '0'); if (p < end) p++; else return IMAP_ANNOTATION_BADENTRY; } else { /* global db for mailbox & server scope annotations */ *uidp = 0; *mboxidp = p; while (*p && p < end) p++; if (p < end) p++; else return IMAP_ANNOTATION_BADENTRY; } *entryp = p; /* XXX: trailing NULLs on non-userid keys? Bogus just at FM */ while (*p && p < end) p++; if (p < end && !*p) *useridp = p+1; else *useridp = NULL; return 0; } #if DEBUG static const char *key_as_string(const annotate_db_t *d, const char *key, int keylen) { const char *mboxid, *entry, *userid; unsigned int uid; int r; static struct buf buf = BUF_INITIALIZER; buf_reset(&buf); r = split_key(d, key, keylen, &mboxid, &uid, &entry, &userid); if (r) buf_appendcstr(&buf, "invalid"); else buf_printf(&buf, "{ mboxid=\"%s\" uid=%u entry=\"%s\" userid=\"%s\" }", mboxid, uid, entry, userid); return buf_cstring(&buf); } #endif static int split_attribs(const char *data, int datalen, struct buf *value, struct annotate_metadata *mdata) { unsigned long tmp; /* for alignment */ const char *tmps; const char *end = data + datalen; /* initialize metadata */ memset(mdata, 0, sizeof(struct annotate_metadata)); /* xxx sanity check the data? */ if (datalen <= 0) return 1; /* * 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 * and skip to the entry's metadata. */ tmps = data + ntohl(tmp) + 1; /* Skip zero-terminated value */ if (tmps < end) { tmps += strlen(tmps) + 1; /* Skip zero-terminated content-type */ tmps += sizeof(unsigned long); /* Skip modifiedsince value */ } if (tmps < end) { /* make sure ntohll's input is correctly aligned */ modseq_t modseq; memcpy(&modseq, tmps, sizeof(modseq)); mdata->modseq = ntohll(modseq); tmps += sizeof(modseq_t); } if (tmps < end) { mdata->flags = *tmps; tmps++; } /* normalise deleted entries */ if (mdata->flags & ANNOTATE_FLAG_DELETED) { buf_reset(value); } return 0; } struct find_rock { const char *pattern; const struct mailbox *mailbox; const mbentry_t *mbentry; const char *entry; struct glob *eglob; unsigned int uid; modseq_t since_modseq; annotate_db_t *d; annotatemore_find_proc_t proc; void *rock; int flags; }; 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 *mboxid, *entry, *userid; unsigned int uid; int r; r = split_key(frock->d, key, keylen, &mboxid, &uid, &entry, &userid); if (r < 0) return 0; if (!userid) return 0; if (frock->uid && frock->uid != ANNOTATE_ANY_UID && frock->uid != uid) return 0; if (!GLOB_MATCH(frock->eglob, entry)) 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 *mboxid, *entry, *userid; unsigned int uid; char newkey[MAX_MAILBOX_PATH+1]; size_t newkeylen; struct buf value = BUF_INITIALIZER; struct annotate_metadata mdata; int r; assert(keylen < MAX_MAILBOX_PATH); r = split_key(frock->d, key, keylen, &mboxid, &uid, &entry, &userid); if (r) { syslog(LOG_ERR, "find_cb: can't split bogus key %*.s", (int)keylen, key); return r; } newkeylen = make_key(NULL, mboxid, uid, entry, userid, newkey, sizeof(newkey)); if (keylen != newkeylen || strncmp(newkey, key, keylen)) { syslog(LOG_ERR, "find_cb: bogus key %s %d %s %s (%d %d)", mboxid, uid, entry, userid, (int)keylen, (int)newkeylen); } r = split_attribs(data, datalen, &value, &mdata); if (r) { buf_free(&value); return r; } #if DEBUG syslog(LOG_ERR, "find_cb: found key %s in %s with modseq " MODSEQ_FMT, key_as_string(frock->d, key, keylen), frock->d->filename, mdata.modseq); #endif if (frock->since_modseq && frock->since_modseq >= mdata.modseq) { #if DEBUG syslog(LOG_ERR,"find_cb: ignoring key %s: " " modseq " MODSEQ_FMT " is <= " MODSEQ_FMT, key_as_string(frock->d, key, keylen), mdata.modseq, frock->since_modseq); #endif buf_free(&value); return 0; } if (((mdata.flags & ANNOTATE_FLAG_DELETED) || !buf_len(&value)) && !(frock->flags & ANNOTATE_TOMBSTONES)) { #if DEBUG syslog(LOG_ERR, "find_cb: ignoring key %s, tombstones are ignored", key_as_string(frock->d, key, keylen)); #endif buf_free(&value); return 0; } if (!r) { const char *mboxname = frock->mbentry ? frock->mbentry->name : ""; char *owner = NULL; if (!strcmp(userid, OWNER_USERID_TOKEN)) { /* construct actual userid from mboxname */ userid = owner = mboxname_to_userid(mboxname); } - r = frock->proc(mboxname, uid, entry, userid, &value, &mdata, - frock->rock); + if (userid) { + r = frock->proc(mboxname, uid, entry, userid, &value, &mdata, + frock->rock); + } else { + syslog(LOG_ERR, "find_cb: ignoring key, userid not found: %s %d %s %s (%d %d)", + mboxid, uid, entry, userid, (int)keylen, (int)newkeylen); + } + free(owner); } buf_free(&value); return r; } static int _findall(struct findall_data *data, void *rock) { struct find_rock *frock = (struct find_rock *) rock; const char *mboxname = "", *mboxid = ""; char key[MAX_MAILBOX_PATH+1], *p; size_t keylen; if (!data || !data->is_exactmatch) return 0; if (data->mbentry) { mboxname = data->mbentry->name; mboxid = data->mbentry->uniqueid; } /* Find fixed-string pattern prefix */ keylen = make_key(mboxname, mboxid, frock->uid, frock->entry, NULL, key, sizeof(key)); for (p = key; keylen; p++, keylen--) { if (*p == '*' || *p == '%') break; } keylen = p - key; frock->mbentry = data->mbentry; return cyrusdb_foreach(frock->d->db, key, keylen, &find_p, &find_cb, frock, tid(frock->d)); } static int annotatemore_findall_full(const char *pattern, /* internal */ const struct mailbox *mailbox, const char *mboxname, unsigned int uid, const char *entry, modseq_t since_modseq, annotatemore_find_proc_t proc, void *rock, int flags) { int r; struct find_rock frock; mbentry_t *mbentry = NULL; init_internal(); assert(pattern || mailbox || mboxname); assert(entry); frock.pattern = pattern; frock.mailbox = mailbox; frock.entry = entry; frock.eglob = glob_init(entry, '/'); frock.d = NULL; frock.uid = uid; frock.proc = proc; frock.rock = rock; frock.since_modseq = since_modseq; frock.flags = flags; /* special case where mboxname is "" and others are empty, this means * server annotations, which are recognised by an empty pattern by the * code below */ if (!mailbox && !pattern && !*mboxname) { mboxname = NULL; pattern = ""; } if (mailbox || mboxname) { if (mailbox) mboxname = mailbox_name(mailbox); r = mboxlist_lookup_allow_all(mboxname, &mbentry, NULL); if (r) goto out; } r = _annotate_getdb(mbentry ? mbentry->uniqueid : NULL, mailbox, uid, 0, &frock.d); if (r) { if (r == CYRUSDB_NOTFOUND) r = 0; goto out; } if (mbentry) { struct findall_data data = { .mbentry = mbentry, .is_exactmatch = 1 }; r = _findall(&data, &frock); } else if (!*pattern) { /* Server entries */ struct findall_data data = { .mbentry = NULL, .is_exactmatch = 1 }; r = _findall(&data, &frock); } else { /* Mailbox pattern */ r = mboxlist_findall(NULL, pattern, 1, NULL, NULL, &_findall, &frock); } out: mboxlist_entry_free(&mbentry); glob_free(&frock.eglob); annotate_putdb(&frock.d); return r; } EXPORTED int annotatemore_findall_mailbox(const struct mailbox *mailbox, unsigned int uid, const char *entry, modseq_t since_modseq, annotatemore_find_proc_t proc, void *rock, int flags) { return annotatemore_findall_full(NULL, mailbox, NULL, uid, entry, since_modseq, proc, rock, flags); } EXPORTED int annotatemore_findall_pattern(const char *pattern, unsigned int uid, const char *entry, modseq_t since_modseq, annotatemore_find_proc_t proc, void *rock, int flags) { return annotatemore_findall_full(pattern, NULL, NULL, uid, entry, since_modseq, proc, rock, flags); } EXPORTED int annotatemore_findall_mboxname(const char *mboxname, unsigned int uid, const char *entry, modseq_t since_modseq, annotatemore_find_proc_t proc, void *rock, int flags) { return annotatemore_findall_full(NULL, NULL, mboxname, uid, entry, since_modseq, proc, rock, flags); } /*************************** Annotate State Management ***************************/ EXPORTED 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; } EXPORTED void annotate_state_begin(annotate_state_t *state) { init_internal(); annotate_begin(state->d); } EXPORTED void annotate_state_abort(annotate_state_t **statep) { if (*statep) annotate_abort((*statep)->d); annotate_state_free(statep); } EXPORTED 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; } EXPORTED void annotate_state_set_auth(annotate_state_t *state, int isadmin, const char *userid, const 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; } EXPORTED int annotate_state_set_server(annotate_state_t *state) { return annotate_state_set_scope(state, NULL, NULL, 0); } EXPORTED int annotate_state_set_mailbox(annotate_state_t *state, struct mailbox *mailbox) { return annotate_state_set_scope(state, NULL, mailbox, 0); } EXPORTED int annotate_state_set_mailbox_mbe(annotate_state_t *state, const mbentry_t *mbentry) { return annotate_state_set_scope(state, mbentry, NULL, 0); } HIDDEN 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) { init_internal(); if (state->ourmailbox) mailbox_close(&state->ourmailbox); state->mailbox = NULL; if (state->ourmbentry) mboxlist_entry_free(&state->ourmbentry); state->mbentry = NULL; state->uid = 0; state->which = ANNOTATION_SCOPE_UNKNOWN; annotate_putdb(&state->d); } static int annotate_state_set_scope(annotate_state_t *state, const mbentry_t *mbentry, struct mailbox *mailbox, unsigned int uid) { int r = 0; annotate_db_t *oldd = NULL; int oldwhich = state->which; init_internal(); /* 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) { assert(!mailbox); assert(!uid); if (!mbentry->server) { /* local mailbox */ r = mailbox_open_iwl(mbentry->name, &mailbox); if (r) goto out; state->ourmailbox = mailbox; } state->mbentry = mbentry; state->which = ANNOTATION_SCOPE_MAILBOX; } else 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_uniqueid(mailbox) : NULL, mailbox, 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(mailbox_name(state->mailbox), &state->ourmbentry, NULL); if (r) { syslog(LOG_ERR, "Failed to lookup mbentry for %s: %s", mailbox_name(state->mailbox), error_message(r)); goto out; } state->mbentry = state->ourmbentry; } 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_PATH+1]; /* XXX MAX_MAILBOX_NAME + entry + userid */ struct buf buf = BUF_INITIALIZER; /* 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 = mailbox_name(state->mailbox); else if (state->mbentry) mboxname = state->mbentry->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); 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, SIZE_T_FMT, 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, SIZE_T_FMT, 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) { /* RFC 5464 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 || state->mbentry); /* Make sure it is a local mailbox annotation */ if (state->mbentry && state->mbentry->server) return 0; if (state->mailbox) acl = mailbox_acl(state->mailbox); else if (state->mbentry) acl = state->mbentry->acl; /* RFC 5464 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 = mailbox_acl(state->mailbox); /* RFC 5257: 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) { uint64_t tavail = 0; struct buf value = BUF_INITIALIZER; (void) partlist_local_find_freespace_most(0, NULL, NULL, &tavail, NULL); buf_printf(&value, "%" PRIuMAX, (uintmax_t)tavail); output_entryatt(state, entry->name, "", &value); buf_free(&value); } static void annotation_get_freespace_total(annotate_state_t *state, struct annotate_entry_list *entry) { uint64_t tavail = 0; uint64_t ttotal = 0; struct buf value = BUF_INITIALIZER; (void) partlist_local_find_freespace_most(0, NULL, NULL, &tavail, &ttotal); buf_printf(&value, "%" PRIuMAX ";%" PRIuMAX, (uintmax_t)tavail, (uintmax_t)ttotal); output_entryatt(state, entry->name, "", &value); buf_free(&value); } static void annotation_get_freespace_percent_most(annotate_state_t *state, struct annotate_entry_list *entry) { uint64_t avail = 0; uint64_t total = 0; struct buf value = BUF_INITIALIZER; (void) partlist_local_find_freespace_most(1, &avail, &total, NULL, NULL); buf_printf(&value, "%" PRIuMAX ";%" PRIuMAX, (uintmax_t)avail, (uintmax_t)total); 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); /* Make sure it is a remote mailbox */ if (!state->mbentry->server) goto out; /* 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->isadmin && (!state->mbentry->acl || !(cyrus_acl_myrights(state->auth_state, state->mbentry->acl) & ACL_LOOKUP))) goto out; 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); /* Make sure it is a local mailbox */ if (state->mbentry->server) goto out; /* Check ACL */ if (!state->isadmin && (!state->mbentry->acl || !(cyrus_acl_myrights(state->auth_state, state->mbentry->acl) & ACL_LOOKUP))) goto out; buf_appendcstr(&value, state->mbentry->partition); output_entryatt(state, entry->name, "", &value); out: buf_free(&value); } static void annotation_get_annotsize(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_annot_used); output_entryatt(state, entry->name, "", &value); 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 = mbentry_metapath(state->mbentry, 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_synccrcs(annotate_state_t *state, struct annotate_entry_list *entry) { struct mailbox *mailbox = state->mailbox; struct buf value = BUF_INITIALIZER; assert(mailbox); buf_printf(&value, "%u %u", mailbox->i.synccrcs.basic, mailbox->i.synccrcs.annot); output_entryatt(state, entry->name, "", &value); buf_free(&value); } static void annotation_get_foldermodseq(annotate_state_t *state, struct annotate_entry_list *entry) { struct buf value = BUF_INITIALIZER; assert(state); annotate_state_need_mbentry(state); assert(state->mbentry); buf_printf(&value, "%llu", state->mbentry->foldermodseq); output_entryatt(state, entry->name, "", &value); buf_free(&value); } static void annotation_get_usermodseq(annotate_state_t *state, struct annotate_entry_list *entry) { struct buf value = BUF_INITIALIZER; struct mboxname_counters counters; char *mboxname = NULL; memset(&counters, 0, sizeof(struct mboxname_counters)); assert(state); assert(state->userid); mboxname = mboxname_user_mbox(state->userid, NULL); mboxname_read_counters(mboxname, &counters); buf_printf(&value, "%llu", counters.highestmodseq); output_entryatt(state, entry->name, state->userid, &value); free(mboxname); buf_free(&value); } static void annotation_get_usercounters(annotate_state_t *state, struct annotate_entry_list *entry) { struct buf value = BUF_INITIALIZER; struct mboxname_counters counters; char *mboxname = NULL; assert(state); assert(state->userid); mboxname = mboxname_user_mbox(state->userid, NULL); int r = mboxname_read_counters(mboxname, &counters); if (!r) buf_printf(&value, "%u %llu %llu %llu %llu %llu %llu %llu %llu %llu %llu %llu %u", counters.version, counters.highestmodseq, counters.mailmodseq, counters.caldavmodseq, counters.carddavmodseq, counters.notesmodseq, counters.mailfoldersmodseq, counters.caldavfoldersmodseq, counters.carddavfoldersmodseq, counters.notesfoldersmodseq, counters.quotamodseq, counters.raclmodseq, counters.uidvalidity); output_entryatt(state, entry->name, state->userid, &value); free(mboxname); 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 (mailbox_uniqueid(state->mailbox)) buf_appendcstr(&value, mailbox_uniqueid(state->mailbox)); 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, const struct annotate_metadata *mdata __attribute__((unused)), 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) { state->found = 0; // if mailbox present, will be a mailbox fetch, otherwise will be a server fetch // with the blank pattern annotatemore_findall_full("", state->mailbox, NULL, state->uid, entry->name, 0, &rw_cb, state, 0); 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 // somehow */ static const annotate_entrydesc_t message_builtin_entries[] = { { /* RFC 5257 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, NULL }, { /* RFC 5257 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 }, { /* we use 'basethrid' to support split threads */ IMAP_ANNOT_NS "basethrid", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED | ATTRIB_VALUE_PRIV, 0, annotation_get_fromdb, annotation_set_todb, NULL, NULL }, { /* prior to version 12, there was no storage for thrid, so it became an annotation */ IMAP_ANNOT_NS "thrid", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED | ATTRIB_VALUE_PRIV, 0, annotation_get_fromdb, annotation_set_todb, NULL, NULL }, { /* prior to version 15, there was no storage for savedate, so it became an annotation */ IMAP_ANNOT_NS "savedate", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED | ATTRIB_VALUE_PRIV, 0, annotation_get_fromdb, annotation_set_todb, NULL, NULL }, { /* prior to version 16, there was no storage for createdmodseq, so it became an annotation */ IMAP_ANNOT_NS "createdmodseq", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED | ATTRIB_VALUE_PRIV, 0, annotation_get_fromdb, annotation_set_todb, NULL, NULL }, { /* Deprecated in favor of "snoozed" */ IMAP_ANNOT_NS "snoozed-until", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_fromdb, annotation_set_todb, NULL, NULL }, { IMAP_ANNOT_NS "snoozed", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_fromdb, annotation_set_todb, NULL, NULL }, { NULL, 0, ANNOTATION_PROXY_T_INVALID, 0, 0, NULL, 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, 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, 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, NULL },{ /* RFC 5464 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, 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, NULL },{ /* * RFC 6154 defines /private/specialuse. */ "/specialuse", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_PRIV, 0, annotation_get_fromdb, annotation_set_specialuse, NULL, 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, NULL },{ IMAP_ANNOT_NS "annotsize", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_annotsize, /*set*/NULL, NULL, NULL },{ IMAP_ANNOT_NS "archive", ATTRIB_TYPE_UINT, BACKEND_ONLY, ATTRIB_VALUE_SHARED, ACL_ADMIN, annotation_get_fromdb, annotation_set_todb, NULL, NULL },{ IMAP_ANNOT_NS "delete", ATTRIB_TYPE_UINT, BACKEND_ONLY, ATTRIB_VALUE_SHARED, ACL_ADMIN, annotation_get_fromdb, annotation_set_todb, NULL, NULL },{ IMAP_ANNOT_NS "duplicatedeliver", ATTRIB_TYPE_BOOLEAN, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_mailboxopt, annotation_set_mailboxopt, NULL, (void *)OPT_IMAP_DUPDELIVER },{ IMAP_ANNOT_NS "expire", ATTRIB_TYPE_UINT, BACKEND_ONLY, ATTRIB_VALUE_SHARED, ACL_ADMIN, annotation_get_fromdb, annotation_set_todb, NULL, NULL },{ IMAP_ANNOT_NS "lastpop", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_lastpop, /*set*/NULL, NULL, NULL },{ IMAP_ANNOT_NS "hasalarms", ATTRIB_TYPE_BOOLEAN, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_mailboxopt, /*set*/NULL, NULL, (void *)OPT_IMAP_HAS_ALARMS },{ IMAP_ANNOT_NS "foldermodseq", ATTRIB_TYPE_UINT, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_foldermodseq, /*set*/NULL, NULL, NULL },{ IMAP_ANNOT_NS "lastupdate", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_lastupdate, /*set*/NULL, NULL, NULL },{ IMAP_ANNOT_NS "news2mail", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED, ACL_ADMIN, annotation_get_fromdb, annotation_set_todb, NULL, NULL },{ IMAP_ANNOT_NS "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, NULL },{ IMAP_ANNOT_NS "pop3newuidl", ATTRIB_TYPE_BOOLEAN, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_mailboxopt, annotation_set_mailboxopt, NULL, (void *)OPT_POP3_NEW_UIDL },{ IMAP_ANNOT_NS "pop3showafter", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_pop3showafter, annotation_set_pop3showafter, NULL, NULL },{ IMAP_ANNOT_NS "search-fuzzy-always", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_fromdb, annotation_set_fuzzyalways, NULL, NULL },{ IMAP_ANNOT_NS "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, NULL },{ IMAP_ANNOT_NS "sharedseen", ATTRIB_TYPE_BOOLEAN, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_mailboxopt, annotation_set_mailboxopt, NULL, (void *)OPT_IMAP_SHAREDSEEN },{ IMAP_ANNOT_NS "sieve", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED, ACL_ADMIN, annotation_get_fromdb, annotation_set_todb, NULL, NULL },{ IMAP_ANNOT_NS "size", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_size, /*set*/NULL, NULL, NULL },{ IMAP_ANNOT_NS "sortorder", ATTRIB_TYPE_UINT, BACKEND_ONLY, ATTRIB_VALUE_PRIV, 0, annotation_get_fromdb, annotation_set_todb, NULL, NULL },{ IMAP_ANNOT_NS "squat", ATTRIB_TYPE_BOOLEAN, BACKEND_ONLY, ATTRIB_VALUE_SHARED, ACL_ADMIN, annotation_get_fromdb, annotation_set_todb, NULL, NULL },{ IMAP_ANNOT_NS "synccrcs", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_synccrcs, NULL, NULL, NULL, },{ IMAP_ANNOT_NS "uniqueid", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_uniqueid, NULL, NULL, NULL },{ NULL, 0, ANNOTATION_PROXY_T_INVALID, 0, 0, NULL, 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, NULL }; static const annotate_entrydesc_t server_builtin_entries[] = { { /* RFC 5464 defines /shared/admin. */ "/admin", ATTRIB_TYPE_STRING, PROXY_AND_BACKEND, ATTRIB_VALUE_SHARED, ACL_ADMIN, annotation_get_fromdb, annotation_set_todb, NULL, NULL },{ /* RFC 5464 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, 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, NULL, (void *)"motd" },{ /* The "usemodseq" was added with conversations support, to allow * a single value to show any changes to anything about a user */ IMAP_ANNOT_NS "usermodseq", ATTRIB_TYPE_UINT, BACKEND_ONLY, ATTRIB_VALUE_PRIV, 0, annotation_get_usermodseq, /*set*/NULL, NULL, NULL },{ /* The "usemodseq" was added with conversations support, to allow * a single value to show any changes to anything about a user */ IMAP_ANNOT_NS "usercounters", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_PRIV, 0, annotation_get_usercounters, /*set*/NULL, NULL, NULL },{ IMAP_ANNOT_NS "expire", ATTRIB_TYPE_UINT, PROXY_AND_BACKEND, ATTRIB_VALUE_SHARED, ACL_ADMIN, annotation_get_fromdb, annotation_set_todb, NULL, NULL },{ IMAP_ANNOT_NS "freespace", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_freespace, /*set*/NULL, NULL, NULL },{ IMAP_ANNOT_NS "freespace/total", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_freespace_total, /*set*/NULL, NULL, NULL },{ IMAP_ANNOT_NS "freespace/percent/most", ATTRIB_TYPE_STRING, BACKEND_ONLY, ATTRIB_VALUE_SHARED, 0, annotation_get_freespace_percent_most, /*set*/NULL, NULL, NULL },{ IMAP_ANNOT_NS "shutdown", ATTRIB_TYPE_STRING, PROXY_AND_BACKEND, ATTRIB_VALUE_SHARED, 0, annotation_get_fromfile, annotation_set_tofile, NULL, (void *)"shutdown" },{ IMAP_ANNOT_NS "squat", ATTRIB_TYPE_BOOLEAN, PROXY_AND_BACKEND, ATTRIB_VALUE_SHARED, ACL_ADMIN, annotation_get_fromdb, annotation_set_todb, NULL, NULL },{ NULL, 0, ANNOTATION_PROXY_T_INVALID, 0, 0, NULL, 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, NULL }; /* Annotation attributes and their flags */ struct annotate_attrib { const char *name; int entry; }; static 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); } } EXPORTED int annotate_state_fetch(annotate_state_t *state, const strarray_t *entries, const strarray_t *attribs, annotate_fetch_cb_t callback, void *rock) { int i; struct glob *g; const ptrarray_t *non_db_entries; const annotate_entrydesc_t *db_entry; int r = 0; init_internal(); annotate_state_start(state); state->callback = callback; state->callback_rock = rock; /* 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, '.'); for (attribcount = 0; annotation_attributes[attribcount].name; attribcount++) { if (GLOB_MATCH(g, annotation_attributes[attribcount].name)) { 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 { syslog(LOG_ERR, "IOERROR: unknown annotation scope %d", state->which); 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, '/'); 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_MATCH(g, desc->name)) { /* 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*/1); 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->mbentry->ext_name, 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 *****************************/ static int _annotate_lookup(const char *mboxname, const char *mboxid, 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; struct annotate_metadata mdata; mbentry_t *mbentry = NULL; init_internal(); if (!mboxid) { if (mboxname && *mboxname) { r = mboxlist_lookup_allow_all(mboxname, &mbentry, NULL); if (r || !mbentry->uniqueid ||(mbentry->mbtype & MBTYPE_DELETED)) { buf_free(value); if (r == IMAP_MAILBOX_NONEXISTENT) r = 0; goto done; } } mboxid = mbentry ? mbentry->uniqueid : ""; } r = _annotate_getdb(uid ? mboxid : NULL, NULL, uid, 0, &d); if (r) { if (r == CYRUSDB_NOTFOUND) r = 0; goto done; } keylen = make_key(mboxname, mboxid, 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, &mdata); if (!r) { /* Force a copy, in case the putdb() call destroys * the per-db data area that @data points to. */ buf_cstring(value); } if (mdata.flags & ANNOTATE_FLAG_DELETED) { buf_free(value); r = CYRUSDB_NOTFOUND; } } if (r == CYRUSDB_NOTFOUND) r = 0; done: mboxlist_entry_free(&mbentry); annotate_putdb(&d); return r; } EXPORTED int _annotate_lookupmask(const char *mboxname, const char *mboxid, uint32_t uid, const char *entry, const char *userid, struct buf *value) { int r = 0; value->len = 0; /* just in case! */ init_internal(); /* only if the user isn't the owner, we look for a masking value */ if (!mboxname_userownsmailbox(userid, mboxname)) r = _annotate_lookup(mboxname, mboxid, uid, entry, userid, value); /* and if there isn't one, we fall through to the shared value */ if (value->len == 0) r = _annotate_lookup(mboxname, mboxid, uid, entry, "", value); /* and because of Bron's use of NULL rather than "" at FastMail... */ if (value->len == 0) r = _annotate_lookup(mboxname, mboxid, uid, entry, NULL, value); return r; } EXPORTED int annotatemore_lookup(const char *mboxname, const char *entry, const char *userid, struct buf *value) { return _annotate_lookup(mboxname, /*mboxid*/NULL, /*uid*/0, entry, userid, value); } EXPORTED int annotatemore_lookup_mbe(const mbentry_t *mbentry, const char *entry, const char *userid, struct buf *value) { return _annotate_lookup(mbentry->name, mbentry->uniqueid, /*uid*/0, entry, userid, value); } EXPORTED int annotatemore_lookup_mbox(const struct mailbox *mailbox, const char *entry, const char *userid, struct buf *value) { return _annotate_lookup(mailbox_name(mailbox), mailbox_uniqueid(mailbox), /*uid*/0, entry, userid, value); } EXPORTED int annotatemore_lookupmask(const char *mboxname, const char *entry, const char *userid, struct buf *value) { return _annotate_lookupmask(mboxname, /*mboxid*/NULL, /*uid*/0, entry, userid, value); } EXPORTED int annotatemore_lookupmask_mbe(const mbentry_t *mbentry, const char *entry, const char *userid, struct buf *value) { return _annotate_lookupmask(mbentry->name, mbentry->uniqueid, /*uid*/0, entry, userid, value); } EXPORTED int annotatemore_lookupmask_mbox(const struct mailbox *mailbox, const char *entry, const char *userid, struct buf *value) { return _annotate_lookupmask(mailbox_name(mailbox), mailbox_uniqueid(mailbox), /*uid*/0, entry, userid, value); } EXPORTED int annotatemore_msg_lookup(const struct mailbox *mailbox, uint32_t uid, const char *entry, const char *userid, struct buf *value) { return _annotate_lookup(mailbox ? mailbox_name(mailbox) : "", mailbox ? mailbox_uniqueid(mailbox) : NULL, uid, entry, userid, value); } EXPORTED int annotatemore_msg_lookupmask(const struct mailbox *mailbox, uint32_t uid, const char *entry, const char *userid, struct buf *value) { return _annotate_lookupmask(mailbox ? mailbox_name(mailbox) : "", mailbox ? mailbox_uniqueid(mailbox) : NULL, uid, entry, userid, value); } static int read_old_value(annotate_db_t *d, const char *key, int keylen, struct buf *valp, struct annotate_metadata *mdata) { int r; size_t datalen; const char *data; 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, valp, mdata); out: return r; } static int make_entry(struct buf *data, const struct buf *value, modseq_t modseq, unsigned char flags) { unsigned long l; static const char contenttype[] = "text/plain"; /* fake */ unsigned long long nmodseq; /* Make sure that native types are wide enough */ assert(sizeof(modseq_t) <= sizeof(unsigned long long)); nmodseq = htonll((unsigned long long) modseq); l = htonl(value->len); buf_appendmap(data, (const char *)&l, sizeof(l)); buf_appendmap(data, value->s ? 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)); /* Append modseq at the end */ buf_appendmap(data, (const char *)&nmodseq, sizeof(nmodseq)); /* Append flags */ buf_putc(data, flags); return 0; } static int write_entry(struct mailbox *mailbox, unsigned int uid, const char *entry, const char *userid, const struct buf *value, int ignorequota, int silent, const struct annotate_metadata *mdata, int maywrite) { char key[MAX_MAILBOX_PATH+1]; int keylen, r; annotate_db_t *d = NULL; struct buf oldval = BUF_INITIALIZER; const char *mboxname = mailbox ? mailbox_name(mailbox) : ""; const char *mboxid = mailbox ? mailbox_uniqueid(mailbox) : ""; modseq_t modseq = mdata ? mdata->modseq : 0; r = _annotate_getdb(mboxid, mailbox, uid, CYRUSDB_CREATE, &d); if (r) return r; /* must be in a transaction to modify the db */ annotate_begin(d); keylen = make_key(mboxname, mboxid, uid, entry, userid, key, sizeof(key)); struct annotate_metadata oldmdata; r = read_old_value(d, key, keylen, &oldval, &oldmdata); if (r) goto out; /* if the value is identical, don't touch the mailbox */ if (oldval.len == value->len && (!value->len || !memcmp(oldval.s, value->s, value->len))) goto out; if (!maywrite) { r = IMAP_PERMISSION_DENIED; goto out; } if (mailbox) { if (!ignorequota) { quota_t qdiffs[QUOTA_NUMRESOURCES] = QUOTA_DIFFS_DONTCARE_INITIALIZER; qdiffs[QUOTA_ANNOTSTORAGE] = value->len - (quota_t)oldval.len; r = mailbox_quota_check(mailbox, qdiffs); if (r) goto out; } /* do the annot-changed here before altering the DB */ mailbox_annot_changed(mailbox, uid, entry, userid, &oldval, value, silent); /* grab the message annotation modseq, if not overridden */ if (uid && !mdata) { modseq = mailbox->i.highestmodseq; } } /* zero length annotation is deletion. * keep tombstones for message annotations */ if (!value->len && !uid) { #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 char flags = 0; if (!value->len || value->s == NULL) { flags |= ANNOTATE_FLAG_DELETED; } else { // this is only here to allow cleanup of invalid values in the past... // the calling of this API with a NULL "userid" is bogus, because that's // supposed to be reserved for the make_key of prefixes - but there has // been API abuse in the past, so some of these are in the wild. *sigh*. // Don't allow new ones to be written if (!userid) goto out; } make_entry(&data, value, modseq, flags); #if DEBUG syslog(LOG_ERR, "write_entry: storing key %s (value: %s) to %s (modseq=" MODSEQ_FMT ")", key_as_string(d, key, keylen), value->s, d->filename, modseq); #endif do { r = cyrusdb_store(d->db, key, keylen, data.s, data.len, tid(d)); } while (r == CYRUSDB_AGAIN); buf_free(&data); } if (!mailbox) sync_log_annotation(""); out: annotate_putdb(&d); buf_free(&oldval); return r; } EXPORTED int annotatemore_rawwrite(const char *mboxname, const char *entry, const char *userid, const struct buf *value) { char key[MAX_MAILBOX_PATH+1]; int keylen, r; annotate_db_t *d = NULL; uint32_t uid = 0; mbentry_t *mbentry = NULL; const char *mboxid = ""; init_internal(); r = _annotate_getdb(NULL, NULL, uid, CYRUSDB_CREATE, &d); if (r) goto done; if (mboxname && *mboxname) { r = mboxlist_lookup(mboxname, &mbentry, NULL); if (r) goto done; mboxid = mbentry->uniqueid; } /* must be in a transaction to modify the db */ annotate_begin(d); keylen = make_key(mboxname, mboxid, uid, entry, userid, key, sizeof(key)); if (value->s == NULL) { do { r = cyrusdb_delete(d->db, key, keylen, tid(d), /*force*/1); } while (r == CYRUSDB_AGAIN); } else { struct buf data = BUF_INITIALIZER; make_entry(&data, value, uid, /*flags*/0); do { r = cyrusdb_store(d->db, key, keylen, data.s, data.len, tid(d)); } while (r == CYRUSDB_AGAIN); buf_free(&data); } if (r) goto done; r = annotate_commit(d); done: mboxlist_entry_free(&mbentry); annotate_putdb(&d); return r; } EXPORTED int annotatemore_write(const char *mboxname, const char *entry, const char *userid, const struct buf *value) { struct mailbox *mailbox = NULL; int r = 0; annotate_db_t *d = NULL; init_internal(); if (mboxname) { r = mailbox_open_iwl(mboxname, &mailbox); if (r) goto done; } r = _annotate_getdb(mailbox_uniqueid(mailbox), mailbox, /*uid*/0, CYRUSDB_CREATE, &d); if (r) goto done; r = write_entry(mailbox, /*uid*/0, entry, userid, value, /*ignorequota*/1, /*silent*/0, NULL, /*maywrite*/1); if (r) goto done; r = annotate_commit(d); done: annotate_putdb(&d); mailbox_close(&mailbox); return r; } EXPORTED int annotatemore_writemask(const char *mboxname, const char *entry, const char *userid, const struct buf *value) { if (mboxname_userownsmailbox(userid, mboxname)) return annotatemore_write(mboxname, entry, "", value); else return annotatemore_write(mboxname, entry, userid, value); } EXPORTED 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, state->silent, NULL, /*maywrite*/1); } EXPORTED int annotate_state_writesilent(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, /*silent*/1, NULL, /*maywrite*/1); } EXPORTED int annotate_state_writemdata(annotate_state_t *state, const char *entry, const char *userid, const struct buf *value, const struct annotate_metadata *mdata) { return write_entry(state->mailbox, state->uid, entry, userid, value, /*ignorequota*/1, 0, mdata, /*maywrite*/1); } EXPORTED int annotate_state_writemask(annotate_state_t *state, const char *entry, const char *userid, const struct buf *value) { /* if the user is the owner, then write to the shared namespace */ if (mboxname_userownsmailbox(userid, mailbox_name(state->mailbox))) return annotate_state_write(state, entry, "", value); else return annotate_state_write(state, entry, userid, value); } static int annotate_canon_value(struct buf *value, int type) { char *p = NULL; unsigned long uwhatever = 0; long whatever = 0; /* check for NIL */ if (value->s == NULL) return 0; switch (type) { case ATTRIB_TYPE_STRING: /* free form */ break; case ATTRIB_TYPE_BOOLEAN: /* make sure it is "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 it is a valid ulong ( >= 0 ) */ errno = 0; buf_cstring(value); uwhatever = 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 it is a valid long */ errno = 0; buf_cstring(value); whatever = 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; } if (whatever || uwhatever) /* filthy compiler magic */ return 0; return 0; } static int _annotate_store_entries(annotate_state_t *state) { struct annotate_entry_list *ee; int r = 0; unsigned oldsilent = state->silent; /* Loop through the list of provided entries to set */ for (ee = state->entry_list ; ee ; ee = ee->next) { int maystore = 1; /* Skip annotations that can't be stored on frontend */ if ((ee->desc->proxytype == BACKEND_ONLY) && (state->mbentry && state->mbentry->server)) continue; if (ee->have_shared && !_annotate_may_store(state, /*shared*/1, ee->desc)) { maystore = 0; } if (ee->have_priv && !_annotate_may_store(state, /*shared*/0, ee->desc)) { maystore = 0; } r = ee->desc->set(state, ee, maystore); if (r) goto done; /* only the first write for message annotations isn't silent! */ if (state->which == ANNOTATION_SCOPE_MESSAGE) state->silent = 1; } done: state->silent = oldsilent; return r; } 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) { /* RFC 5464 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 it is a local mailbox annotation */ if (state->mbentry && state->mbentry->server) return 0; acl = mailbox_acl(state->mailbox); /* RFC 5464 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 = mailbox_acl(state->mailbox); /* RFC 5257: 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, int maywrite) { const char *filename = (const char *)entry->desc->rock; char path[MAX_MAILBOX_PATH+1]; int r; FILE *f; if (!maywrite) return IMAP_PERMISSION_DENIED; 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 maywrite) { int r = 0; if (entry->have_shared) r = write_entry(state->mailbox, state->uid, entry->name, "", &entry->shared, 0, state->silent, NULL, maywrite); if (!r && entry->have_priv) r = write_entry(state->mailbox, state->uid, entry->name, state->userid, &entry->priv, 0, state->silent, NULL, maywrite); return r; } static int annotation_set_mailboxopt(annotate_state_t *state, struct annotate_entry_list *entry, int maywrite) { 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) { if (!maywrite) return IMAP_PERMISSION_DENIED; mailbox_index_dirty(mailbox); mailbox_modseq_dirty(mailbox); mailbox->i.options = newopts; mboxlist_update_foldermodseq(mailbox_name(mailbox), mailbox->i.highestmodseq); } return 0; } static int annotation_set_pop3showafter(annotate_state_t *state, struct annotate_entry_list *entry, int maywrite) { 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_rfc5322(buf_cstring(&entry->shared), &date, DATETIME_FULL); if (r < 0) return IMAP_PROTOCOL_BAD_PARAMETERS; } if (date != mailbox->i.pop3_show_after) { if (!maywrite) return IMAP_PERMISSION_DENIED; mailbox_index_dirty(mailbox); mailbox_modseq_dirty(mailbox); mailbox->i.pop3_show_after = date; mboxlist_update_foldermodseq(mailbox_name(mailbox), mailbox->i.highestmodseq); } return 0; } static int annotation_set_fuzzyalways(annotate_state_t *state, struct annotate_entry_list *entry, int maywrite) { struct mailbox *mailbox = state->mailbox; assert(mailbox); if (!mboxname_isusermailbox(mailbox_name(mailbox), /*isinbox*/1)) { return IMAP_PERMISSION_DENIED; } if (buf_len(&entry->shared) && config_parse_switch(buf_cstring(&entry->shared)) < 0) { return IMAP_ANNOTATION_BADENTRY; } return annotation_set_todb(state, entry, maywrite); } EXPORTED int specialuse_validate(const char *mboxname, const char *userid, const char *src, struct buf *dest, int allow_dups) { const char *specialuse_extra_opt = config_getstring(IMAPOPT_SPECIALUSE_EXTRA); char *strval = NULL; strarray_t *valid = NULL; strarray_t *new_attribs = NULL; strarray_t *cur_attribs = NULL; struct buf mbattribs = BUF_INITIALIZER; int i, j; int r = 0; if (!src) { buf_reset(dest); return 0; } /* If there is a valid mboxname, we get the current specialuse annotations. */ if (mboxname) { annotatemore_lookup(mboxname, "/specialuse", userid, &mbattribs); if (mbattribs.len) { cur_attribs = strarray_split(buf_cstring(&mbattribs), NULL, 0); } } /* check specialuse_extra option if set */ if (specialuse_extra_opt) valid = strarray_split(specialuse_extra_opt, NULL, 0); else valid = strarray_new(); /* strarray_add(valid, "\\All"); -- we don't support virtual folders right now */ strarray_add(valid, "\\Archive"); strarray_add(valid, "\\Drafts"); /* strarray_add(valid, "\\Flagged"); -- we don't support virtual folders right now */ strarray_add(valid, "\\Important"); // draft-ietf-specialuse-important strarray_add(valid, "\\Junk"); strarray_add(valid, "\\Sent"); strarray_add(valid, "\\Trash"); strarray_add(valid, "\\Snoozed"); // JMAP new_attribs = strarray_split(src, NULL, 0); for (i = 0; i < new_attribs->count; i++) { int skip_mbcheck = allow_dups; const char *item = strarray_nth(new_attribs, i); for (j = 0; j < valid->count; j++) { /* can't use find here */ if (!strcasecmp(strarray_nth(valid, j), item)) break; /* or without the leading '\' */ if (!strcasecmp(strarray_nth(valid, j) + 1, item)) break; } if (j >= valid->count) { r = IMAP_ANNOTATION_BADENTRY; goto done; } if (cur_attribs && (strarray_find_case(cur_attribs, strarray_nth(valid, j), 0) >= 0)) { /* The mailbox has this specialuse attribute set already */ skip_mbcheck = 1; } /* don't allow names that are already in use */ if (!skip_mbcheck) { char *mbname = mboxlist_find_specialuse(strarray_nth(valid, j), userid); if (mbname) { free(mbname); r = IMAP_MAILBOX_SPECIALUSE; goto done; } } /* some attributes may not be set on mailboxes containing children */ if (mboxname && config_getstring(IMAPOPT_SPECIALUSE_NOCHILDREN)) { strarray_t *forbidden = strarray_split( config_getstring(IMAPOPT_SPECIALUSE_NOCHILDREN), NULL, STRARRAY_TRIM ); if (strarray_find(forbidden, strarray_nth(valid, j), 0) != -1 && mboxlist_haschildren(mboxname)) { r = IMAP_MAILBOX_HASCHILDREN; } strarray_free(forbidden); } /* normalise the value */ strarray_set(new_attribs, i, strarray_nth(valid, j)); } strval = strarray_join(new_attribs, " "); buf_setcstr(dest, strval); done: free(strval); strarray_free(valid); strarray_free(new_attribs); strarray_free(cur_attribs); buf_free(&mbattribs); return r; } static int annotation_set_specialuse(annotate_state_t *state, struct annotate_entry_list *entry, int maywrite) { struct buf res = BUF_INITIALIZER; int r = IMAP_PERMISSION_DENIED; assert(state->mailbox); /* Effectively removes the annotation */ if (entry->priv.s == NULL) { r = write_entry(state->mailbox, state->uid, entry->name, state->userid, &entry->priv, /*ignorequota*/0, /*silent*/0, NULL, maywrite); goto done; } r = specialuse_validate(mailbox_name(state->mailbox), state->userid, buf_cstring(&entry->priv), &res, 0); if (r) goto done; r = write_entry(state->mailbox, state->uid, entry->name, state->userid, &res, /*ignorequota*/0, state->silent, NULL, maywrite); done: buf_free(&res); return r; } static int find_desc_store(annotate_state_t *state, const char *name, const annotate_entrydesc_t **descp) { int scope = state->which; 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 { syslog(LOG_ERR, "IOERROR: unknown scope in find_desc_store %d", scope); return IMAP_INTERNAL; } /* check for DAV annotations */ if (state->mailbox && mbtypes_dav(mailbox_mbtype(state->mailbox)) && !strncmp(name, DAV_ANNOT_NS, strlen(DAV_ANNOT_NS))) { *descp = db_entry; return 0; } /* check known IMAP annotations */ 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/cmu */ if (scope == ANNOTATION_SCOPE_MESSAGE && !strncmp(name, "/flags/", 7)) return IMAP_PERMISSION_DENIED; if (!strncmp(name, IMAP_ANNOT_NS, strlen(IMAP_ANNOT_NS))) return IMAP_PERMISSION_DENIED; *descp = db_entry; return 0; } EXPORTED 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, 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); } else 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->ext_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, const struct annotate_metadata *mdata __attribute__((unused)), void *rock) { struct rename_rock *rrock = (struct rename_rock *) rock; int r = 0; if (rrock->newmailbox && /* snoozed MUST only appear on one copy of a message */ strcmp(entry, IMAP_ANNOT_NS "snoozed") && /* displayname stores the UTF-8 encoded JMAP name of a mailbox */ strcmp(entry, IMAP_ANNOT_NS "displayname")) { /* create newly renamed entry */ const char *newuserid = userid; if (rrock->olduserid && rrock->newuserid && !strcmpsafe(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, /*ignorequota*/0, /*silent*/0, NULL, /*maywrite*/1); } if (!rrock->copy && !r) { /* delete existing entry */ struct buf dattrib = BUF_INITIALIZER; r = write_entry(rrock->oldmailbox, uid, entry, userid, &dattrib, /*ignorequota*/0, /*silent*/0, NULL, /*maywrite*/1); } return r; } EXPORTED int annotate_rename_mailbox(struct mailbox *oldmailbox, struct mailbox *newmailbox) { /* rename one mailbox */ char *olduserid = mboxname_to_userid(mailbox_name(oldmailbox)); char *newuserid = mboxname_to_userid(mailbox_name(newmailbox)); annotate_db_t *d = NULL; int r = 0; init_internal(); /* rewrite any per-folder annotations from the global db */ r = _annotate_getdb(NULL, 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); if (mailbox_uniqueid(newmailbox) && strcmp(mailbox_uniqueid(oldmailbox), mailbox_uniqueid(newmailbox))) { /* copy here - delete will dispose of old records later XXX This code appears to only be necessary to allow the annotate:rename unit test to pass. In fact, that test may be obsolete for mbpath-by-id. */ r = _annotate_rewrite(oldmailbox, 0, olduserid, newmailbox, 0, newuserid, /*copy*/1); } /* delete displayname records, as they're wrong now * * XXX this is awfully tricky -- there's no direct api for deleting * XXX a single annotation, so instead we're abusing "rename_cb". * XXX but which trickery to use depends on how we got here... * .newmailbox = NULL: * nothing to copy, we already took care of that above * .copy = 0: * it's a move, so delete it from "oldmailbox" after "copying" it * .oldmailbox = whichever object will persist... * so the quota updates get applied to the correct place */ struct rename_rock rrock = { .newmailbox = NULL, .copy = 0 }; if (mailbox_mbtype(oldmailbox) & MBTYPE_LEGACY_DIRS) { /* legacy mailbox. "newmailbox" is the object that will persist */ rrock.oldmailbox = newmailbox; } else { /* uuid mailbox. "oldmailbox" is the object that will persist */ rrock.oldmailbox = oldmailbox; } /* XXX regardless of rrock trickiness above, the mailbox argument here * XXX must be "oldmailbox", because "newmailbox" doesn't fully exist * XXX (yet) and won't be found in either case */ r = annotatemore_findall_mailbox(oldmailbox, /*olduid*/0, IMAP_ANNOT_NS "displayname", /*modseq*/0, &rename_cb, &rrock, /*flags*/0); 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_mailbox(oldmailbox, olduid, "*", /*modseq*/0, &rename_cb, &rrock, /*flags*/0); } EXPORTED int annotate_delete_mailbox(struct mailbox *mailbox) { int r = 0; char *fname = NULL; annotate_db_t *d = NULL; int is_rename = 0; init_internal(); assert(mailbox); if (!mboxname_isdeletedmailbox(mailbox_name(mailbox), NULL)) { mbentry_t *mbentry = NULL; r = mboxlist_lookup_by_uniqueid(mailbox_uniqueid(mailbox), &mbentry, NULL); if (r) goto out; is_rename = strcmp(mailbox_name(mailbox), mbentry->name); mboxlist_entry_free(&mbentry); } if (!is_rename) { /* remove any per-folder annotations from the global db */ r = _annotate_getdb(NULL, 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 && r != IMAP_MAILBOX_NONEXISTENT) goto out; r = annotate_commit(d); } out: annotate_putdb(&d); free(fname); return r; } EXPORTED 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; init_internal(); r = _annotate_getdb(mailbox_uniqueid(newmailbox), newmailbox, 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; } static int cleanup_cb(void *rock, const char *key, size_t keylen, const char *data __attribute__((unused)), size_t datalen __attribute__((unused))) { annotate_db_t *d = (annotate_db_t *)rock; return cyrusdb_delete(d->db, key, keylen, tid(d), /*force*/1); } /* clean up WITHOUT counting usage again, we already removed that when * we expunged the record */ HIDDEN int annotate_msg_cleanup(struct mailbox *mailbox, unsigned int uid) { char key[MAX_MAILBOX_PATH+1]; size_t keylen; int r = 0; annotate_db_t *d = NULL; assert(uid); r = _annotate_getdb(mailbox_uniqueid(mailbox), mailbox, uid, 0, &d); if (r) return r; /* must be in a transaction to modify the db */ 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(mailbox->annot_state != NULL); assert(mailbox->annot_state->d == d); keylen = make_key(mailbox_name(mailbox), mailbox_uniqueid(mailbox), uid, "", NULL, key, sizeof(key)); r = cyrusdb_foreach(d->db, key, keylen, NULL, &cleanup_cb, d, tid(d)); annotate_putdb(&d); return r; } /************************* Annotation Initialization ************************/ /* The following code is courtesy of Thomas Viehmann */ static const struct annotate_attrib annotation_scope_names[] = { { "server", ANNOTATION_SCOPE_SERVER }, { "mailbox", ANNOTATION_SCOPE_MAILBOX }, { "message", ANNOTATION_SCOPE_MESSAGE }, { NULL, 0 } }; static const struct annotate_attrib annotation_proxy_type_names[] = { { "proxy", PROXY_ONLY }, { "backend", BACKEND_ONLY }, { "proxy_and_backend", PROXY_AND_BACKEND }, { NULL, 0 } }; static 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, "invalid annotation attributes"); 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 = NULL; /* 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 necessary */ ae = xzmalloc(sizeof(*ae)); if (!(p = get_token(&state, ".-_/:"))) goto bad; /* TV-TODO: should test for empty */ if (!strncmp(p, IMAP_ANNOT_NS, strlen(IMAP_ANNOT_NS))) { parse_error(&state, "annotation under " IMAP_ANNOT_NS); goto bad; } /* we implement case-insensitivity by lcase-and-compare, so make * sure the source is lcase'd! */ ae->freeme = xstrdup(p); lcase(ae->freeme); ae->name = ae->freeme; 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)) { /* RFC 5257 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; cyrus_acl_strtomask(p, &ae->extra_rights); /* XXX and if strtomask fails? */ 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(ae->freeme); free(ae); tok_fini(&state.tok); continue; } #if 0 /* Suppress the syslog message to fix the unit tests, but have the * syslog message to aid the admin ... */ 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); #endif fclose(f); } static int _check_rec_cb(void *rock, const char *key, size_t keylen, const char *data __attribute__((unused)), size_t datalen __attribute__((unused))) { int *do_upgrade = (int *) rock; annotate_db_t db = { NULL, 0, NULL, NULL, NULL, NULL, 0 }; const char *mboxid, *entry, *userid; unsigned uid; split_key(&db, key, keylen, &mboxid, &uid, &entry, &userid); *do_upgrade = (mboxlist_lookup_by_uniqueid(mboxid, NULL, NULL) != 0); return CYRUSDB_DONE; } static int _upgrade_cb(void *rock, const char *key, size_t keylen, const char *data, size_t datalen) { annotate_db_t *db = (annotate_db_t *) rock; mbentry_t *mbentry = NULL; const char *mboxname, *entry, *userid; char newkey[MAX_MAILBOX_PATH+1]; unsigned uid; int r; split_key(db, key, keylen, &mboxname, &uid, &entry, &userid); r = mboxlist_lookup(mboxname, &mbentry, NULL); if (r) return 0; keylen = make_key(mboxname, mbentry->uniqueid, uid, entry, userid, newkey, sizeof(newkey)); mboxlist_entry_free(&mbentry); do { r = cyrusdb_store(db->db, newkey, keylen, data, datalen, tid(db)); } while (r == CYRUSDB_AGAIN); return 0; } EXPORTED int annotatemore_upgrade(void) { annotate_db_t *db; int r, r2 = 0, do_upgrade = 0; struct buf buf = BUF_INITIALIZER; struct db *backup = NULL; char *fname; /* check if we need to upgrade */ annotatemore_open(); r = _annotate_getdb(NULL, NULL, 0, 0, &db); if (r) goto done; r = cyrusdb_foreach(db->db, "", 0, NULL, _check_rec_cb, &do_upgrade, NULL); annotatemore_close(); if (r != CYRUSDB_DONE) return r; else if (!do_upgrade) return 0; /* create db file names */ annotate_dbname_mbentry(NULL, &fname); buf_setcstr(&buf, fname); buf_appendcstr(&buf, ".OLD"); /* rename db file to backup */ r = rename(fname, buf_cstring(&buf)); free(fname); if (r) goto done; /* open backup db file */ r = cyrusdb_open(DB, buf_cstring(&buf), 0, &backup); if (r) { syslog(LOG_ERR, "DBERROR: opening %s: %s", buf_cstring(&buf), cyrusdb_strerror(r)); fatal("can't open annotations file", EX_TEMPFAIL); } /* open a new db file */ annotatemore_open(); r = _annotate_getdb(NULL, NULL, 0, CYRUSDB_CREATE, &db); if (r) goto done; /* perform upgrade from backup to new db */ annotate_begin(db); r = cyrusdb_foreach(backup, "", 0, NULL, _upgrade_cb, db, NULL); r2 = cyrusdb_close(backup); if (r2) { syslog(LOG_ERR, "DBERROR: error closing %s: %s", buf_cstring(&buf), cyrusdb_strerror(r2)); } /* complete txn on new db */ if (db->in_txn) { if (r) { annotate_abort(db); } else { r2 = annotate_commit(db); } if (r2) { syslog(LOG_ERR, "DBERROR: error %s txn in annotations_upgrade: %s", r ? "aborting" : "committing", cyrusdb_strerror(r2)); } } annotatemore_close(); done: buf_free(&buf); return r; }