/* mboxname.c -- Mailbox list 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 <config.h>

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sysexits.h>
#include <syslog.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#include "assert.h"
#include "byteorder64.h"
#include "crc32.h"
#include "glob.h"
#include "global.h"
#include "mailbox.h"
#include "map.h"
#include "retry.h"
#include "user.h"
#include "util.h"
#include "xmalloc.h"

/* generated headers are not necessarily in current directory */
#include "imap/imap_err.h"

#include "mboxname.h"
#include "mboxlist.h"
#include "cyr_lock.h"

struct mboxlocklist {
    struct mboxlocklist *next;
    struct mboxlock l;
    int nopen;
};

static struct mboxlocklist *open_mboxlocks = NULL;

static struct namespace *admin_namespace;

struct mbname_parts {
    /* master data */
    strarray_t *boxes;
    time_t is_deleted;
    char *localpart;
    char *domain;

    /* actual namespace */
    const struct namespace *extns;
    char *extuserid;

    /* cache data */
    char *userid;
    char *intname;
    char *extname;
    char *recipient;
};

#define XX 127
/*
 * Table for decoding modified base64 for IMAP UTF-7 mailbox names
 */
static const char index_mod64[256] = {
    XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
    XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
    XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,62, 63,XX,XX,XX,
    52,53,54,55, 56,57,58,59, 60,61,XX,XX, XX,XX,XX,XX,
    XX, 0, 1, 2,  3, 4, 5, 6,  7, 8, 9,10, 11,12,13,14,
    15,16,17,18, 19,20,21,22, 23,24,25,XX, XX,XX,XX,XX,
    XX,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
    41,42,43,44, 45,46,47,48, 49,50,51,XX, XX,XX,XX,XX,
    XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
    XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
    XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
    XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
    XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
    XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
    XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
    XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
};
#define CHARMOD64(c)  (index_mod64[(unsigned char)(c)])

#define FNAME_SHAREDPREFIX "shared"


static struct mboxlocklist *create_lockitem(const char *name)
{
    struct mboxlocklist *item = xmalloc(sizeof(struct mboxlocklist));
    item->next = open_mboxlocks;
    open_mboxlocks = item;

    item->nopen = 1;
    item->l.name = xstrdup(name);
    item->l.lock_fd = -1;
    item->l.locktype = 0;

    return item;
}

static struct mboxlocklist *find_lockitem(const char *name)
{
    struct mboxlocklist *item;

    for (item = open_mboxlocks; item; item = item->next) {
        if (!strcmp(name, item->l.name))
            return item;
    }

    return NULL;
}

static void remove_lockitem(struct mboxlocklist *remitem)
{
    struct mboxlocklist *item;
    struct mboxlocklist *previtem = NULL;

    for (item = open_mboxlocks; item; item = item->next) {
        if (item == remitem) {
            if (previtem)
                previtem->next = item->next;
            else
                open_mboxlocks = item->next;
            if (item->l.lock_fd != -1) {
                if (item->l.locktype)
                    lock_unlock(item->l.lock_fd, item->l.name);
                close(item->l.lock_fd);
            }
            free(item->l.name);
            free(item);
            return;
        }
        previtem = item;
    }

    fatal("didn't find item in list", EX_SOFTWARE);
}

/* name locking support */

EXPORTED int mboxname_lock(const char *mboxname, struct mboxlock **mboxlockptr,
                  int locktype_and_flags)
{
    const char *fname;
    int r = 0;
    struct mboxlocklist *lockitem;
    int nonblock;
    int locktype;

    nonblock = !!(locktype_and_flags & LOCK_NONBLOCK);
    locktype = (locktype_and_flags & ~LOCK_NONBLOCK);

    fname = mboxname_lockpath(mboxname);
    if (!fname)
        return IMAP_MAILBOX_BADNAME;

    lockitem = find_lockitem(mboxname);

    /* already open?  just use this one */
    if (lockitem) {
        /* can't change locktype! */
        if (lockitem->l.locktype != locktype)
            return IMAP_MAILBOX_LOCKED;

        lockitem->nopen++;
        goto done;
    }

    lockitem = create_lockitem(mboxname);

    /* assume success, and only create directory on failure.
     * More efficient on a common codepath */
    lockitem->l.lock_fd = open(fname, O_CREAT | O_TRUNC | O_RDWR, 0666);
    if (lockitem->l.lock_fd == -1) {
        if (cyrus_mkdir(fname, 0755) == -1) {
            r = IMAP_IOERROR;
            goto done;
        }
        lockitem->l.lock_fd = open(fname, O_CREAT | O_TRUNC | O_RDWR, 0666);
    }
    /* but if it still didn't succeed, we have problems */
    if (lockitem->l.lock_fd == -1) {
        r = IMAP_IOERROR;
        goto done;
    }

    r = lock_setlock(lockitem->l.lock_fd,
                     locktype == LOCK_EXCLUSIVE,
                     nonblock, fname);
    if (!r) lockitem->l.locktype = locktype;
    else if (errno == EWOULDBLOCK) r = IMAP_MAILBOX_LOCKED;
    else r = errno;

done:
    if (r) remove_lockitem(lockitem);
    else *mboxlockptr = &lockitem->l;

    return r;
}

EXPORTED void mboxname_release(struct mboxlock **mboxlockptr)
{
    if (!*mboxlockptr) return;

    struct mboxlocklist *lockitem;
    struct mboxlock *lock = *mboxlockptr;

    lockitem = find_lockitem(lock->name);
    assert(lockitem && &lockitem->l == lock);

    *mboxlockptr = NULL;

    if (lockitem->nopen > 1) {
        lockitem->nopen--;
        return;
    }

    remove_lockitem(lockitem);
}

EXPORTED int mboxname_islocked(const char *mboxname)
{
    return find_lockitem(mboxname) ? 1 : 0;
}

EXPORTED struct mboxlock *mboxname_usernamespacelock(const char *mboxname)
{
    mbname_t *mbname = mbname_from_intname(mboxname);
    struct mboxlock *lock = user_namespacelock(mbname_userid(mbname));
    mbname_free(&mbname);
    return lock;
}

/******************** mbname stuff **********************/

static void _mbdirty(mbname_t *mbname)
{
    free(mbname->userid);
    free(mbname->intname);
    free(mbname->extname);
    free(mbname->recipient);

    mbname->userid = NULL;
    mbname->intname = NULL;
    mbname->extname = NULL;
    mbname->recipient = NULL;
}

EXPORTED void mbname_downcaseuser(mbname_t *mbname)
{
    _mbdirty(mbname);
    if (mbname->localpart) lcase(mbname->localpart);
    if (mbname->domain) lcase(mbname->domain);
}

EXPORTED void mbname_set_localpart(mbname_t *mbname, const char *localpart)
{
    _mbdirty(mbname);
    free(mbname->localpart);
    mbname->localpart = xstrdupnull(localpart);
}

EXPORTED void mbname_set_domain(mbname_t *mbname, const char *domain)
{
    _mbdirty(mbname);
    free(mbname->domain);
    mbname->domain = strcmpsafe(domain, config_defdomain) ? xstrdupnull(domain) : NULL;
}

EXPORTED void mbname_set_boxes(mbname_t *mbname, const strarray_t *boxes)
{
    _mbdirty(mbname);
    strarray_free(mbname->boxes);
    if (boxes)
        mbname->boxes = strarray_dup(boxes);
    else
        mbname->boxes = NULL;
}

EXPORTED void mbname_push_boxes(mbname_t *mbname, const char *item)
{
    _mbdirty(mbname);
    if (!mbname->boxes) mbname->boxes = strarray_new();
    strarray_push(mbname->boxes, item);
}

EXPORTED char *mbname_pop_boxes(mbname_t *mbname)
{
    _mbdirty(mbname);
    if (!mbname->boxes) mbname->boxes = strarray_new();
    return strarray_pop(mbname->boxes);
}

EXPORTED void mbname_truncate_boxes(mbname_t *mbname, size_t len)
{
    _mbdirty(mbname);
    if (!mbname->boxes) mbname->boxes = strarray_new();
    strarray_truncate(mbname->boxes, len);
}

EXPORTED void mbname_set_isdeleted(mbname_t *mbname, time_t isdel)
{
    _mbdirty(mbname);
    mbname->is_deleted = isdel;
}

EXPORTED mbname_t *mbname_from_userid(const char *userid)
{
    mbname_t *mbname = xzmalloc(sizeof(mbname_t));
    const char *p;

    if (!userid)
        return mbname;

    if (!*userid)
        return mbname; // empty string, *sigh*

    mbname->userid = xstrdup(userid); // may as well cache it

    p = strrchr(userid, '@');
    if (p) {
        mbname->localpart = xstrndup(userid, p - userid);
        const char *domain = p+1;
        mbname->domain = strcmpsafe(domain, config_defdomain) ? xstrdupnull(domain) : NULL;
    }
    else {
        mbname->localpart = xstrdup(userid);
    }

    return mbname;
}

EXPORTED mbname_t *mbname_from_recipient(const char *recipient, const struct namespace *ns)
{
    mbname_t *mbname = xzmalloc(sizeof(mbname_t));

    if (!recipient)
        return mbname;

    mbname->recipient = xstrdup(recipient); // may as well cache it
    mbname->extns = ns;

    const char *at = strrchr(recipient, '@');
    if (at) {
        mbname->localpart = xstrndup(recipient, at - recipient);
        const char *domain = at+1;
        if (config_virtdomains && strcmpsafe(domain, config_defdomain))
            mbname->domain = xstrdupnull(domain);
        /* otherwise we ignore domain entirely */
    }
    else {
        mbname->localpart = xstrdup(recipient);
    }

    char *plus = strchr(mbname->localpart, '+');
    if (plus) {
        char sep[2];
        sep[0] = ns->hier_sep;
        sep[1] = '\0';
        *plus = '\0';
        mbname->boxes = strarray_split(plus+1, sep, /*flags*/0);
    }
    else
        mbname->boxes = strarray_new();

    return mbname;
}

EXPORTED mbname_t *mbname_from_extsub(const char *subfolder, const struct namespace *ns, const char *userid)
{
    mbname_t *mbname = mbname_from_userid(userid);

    if (!subfolder)
        return mbname;

    /* we know boxes isn't set already */
    assert(!mbname->boxes);
    char sep[2];
    sep[0] = ns->hier_sep;
    sep[1] = '\0';
    mbname->boxes = strarray_split(subfolder, sep, /*flags*/0);

    return mbname;
}

EXPORTED mbname_t *mbname_dup(const mbname_t *orig)
{
    mbname_t *mbname = xzmalloc(sizeof(mbname_t));

    mbname->localpart = xstrdupnull(orig->localpart);
    mbname->domain = xstrdupnull(orig->domain);
    mbname->is_deleted = orig->is_deleted;
    if (orig->boxes) mbname->boxes = strarray_dup(orig->boxes);

    return mbname;
}

static void _append_intbuf(struct buf *buf, const char *val)
{
    const char *p;
    for (p = val; *p; p++) {
        switch (*p) {
        case '.':
            buf_putc(buf, '^');
            break;
        default:
            buf_putc(buf, *p);
            break;
        }
    }
}

static strarray_t *_array_from_intname(strarray_t *a)
{
    int i;
    for (i = 0; i < strarray_size(a); i++) {
        char *p;
        for (p = a->data[i]; *p; p++) {
            switch (*p) {
            case '^':
                *p = '.';
                break;
            default:
                break;
            }
        }
    }
    return a;
}

static void _append_extbuf(const struct namespace *ns, struct buf *buf, const char *val)
{
    const char *p;
    int isuhs = (ns->hier_sep == '/');
    for (p = val; *p; p++) {
        switch (*p) {
        case '.':
            if (isuhs) buf_putc(buf, '.');
            else buf_putc(buf, '^');
            break;
        default:
            buf_putc(buf, *p);
            break;
        }
    }
}

static strarray_t *_array_from_extname(const struct namespace *ns, strarray_t *a)
{
    int i;
    int isuhs = (ns->hier_sep == '/');
    for (i = 0; i < strarray_size(a); i++) {
        char *p;
        for (p = a->data[i]; *p; p++) {
            switch (*p) {
            case '^':
                if (isuhs) goto err;
                else *p = '.';
                break;
            case '/':
                goto err;
            default:
                break;
            }
        }
    }
    return a;

err:
    strarray_free(a);
    return NULL;
}


EXPORTED mbname_t *mbname_from_intname(const char *intname)
{
    mbname_t *mbname = xzmalloc(sizeof(mbname_t));
    const char *p;

    if (!intname)
        return mbname;

    if (!*intname)
        return mbname; // empty string, *sigh*

    const char *dp = config_getstring(IMAPOPT_DELETEDPREFIX);

    mbname->intname = xstrdup(intname); // may as well cache it

    p = strchr(intname, '!');
    if (p) {
        mbname->domain = xstrndup(intname, p - intname);
        if (!strcmpsafe(mbname->domain, config_defdomain)) {
            free(mbname->domain);
            mbname->domain = NULL;
        }
        intname = p+1;
    }

    mbname->boxes = _array_from_intname(strarray_split(intname, ".", 0));

    if (!strarray_size(mbname->boxes))
        return mbname;

    if (strarray_size(mbname->boxes) > 2 && !strcmpsafe(strarray_nth(mbname->boxes, 0), dp)) {
        free(strarray_shift(mbname->boxes));
        char *delval = strarray_pop(mbname->boxes);
        mbname->is_deleted = strtoul(delval, NULL, 16);
        free(delval);
    }

    if (strarray_size(mbname->boxes) > 1 && !strcmpsafe(strarray_nth(mbname->boxes, 0), "user")) {
        free(strarray_shift(mbname->boxes));
        mbname->localpart = strarray_shift(mbname->boxes);
    }

    return mbname;
}

EXPORTED mbname_t *mbname_from_extname(const char *extname, const struct namespace *ns, const char *userid)
{
    int crossdomains = config_getswitch(IMAPOPT_CROSSDOMAINS) && !ns->isadmin;
    int cdother = config_getswitch(IMAPOPT_CROSSDOMAINS_ONLYOTHER);
    /* old-school virtdomains requires admin to be a different domain than the userid */
    int admindomains = config_virtdomains && ns->isadmin;

    /* specialuse magic */
    if (extname && extname[0] == '\\') {
        char *intname = mboxlist_find_specialuse(extname, userid);
        mbname_t *mbname = mbname_from_intname(intname);
        free(intname);
        return mbname;
    }

    mbname_t *mbname = xzmalloc(sizeof(mbname_t));
    char sepstr[2];
    char *p = NULL;

    if (!extname)
        return mbname;

    if (!*extname)
        return mbname; // empty string, *sigh*

    sepstr[0] = ns->hier_sep;
    sepstr[1] = '\0';

    mbname->extname = xstrdup(extname); // may as well cache it

    mbname_t *userparts = mbname_from_userid(userid);

    if (admindomains) {
        p = strrchr(mbname->extname, '@');
        if (p) {
            *p = '\0';
            if (strcmpsafe(p+1, config_defdomain))
                mbname->domain = xstrdup(p+1);
        }
        else {
            // domain admin?
            mbname->domain = xstrdupnull(mbname_domain(userparts));
        }
    }
    else if (!crossdomains) {
        // non-crossdomains, we're always in the user's domain
        mbname->domain = xstrdupnull(mbname_domain(userparts));
    }

    mbname->boxes = _array_from_extname(ns, strarray_split(mbname->extname, sepstr, 0));

    if (p) *p = '@'; // rebuild extname for later use

    if (!mbname->boxes)
        goto done;

    if (!strarray_size(mbname->boxes))
        goto done;

    if (ns->isalt) {
        /* admin can't be in here, so we can ignore that :) - and hence also
         * the DELETED namespace */
        assert(!ns->isadmin);

        const char *toplevel = strarray_nth(mbname->boxes, 0);

        const char *up = config_getstring(IMAPOPT_USERPREFIX);
        const char *sp = config_getstring(IMAPOPT_SHAREDPREFIX);
        const char *ap = config_getstring(IMAPOPT_ALTPREFIX);

        if (!strcmpsafe(toplevel, ap)) {
            free(strarray_shift(mbname->boxes));

            /* everything belongs to the userid */
            mbname->localpart = xstrdupnull(mbname_localpart(userparts));
            /* otherwise it was done above */
            if (crossdomains) mbname->domain = xstrdupnull(mbname_domain(userparts));

            goto done;
        }

        else if (!strcmpsafe(toplevel, up)) {
            /* other user namespace */
            free(strarray_shift(mbname->boxes));
            mbname->localpart = strarray_shift(mbname->boxes);
            if (crossdomains && mbname->localpart) {
                char *p = strrchr(mbname->localpart, '@');
                if (p) {
                    *p = '\0';
                    if (strcmpsafe(p+1, config_defdomain))
                        mbname->domain = xstrdup(p+1);
                }
                else if (cdother) {
                    mbname->domain = xstrdupnull(mbname_domain(userparts));
                }
                /* otherwise it must be in defdomain.  Domains are
                 * always specified in crossdomains */
            }
            goto done;
        }

        else if (!strcmpsafe(toplevel, sp)) {
            /* shared namespace, no user */
            free(strarray_shift(mbname->boxes));
            if (crossdomains) {
                const char *toplevel = strarray_nth(mbname->boxes, 0);
                if (toplevel && strrchr(toplevel, '@')) {
                    char *p = (char *)strrchr(toplevel, '@');
                    *p = '\0';
                    if (strcmpsafe(p+1, config_defdomain))
                        mbname->domain = xstrdup(p+1);
                }
                else if (cdother) {
                    mbname->domain = xstrdupnull(mbname_domain(userparts));
                }
            }
            goto done;
        }

        /* everything else belongs to the userid */
        mbname->localpart = xstrdupnull(mbname_localpart(userparts));
        /* otherwise it was done above */
        if (crossdomains) mbname->domain = xstrdupnull(mbname_domain(userparts));
        /* special case INBOX case, because horrible */
        if (!strcasecmpsafe(toplevel, "INBOX")) {
            if (strarray_size(mbname->boxes) == 1) {
                free(strarray_shift(mbname->boxes));
            }
            else {
                /* force to upper case */
                char *p = (char *)toplevel;
                for (; *p; ++p)
                    *p = toupper(*p);
            }
        }

        goto done;
    }

    const char *dp = config_getstring(IMAPOPT_DELETEDPREFIX);

    /* special inbox with insensitivity still, because horrible */
    if (!strcasecmpsafe(strarray_nth(mbname->boxes, 0), "INBOX")) {
        free(strarray_shift(mbname->boxes));
        mbname->localpart = xstrdupnull(mbname_localpart(userparts));
        /* otherwise it was done above */
        if (crossdomains) mbname->domain = xstrdupnull(mbname_domain(userparts));
        goto done;
    }

    /* deleted prefix first */
    if (ns->isadmin && !strcmpsafe(strarray_nth(mbname->boxes, 0), dp)) {
        free(strarray_shift(mbname->boxes));
        char *delval = strarray_pop(mbname->boxes);
        if (!delval)
            goto done;
        mbname->is_deleted = strtoul(delval, NULL, 16);
        free(delval);
    }

    if (!strarray_size(mbname->boxes))
        goto done;

    /* now look for user */
    if (!strcmpsafe(strarray_nth(mbname->boxes, 0), "user")) {
        free(strarray_shift(mbname->boxes));
        mbname->localpart = strarray_shift(mbname->boxes);
        if (crossdomains && mbname->localpart) {
            char *p = strrchr(mbname->localpart, '@');
            if (p) {
                *p = '\0';
                if (strcmpsafe(p+1, config_defdomain))
                    mbname->domain = xstrdup(p+1);
            }
            else if (cdother) {
                mbname->domain = xstrdupnull(mbname_domain(userparts));
            }
        }
        goto done;
    }

    /* shared folders: are in user's domain unless admin */
    if ((config_virtdomains && !ns->isadmin) || crossdomains) {
        free(mbname->domain);
        mbname->domain = xstrdupnull(mbname_domain(userparts));
    }

 done:
    mbname_free(&userparts);

    return mbname;
}

EXPORTED void mbname_free(mbname_t **mbnamep)
{
    mbname_t *mbname = *mbnamep;
    if (!mbname) return;

    *mbnamep = NULL;

    strarray_free(mbname->boxes);
    free(mbname->localpart);
    free(mbname->domain);

    /* cached values */
    free(mbname->userid);
    free(mbname->intname);
    free(mbname->extname);
    free(mbname->extuserid);
    free(mbname->recipient);

    /* thing itself */
    free(mbname);
}

EXPORTED char *mboxname_to_userid(const char *intname)
{
    mbname_t *mbname = mbname_from_intname(intname);
    char *res = xstrdupnull(mbname_userid(mbname));
    mbname_free(&mbname);
    return res;
}

EXPORTED char *mboxname_from_external(const char *extname, const struct namespace *ns, const char *userid)
{
    mbname_t *mbname = mbname_from_extname(extname, ns, userid);
    char *res = xstrdupnull(mbname_intname(mbname));
    mbname_free(&mbname);
    return res;
}

EXPORTED char *mboxname_to_external(const char *intname, const struct namespace *ns, const char *userid)
{
    mbname_t *mbname = mbname_from_intname(intname);
    char *res = xstrdupnull(mbname_extname(mbname, ns, userid));
    mbname_free(&mbname);
    return res;
}

/* all mailboxes have an internal name representation, so this
 * function should never return a NULL.
 */
EXPORTED const char *mbname_intname(const mbname_t *mbname)
{
    if (mbname->intname)
        return mbname->intname;

    struct buf buf = BUF_INITIALIZER;
    const char *dp = config_getstring(IMAPOPT_DELETEDPREFIX);
    int sep = 0;
    int i;

    strarray_t *boxes = strarray_dup(mbname_boxes(mbname));

    if (mbname->domain) {
        buf_appendcstr(&buf, mbname->domain);
        buf_putc(&buf, '!');
    }

    if (mbname->is_deleted) {
        buf_appendcstr(&buf, dp);
        sep = 1;
    }

    if (mbname->localpart) {
        if (sep) buf_putc(&buf, '.');
        buf_appendcstr(&buf, "user.");
        _append_intbuf(&buf, mbname->localpart);
        sep = 1;
    }

    for (i = 0; i < strarray_size(boxes); i++) {
        if (sep) buf_putc(&buf, '.');
        _append_intbuf(&buf, strarray_nth(boxes, i));
        sep = 1;
    }

    if (mbname->is_deleted) {
        if (sep) buf_putc(&buf, '.');
        buf_printf(&buf, "%X", (unsigned)mbname->is_deleted);
        sep = 1;
    }

    mbname_t *backdoor = (mbname_t *)mbname;
    backdoor->intname = buf_release(&buf);

    buf_free(&buf);
    strarray_free(boxes);

    return mbname->intname;
}

/* A userid may or may not have a domain - it's just localpart if the
 * domain is unspecified or config_defdomain.  It totally ignores any parts.
 * It's always NULL if there's no localpart
 */
EXPORTED const char *mbname_userid(const mbname_t *mbname)
{
    if (!mbname->localpart)
        return NULL;

    if (mbname->userid)
        return mbname->userid;

    struct buf buf = BUF_INITIALIZER;

    buf_appendcstr(&buf, mbname->localpart);

    if (mbname->domain) {
        buf_putc(&buf, '@');
        buf_appendcstr(&buf, mbname->domain);
    }

    mbname_t *backdoor = (mbname_t *)mbname;
    backdoor->userid = buf_release(&buf);

    buf_free(&buf);

    return mbname->userid;
}

/* A "recipient" is a full username in external form (including domain) with an optional
 * +addressed mailbox in external form, no INBOX prefix (since they can only be mailboxes
 * owned by the user.
 *
 * shared folders (no user) are prefixed with a +, i.e. +shared@domain.com
 *
 * DELETED folders have no recipient, ever.
 */
EXPORTED const char *mbname_recipient(const mbname_t *mbname, const struct namespace *ns)
{
    if (mbname->is_deleted) return NULL;

    /* gotta match up! */
    if (mbname->recipient && ns == mbname->extns)
        return mbname->recipient;

    struct buf buf = BUF_INITIALIZER;

    if (mbname->localpart) {
        /* user mailbox */
        buf_appendcstr(&buf, mbname->localpart);
    }
    else {
        /* shared mailbox */
        buf_appendcstr(&buf, config_getstring(IMAPOPT_POSTUSER));
    }

    int i;
    for (i = 0; i < strarray_size(mbname->boxes); i++) {
        buf_putc(&buf, i ? ns->hier_sep : '+');
        buf_appendcstr(&buf, strarray_nth(mbname->boxes, i));
    }

    buf_putc(&buf, '@');
    buf_appendcstr(&buf, mbname->domain ? mbname->domain : config_defdomain);

    mbname_t *backdoor = (mbname_t *)mbname;
    free(backdoor->recipient);
    backdoor->recipient = buf_release(&buf);
    backdoor->extns = ns;

    buf_free(&buf);

    return mbname->recipient;
}

/* This is one of the most complex parts of the code - generating an external
 * name based on the namespace, the 'isadmin' status, and of course the current
 * user.  There are some interesting things to look out for:
 *
 * Due to ambiguity, some names won't be representable in the external namespace,
 * so this function can return a NULL in those cases.
 */
EXPORTED int mbname_category(const mbname_t *mbname, const struct namespace *ns, const char *userid)
{
    if (!mbname_localpart(mbname)) return MBNAME_SHARED;
    if (mbname_isdeleted(mbname)) {
        if (strcmpsafe(mbname_userid(mbname), userid)) return MBNAME_OTHERDELETED;
        return MBNAME_OWNERDELETED;
    }

    if (strcmpsafe(mbname_userid(mbname), userid)) return MBNAME_OTHERUSER;

    const strarray_t *boxes = mbname_boxes(mbname);

    if (!strarray_size(boxes)) return MBNAME_INBOX;

    if (ns->isalt) {
        const char *toplevel = strarray_nth(boxes, 0);

        /* exact "INBOX" */
        if (!strcmpsafe(toplevel, "INBOX")) {
            if (strarray_size(boxes) == 1) return MBNAME_ALTINBOX;
            return MBNAME_INBOXSUB;
        }

        /* other "INBOX" spellings */
        if (!strcasecmpsafe(toplevel, "INBOX")) return MBNAME_ALTPREFIX;

        /* other prefixes that are special */
        if (!strcmpsafe(toplevel, config_getstring(IMAPOPT_USERPREFIX))) return MBNAME_ALTPREFIX;
        if (!strcmpsafe(toplevel, config_getstring(IMAPOPT_SHAREDPREFIX))) return MBNAME_ALTPREFIX;
        if (!strcmpsafe(toplevel, config_getstring(IMAPOPT_ALTPREFIX))) return MBNAME_ALTPREFIX;
    }

    /* everything else is owner */

    return MBNAME_OWNER;
}

EXPORTED const char *mbname_category_prefix(int category, const struct namespace *ns)
{
    if (ns->isalt) {
        switch (category) {
            case MBNAME_ALTINBOX:
                return config_getstring(IMAPOPT_ALTPREFIX);
            case MBNAME_OTHERUSER:
                return config_getstring(IMAPOPT_USERPREFIX);
            case MBNAME_SHARED:
                return config_getstring(IMAPOPT_SHAREDPREFIX);
            default:
                return NULL;
        }
    }
    else {
        if (category == MBNAME_OTHERUSER) return "user";
    }

    return NULL;
}

EXPORTED const char *mbname_extname(const mbname_t *mbname, const struct namespace *ns, const char *userid)
{
    int crossdomains = config_getswitch(IMAPOPT_CROSSDOMAINS) && !ns->isadmin;
    int cdother = config_getswitch(IMAPOPT_CROSSDOMAINS_ONLYOTHER);
    /* old-school virtdomains requires admin to be a different domain than the userid */
    int admindomains = config_virtdomains && ns->isadmin;

    /* gotta match up! */
    if (mbname->extname && ns == mbname->extns && !strcmpsafe(userid, mbname->extuserid))
        return mbname->extname;

    struct buf buf = BUF_INITIALIZER;

    /* have to zero out any existing value just in case we drop through */
    mbname_t *backdoor = (mbname_t *)mbname;
    if (backdoor->extname) {
        free(backdoor->extname);
        backdoor->extname = NULL;
        backdoor->extns = ns;
        free(backdoor->extuserid);
        backdoor->extuserid = xstrdupnull(userid);
    }

    mbname_t *userparts = mbname_from_userid(userid);
    strarray_t *boxes = strarray_dup(mbname_boxes(mbname));

    if (ns->isalt) {
        assert(!ns->isadmin);

        const char *up = config_getstring(IMAPOPT_USERPREFIX);
        const char *sp = config_getstring(IMAPOPT_SHAREDPREFIX);
        const char *ap = config_getstring(IMAPOPT_ALTPREFIX);

        /* DELETED mailboxes have no extname in alt namespace.
         * There's also no need to display domains unless in crossdomains,
         * because admins are never in altnamespace, and only admins can
         * see domains in the admindomains space */
        if (mbname->is_deleted)
            goto done;

        /* shared */
        if (!mbname_localpart(mbname)) {
            /* can't represent an empty mailbox */
            if (!strarray_size(boxes))
                goto done;

            const char *toplevel = strarray_nth(boxes, 0);

            if (strarray_size(boxes) == 1 && !strcmpsafe(toplevel, "user")) {
                /* special case user all by itself */
                buf_appendcstr(&buf, up);
                goto end;
            }
            buf_appendcstr(&buf, sp);
            buf_putc(&buf, ns->hier_sep);
            _append_extbuf(ns, &buf, toplevel);
            /* domains go on the top level folder */
            if (crossdomains) {
                const char *domain = mbname_domain(mbname);
                if (!cdother || strcmpsafe(domain, mbname_domain(userparts))) {
                    if (!domain) domain = config_defdomain;
                    buf_putc(&buf, '@');
                    _append_extbuf(ns, &buf, domain);
                }
            }
            int i;
            for (i = 1; i < strarray_size(boxes); i++) {
                buf_putc(&buf, ns->hier_sep);
                _append_extbuf(ns, &buf, strarray_nth(boxes, i));
            }
            goto end;
        }

        /* other users */
        if (strcmpsafe(mbname_userid(mbname), userid)) {
            buf_appendcstr(&buf, up);
            buf_putc(&buf, ns->hier_sep);
            _append_extbuf(ns, &buf, mbname_localpart(mbname));
            if (crossdomains) {
                const char *domain = mbname_domain(mbname);
                if (!cdother || strcmpsafe(domain, mbname_domain(userparts))) {
                    if (!domain) domain = config_defdomain;
                    buf_putc(&buf, '@');
                    _append_extbuf(ns, &buf, domain);
                }
            }
            int i;
            for (i = 0; i < strarray_size(boxes); i++) {
                buf_putc(&buf, ns->hier_sep);
                _append_extbuf(ns, &buf, strarray_nth(boxes, i));
            }
            goto end;
        }

        /* own user */
        if (!strarray_size(boxes)) {
            buf_appendcstr(&buf, "INBOX");
            goto end;
        }

        const char *toplevel = strarray_nth(boxes, 0);
        /* INBOX is very special, because it can only be represented with exact case,
         * and it skips a level. Everything else including allcaps INBOX goes into
         * the Alt Prefix */
        if (!strcasecmpsafe(toplevel, "INBOX")) {
            if (strarray_size(boxes) == 1 || strcmpsafe(toplevel, "INBOX")) {
                buf_appendcstr(&buf, ap);
                buf_putc(&buf, ns->hier_sep);
            }
        }
        /* likewise anything exactly matching the user, alt or shared prefixes, both top level
         * or with children goes into alt prefix */
        else if (!strcmpsafe(toplevel, up) || !strcmpsafe(toplevel, sp) || !strcmpsafe(toplevel, ap)) {
            buf_appendcstr(&buf, ap);
            buf_putc(&buf, ns->hier_sep);
        }

         _append_extbuf(ns, &buf, toplevel);

        int i;
        for (i = 1; i < strarray_size(boxes); i++) {
           buf_putc(&buf, ns->hier_sep);
            _append_extbuf(ns, &buf, strarray_nth(boxes, i));
        }

        goto end;
    }

    if (mbname->is_deleted) {
        buf_appendcstr(&buf, config_getstring(IMAPOPT_DELETEDPREFIX));
        buf_putc(&buf, ns->hier_sep);
    }

    /* shared */
    if (!mbname_localpart(mbname)) {
        /* invalid names - not sure it's even possible, but hey */
        if (!strarray_size(boxes))
            goto done;
        if (!strcasecmpsafe(strarray_nth(boxes, 0), "INBOX"))
            goto done;

        /* shared folders can ONLY be in the same domain except for admin */
        if (!admindomains && strcmpsafe(mbname_domain(mbname), mbname_domain(userparts)))
            goto done;

        /* note "user" precisely appears here, but no need to special case it
         * since the output is the same */
        int i;
        for (i = 0; i < strarray_size(boxes); i++) {
            if (i) buf_putc(&buf, ns->hier_sep);
            _append_extbuf(ns, &buf, strarray_nth(boxes, i));
        }

        goto end;
    }

    /* other users or DELETED */
    if (mbname->is_deleted || strcmpsafe(mbname_userid(mbname), userid)) {
        buf_appendcstr(&buf, "user");
        buf_putc(&buf, ns->hier_sep);
        _append_extbuf(ns, &buf, mbname_localpart(mbname));
        if (crossdomains) {
            const char *domain = mbname_domain(mbname);
            if (!cdother || strcmpsafe(domain, mbname_domain(userparts))) {
                if (!domain) domain = config_defdomain;
                buf_putc(&buf, '@');
                _append_extbuf(ns, &buf, domain);
            }
        }
        /* shared folders can ONLY be in the same domain except for admin */
        else if (!admindomains && strcmpsafe(mbname_domain(mbname), mbname_domain(userparts)))
            goto done;
        int i;
        for (i = 0; i < strarray_size(boxes); i++) {
            buf_putc(&buf, ns->hier_sep);
            _append_extbuf(ns, &buf, strarray_nth(boxes, i));
        }
        goto end;
    }

    buf_appendcstr(&buf, "INBOX");
    int i;
    for (i = 0; i < strarray_size(boxes); i++) {
       buf_putc(&buf, ns->hier_sep);
       _append_extbuf(ns, &buf, strarray_nth(boxes, i));
    }

 end:

    /* note: kinda bogus in altnamespace, meh */
    if (mbname->is_deleted) {
        buf_putc(&buf, ns->hier_sep);
        buf_printf(&buf, "%X", (unsigned)mbname->is_deleted);
    }

    if (admindomains && mbname_domain(mbname)) {
        buf_putc(&buf, '@');
        buf_appendcstr(&buf, mbname_domain(mbname));
    }

    backdoor->extname = buf_release(&buf);

 done:

    buf_free(&buf);
    mbname_free(&userparts);
    strarray_free(boxes);

    return mbname->extname;
}

EXPORTED const char *mbname_domain(const mbname_t *mbname)
{
    return mbname->domain;
}

EXPORTED const char *mbname_localpart(const mbname_t *mbname)
{
    return mbname->localpart;
}

EXPORTED time_t mbname_isdeleted(const mbname_t *mbname)
{
    return mbname->is_deleted;
}

EXPORTED const strarray_t *mbname_boxes(const mbname_t *mbname)
{
    if (!mbname->boxes) {
        mbname_t *backdoor = (mbname_t *)mbname;
        backdoor->boxes = strarray_new();
    }
    return mbname->boxes;
}

/*
 * Create namespace based on config options.
 */
EXPORTED int mboxname_init_namespace(struct namespace *namespace, int isadmin)
{
    const char *prefix;

    assert(namespace != NULL);

    namespace->isadmin = isadmin;

    namespace->hier_sep =
        config_getswitch(IMAPOPT_UNIXHIERARCHYSEP) ? '/' : '.';
    namespace->isalt = !isadmin && config_getswitch(IMAPOPT_ALTNAMESPACE);

    namespace->accessible[NAMESPACE_INBOX] = 1;
    namespace->accessible[NAMESPACE_USER] = !config_getswitch(IMAPOPT_DISABLE_USER_NAMESPACE);
    namespace->accessible[NAMESPACE_SHARED] = !config_getswitch(IMAPOPT_DISABLE_SHARED_NAMESPACE);

    if (namespace->isalt) {
        /* alternate namespace */
        strcpy(namespace->prefix[NAMESPACE_INBOX], "");

        prefix = config_getstring(IMAPOPT_USERPREFIX);
        if (!prefix || strlen(prefix) == 0 ||
            strlen(prefix) >= MAX_NAMESPACE_PREFIX ||
            strchr(prefix,namespace->hier_sep) != NULL)
            return IMAP_NAMESPACE_BADPREFIX;
        sprintf(namespace->prefix[NAMESPACE_USER], "%.*s%c",
                MAX_NAMESPACE_PREFIX-1, prefix, namespace->hier_sep);

        prefix = config_getstring(IMAPOPT_SHAREDPREFIX);
        if (!prefix || strlen(prefix) == 0 ||
            strlen(prefix) >= MAX_NAMESPACE_PREFIX ||
            strchr(prefix, namespace->hier_sep) != NULL ||
            !strncmp(namespace->prefix[NAMESPACE_USER], prefix, strlen(prefix)))
            return IMAP_NAMESPACE_BADPREFIX;

        if (!isadmin) {
            sprintf(namespace->prefix[NAMESPACE_SHARED], "%.*s%c",
                MAX_NAMESPACE_PREFIX-1, prefix, namespace->hier_sep);
        }
    }

    else {
        /* standard namespace */
        sprintf(namespace->prefix[NAMESPACE_INBOX], "%s%c",
                "INBOX", namespace->hier_sep);
        sprintf(namespace->prefix[NAMESPACE_USER], "%s%c",
                "user", namespace->hier_sep);
        strcpy(namespace->prefix[NAMESPACE_SHARED], "");
    }

    return 0;
}

EXPORTED struct namespace *mboxname_get_adminnamespace()
{
    static struct namespace ns;
    if (!admin_namespace) {
        mboxname_init_namespace(&ns, /*isadmin*/1);
        admin_namespace = &ns;
    }
    return admin_namespace;
}

/*
 * Return nonzero if 'userid' owns the (internal) mailbox 'name'.
 */
EXPORTED int mboxname_userownsmailbox(const char *userid, const char *name)
{
    mbname_t *mbname = mbname_from_intname(name);
    int res = !strcmpsafe(mbname_userid(mbname), userid);
    mbname_free(&mbname);

    return res;
}

/*
 * If (internal) mailbox 'name' is a user's mailbox (optionally INBOX),
 * returns 1, otherwise returns 0.
 */
EXPORTED int mboxname_isusermailbox(const char *name, int isinbox)
{
    mbname_t *mbname = mbname_from_intname(name);
    int res = 0;

    if (mbname_localpart(mbname) && !mbname_isdeleted(mbname)) {
        if (!isinbox || !strarray_size(mbname_boxes(mbname)))
            res = 1;
    }

    mbname_free(&mbname);
    return res;
}

/*
 * If (internal) mailbox 'name' is a user's Trash folder returns 1,
 * otherwise returns 0
 * XXX: use roles rather than hard coded name?
 */
EXPORTED int mboxname_isusertrash(const char *name)
{
    mbname_t *mbname = mbname_from_intname(name);
    int res = 0;

    if (mbname_localpart(mbname) && !mbname_isdeleted(mbname)) {
        const strarray_t *boxes = mbname_boxes(mbname);
        if (strarray_size(boxes) == 1 && !strcmpsafe(strarray_nth(boxes, 0), "Trash"))
            res = 1;
    }

    mbname_free(&mbname);
    return res;
}

/*
 * If (internal) mailbox 'name' is a DELETED mailbox
 * returns boolean
 */
EXPORTED int mboxname_isdeletedmailbox(const char *name, time_t *timestampp)
{
    mbname_t *mbname = mbname_from_intname(name);
    time_t res = mbname_isdeleted(mbname);
    mbname_free(&mbname);

    if (timestampp)
        *timestampp = res;

    return res ? 1 : 0;
}

/*
 * If (internal) mailbox 'name' is a CALENDAR mailbox
 * returns boolean
 */
EXPORTED int mboxname_iscalendarmailbox(const char *name, int mbtype)
{
    if (mbtype & MBTYPE_CALENDAR) return 1;  /* Only works on backends */
    int res = 0;

    mbname_t *mbname = mbname_from_intname(name);
    const strarray_t *boxes = mbname_boxes(mbname);
    const char *prefix = config_getstring(IMAPOPT_CALENDARPREFIX);
    if (strarray_size(boxes) && !strcmpsafe(prefix, strarray_nth(boxes, 0)))
        res = 1;

    mbname_free(&mbname);
    return res;
}

/*
 * If (internal) mailbox 'name' is a ADDRESSBOOK mailbox
 * returns boolean
 */
EXPORTED int mboxname_isaddressbookmailbox(const char *name, int mbtype)
{
    if (mbtype & MBTYPE_ADDRESSBOOK) return 1;  /* Only works on backends */
    int res = 0;

    mbname_t *mbname = mbname_from_intname(name);
    const strarray_t *boxes = mbname_boxes(mbname);
    const char *prefix = config_getstring(IMAPOPT_ADDRESSBOOKPREFIX);
    if (strarray_size(boxes) && !strcmpsafe(prefix, strarray_nth(boxes, 0)))
        res = 1;

    mbname_free(&mbname);
    return res;
}

/*
 * If (internal) mailbox 'name' is a DAVDRIVE mailbox
 * returns boolean
 */
EXPORTED int mboxname_isdavdrivemailbox(const char *name, int mbtype)
{
    if (mbtype & MBTYPE_COLLECTION) return 1;  /* Only works on backends */
    int res = 0;

    mbname_t *mbname = mbname_from_intname(name);
    const strarray_t *boxes = mbname_boxes(mbname);
    const char *prefix = config_getstring(IMAPOPT_DAVDRIVEPREFIX);
    if (strarray_size(boxes) && !strcmpsafe(prefix, strarray_nth(boxes, 0)))
        res = 1;

    mbname_free(&mbname);
    return res;
}

/*
 * If (internal) mailbox 'name' is a DAVNOTIFICATIONS mailbox
 * returns boolean
 */
EXPORTED int mboxname_isdavnotificationsmailbox(const char *name, int mbtype)
{
    if (mbtype & MBTYPE_COLLECTION) return 1;  /* Only works on backends */
    int res = 0;

    mbname_t *mbname = mbname_from_intname(name);
    const strarray_t *boxes = mbname_boxes(mbname);
    const char *prefix = config_getstring(IMAPOPT_DAVNOTIFICATIONSPREFIX);
    if (strarray_size(boxes) && !strcmpsafe(prefix, strarray_nth(boxes, 0)))
        res = 1;

    mbname_free(&mbname);
    return res;
}

/*
 * If (internal) mailbox 'name' is a user's "Notes" mailbox
 * returns boolean
 */
EXPORTED int mboxname_isnotesmailbox(const char *name, int mbtype __attribute__((unused)))
{
    int res = 0;

    mbname_t *mbname = mbname_from_intname(name);
    const strarray_t *boxes = mbname_boxes(mbname);
    const char *prefix = config_getstring(IMAPOPT_NOTESMAILBOX);
    if (strarray_size(boxes) && !strcmpsafe(prefix, strarray_nth(boxes, 0)))
        res = 1;

    mbname_free(&mbname);
    return res;
}

/*
 * If (internal) mailbox 'name' is a user's #jmapsubmission mailbox
 * returns boolean
 */
EXPORTED int mboxname_issubmissionmailbox(const char *name, int mbtype)
{
    if (mbtype & MBTYPE_SUBMISSION) return 1;  /* Only works on backends */
    int res = 0;

    mbname_t *mbname = mbname_from_intname(name);
    const strarray_t *boxes = mbname_boxes(mbname);
    const char *prefix = config_getstring(IMAPOPT_JMAPSUBMISSIONFOLDER);
    if (strarray_size(boxes) && !strcmpsafe(prefix, strarray_nth(boxes, 0)))
        res = 1;

    mbname_free(&mbname);
    return res;
}

/*
 * If (internal) mailbox 'name' is a user's #jmappushsubscription mailbox
 * returns boolean
 */
EXPORTED int mboxname_ispushsubscriptionmailbox(const char *name, int mbtype)
{
    if (mbtype & MBTYPE_PUSHSUBSCRIPTION) return 1;  /* Only works on backends */
    int res = 0;

    mbname_t *mbname = mbname_from_intname(name);
    const strarray_t *boxes = mbname_boxes(mbname);
    const char *prefix = config_getstring(IMAPOPT_JMAPPUSHSUBSCRIPTIONFOLDER);
    if (strarray_size(boxes) && !strcmpsafe(prefix, strarray_nth(boxes, 0)))
        res = 1;

    mbname_free(&mbname);
    return res;
}

/*
 * If (internal) mailbox 'name' is a user's #jmap upload mailbox
 * returns boolean
 */
EXPORTED int mboxname_isjmapuploadmailbox(const char *name, int mbtype __attribute__((unused)))
{
    int res = 0;

    mbname_t *mbname = mbname_from_intname(name);
    const strarray_t *boxes = mbname_boxes(mbname);
    const char *prefix = config_getstring(IMAPOPT_JMAPUPLOADFOLDER);

    if (strarray_size(boxes) && !strcmpsafe(prefix, strarray_nth(boxes, 0)))
        res = 1;

    mbname_free(&mbname);
    return res;
}

EXPORTED char *mboxname_user_mbox(const char *userid, const char *subfolder)
{
    if (!userid) return NULL;

    mbname_t *mbname = mbname_from_userid(userid);

    if (subfolder) {
        strarray_t *bits = strarray_split(subfolder, ".", 0);
        mbname_set_boxes(mbname, bits);
        strarray_free(bits);
    }

    char *res = xstrdup(mbname_intname(mbname));
    mbname_free(&mbname);

    return res;
}

EXPORTED char *mboxname_abook(const char *userid, const char *collection)
{
    mbname_t *mbname = mbname_from_userid(userid);

    mbname_push_boxes(mbname, config_getstring(IMAPOPT_ADDRESSBOOKPREFIX));
    if (collection) mbname_push_boxes(mbname, collection);

    char *res = xstrdup(mbname_intname(mbname));
    mbname_free(&mbname);

    return res;
}

EXPORTED char *mboxname_cal(const char *userid, const char *collection)
{
    mbname_t *mbname = mbname_from_userid(userid);

    mbname_push_boxes(mbname, config_getstring(IMAPOPT_CALENDARPREFIX));
    if (collection) mbname_push_boxes(mbname, collection);

    char *res = xstrdup(mbname_intname(mbname));
    mbname_free(&mbname);

    return res;
}

/*
 * Check whether two parts have the same userid.
 * Returns: 1 if the userids are the same, 0 if not.
 */
EXPORTED int mbname_same_userid(const mbname_t *a, const mbname_t *b)
{
    int r;

    r = strcmpsafe(a->domain, b->domain);
    if (!r)
        r = strcmpsafe(a->localpart, b->localpart);
    return !r;
}

/*
 * Check whether two mboxnames have the same userid.
 * Needed for some corner cases in the COPY command.
 * Returns: 1 if the userids are the same, 0 if not,
 *          or negative error.
 */
EXPORTED int mboxname_same_userid(const char *name1, const char *name2)
{
    int r;
    mbname_t *p1 = mbname_from_intname(name1);
    mbname_t *p2 = mbname_from_intname(name2);

    r = mbname_same_userid(p1, p2);

    mbname_free(&p1);
    mbname_free(&p2);

    return r;
}

/*
 * Apply site policy restrictions on mailbox names.
 * Restrictions are hardwired for now.
 * NOTE: '^' is '.' externally in unixhs, and invalid in unixhs
 *
 * The set of printable chars that are not in GOODCHARS are:
 *    !"%&/;<>\`{|}
 */
#define GOODCHARS " #$'()*+,-.0123456789:=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz~"
HIDDEN int mboxname_policycheck(const char *name)
{
    const char *p;
    int sawutf7 = 0;
    unsigned c1, c2, c3, c4, c5, c6, c7, c8;
    int ucs4;
    int namelen = strlen(name);
    int hasdom = 0;

    /* We reserve mailboxes.db keys beginning with $ for internal use
     * (e.g. $RACL), so don't allow a real mailbox to sneak in there.
     *
     * N.B This is only forbidden at the absolute top of the internal
     * namespace: stuff like "user.foo.$bar", "domain!user.foo.$bar",
     * "domain!$bar", and even "user.$bar" are all still valid here,
     * because none of those names start with $, and won't conflict.
     */
    if (name[0] == '$')
        return IMAP_MAILBOX_BADNAME;

    /* Skip policy check on mailbox created in delayed delete namespace
     * assuming the mailbox existed before and was OK then.
     * This should allow mailboxes that are extremely long to be
     * deleted when delayed_delete is enabled.
     * A thorough fix might remove the prefix and timestamp
     * then continue with the check
     */
    if (mboxname_isdeletedmailbox(name, NULL))
        return 0;

    if (namelen > MAX_MAILBOX_NAME)
        return IMAP_MAILBOX_BADNAME;

    /* find the virtual domain, if any.  We don't sanity check domain
       names yet - maybe we should */
    p = strchr(name, '!');
    if (p) {
        if (config_virtdomains) {
            name = p + 1;
            namelen = strlen(name);
            hasdom = 1;
        }
        else
            return IMAP_MAILBOX_BADNAME;
    }

    /* bad mbox patterns */
    // empty name
    if (!name[0]) return IMAP_MAILBOX_BADNAME;
    // leading dot
    if (name[0] == '.') return IMAP_MAILBOX_BADNAME;
    // leading ~
    if (name[0] == '~') return IMAP_MAILBOX_BADNAME;
    // trailing dot
    if (name[namelen-1] == '.') return IMAP_MAILBOX_BADNAME;
    // double dot (zero length path item)
    if (strstr(name, "..")) return IMAP_MAILBOX_BADNAME;
    // non-" " whitespace
    if (strchr(name, '\r')) return IMAP_MAILBOX_BADNAME;
    if (strchr(name, '\n')) return IMAP_MAILBOX_BADNAME;
    if (strchr(name, '\t')) return IMAP_MAILBOX_BADNAME;
    // top level user
    if (!strcmp(name, "user")) return IMAP_MAILBOX_BADNAME;
    // special users
    if (!strcmp(name, "user.anyone")) return IMAP_MAILBOX_BADNAME;
    if (!strcmp(name, "user.anonymous")) return IMAP_MAILBOX_BADNAME;
    // redundant but explicit ban on userids starting with '%'
    // (would conflict with backups of shared mailboxes)
    if (!strncmp(name, "user.%", 6)) return IMAP_MAILBOX_BADNAME;

    while (*name) {
        if (*name == '&') {
            /* Modified UTF-7 */
            name++;
            while (*name != '-') {
                if (sawutf7) {
                    /* Two adjacent utf7 sequences */
                    return IMAP_MAILBOX_BADNAME;
                }

                if ((c1 = CHARMOD64(*name++)) == XX ||
                    (c2 = CHARMOD64(*name++)) == XX ||
                    (c3 = CHARMOD64(*name++)) == XX) {
                    /* Non-base64 character */
                    return IMAP_MAILBOX_BADNAME;
                }
                ucs4 = (c1 << 10) | (c2 << 4) | (c3 >> 2);
                if ((ucs4 & 0xff80) == 0) {
                    /* US-ASCII character */
                    return IMAP_MAILBOX_BADNAME;
                }
                if (*name == '-') {
                    /* Trailing bits not zero */
                    if (c3 & 0x03) return IMAP_MAILBOX_BADNAME;

                    /* End of UTF-7 sequence */
                    break;
                }

                if ((c4 = CHARMOD64(*name++)) == XX ||
                    (c5 = CHARMOD64(*name++)) == XX ||
                    (c6 = CHARMOD64(*name++)) == XX) {
                    /* Non-base64 character */
                    return IMAP_MAILBOX_BADNAME;
                }
                ucs4 = ((c3 & 0x03) << 14) | (c4 << 8) | (c5 << 2) | (c6 >> 4);
                if ((ucs4 & 0xff80) == 0) {
                    /* US-ASCII character */
                    return IMAP_MAILBOX_BADNAME;
                }
                if (*name == '-') {
                    /* Trailing bits not zero */
                    if (c6 & 0x0f) return IMAP_MAILBOX_BADNAME;

                    /* End of UTF-7 sequence */
                    break;
                }

                if ((c7 = CHARMOD64(*name++)) == XX ||
                    (c8 = CHARMOD64(*name++)) == XX) {
                    /* Non-base64 character */
                    return IMAP_MAILBOX_BADNAME;
                }
                ucs4 = ((c6 & 0x0f) << 12) | (c7 << 6) | c8;
                if ((ucs4 & 0xff80) == 0) {
                    /* US-ASCII character */
                    return IMAP_MAILBOX_BADNAME;
                }
            }

            if (name[-1] == '&') sawutf7 = 0; /* '&-' is sequence for '&' */
            else sawutf7 = 1;

            name++;             /* Skip over terminating '-' */
        }
        else {
            if (!(strchr(GOODCHARS, *name) || (hasdom && *name == '!')))
                return IMAP_MAILBOX_BADNAME;
            name++;
            sawutf7 = 0;
        }
    }
    return 0;
}

EXPORTED int mboxname_is_prefix(const char *longstr, const char *shortstr)
{
    int longlen = strlen(longstr);
    int shortlen = strlen(shortstr);

    /* can't be a child */
    if (longlen < shortlen)
        return 0;

    /* don't match along same length */
    if (strncmp(longstr, shortstr, shortlen))
        return 0;

    /* longer, and not a separator */
    if (longlen > shortlen && longstr[shortlen] != '.')
        return 0;

    /* it's a match! */
    return 1;
}


EXPORTED void mboxname_hash(char *dest, size_t destlen,
                            const char *root,
                            const char *name)
{
    mbname_t *mbname = mbname_from_intname(name);
    struct buf buf = BUF_INITIALIZER;

    buf_setcstr(&buf, root);

    const char *domain = mbname_domain(mbname);
    strarray_t *boxes = strarray_dup(mbname_boxes(mbname));

    if (domain) {
        if (config_hashimapspool) {
            char c = dir_hash_c(domain, config_fulldirhash);
            buf_printf(&buf, "%s%c/%s", FNAME_DOMAINDIR, c, domain);
        }
        else {
            buf_printf(&buf, "%s%s", FNAME_DOMAINDIR, domain);
        }
    }

    if (mbname_localpart(mbname)) {
        strarray_unshift(boxes, mbname_localpart(mbname));
        strarray_unshift(boxes, "user");
    }
    if (mbname_isdeleted(mbname)) {
        struct buf dbuf = BUF_INITIALIZER;
        buf_printf(&dbuf, "%X", (unsigned)mbname_isdeleted(mbname));
        strarray_unshift(boxes, config_getstring(IMAPOPT_DELETEDPREFIX));
        strarray_push(boxes, buf_cstring(&dbuf));
        buf_free(&dbuf);
    }

    if (config_hashimapspool && strarray_size(boxes)) {
        const char *idx = strarray_size(boxes) > 1 ? strarray_nth(boxes, 1) : strarray_nth(boxes, 0);
        char c = dir_hash_c(idx, config_fulldirhash);
        buf_printf(&buf, "/%c", c);
    }

    int i;
    for (i = 0; i < strarray_size(boxes); i++) {
        buf_putc(&buf, '/');
        _append_intbuf(&buf, strarray_nth(boxes, i));
    }

    /* for now, keep API even though we're doing a buffer inside here */
    strncpy(dest, buf_cstring(&buf), destlen);

    buf_free(&buf);
    strarray_free(boxes);
    mbname_free(&mbname);
}

/* note: mboxname must be internal */
EXPORTED char *mboxname_datapath(const char *partition,
                                 const char *mboxname,
                                 const char *uniqueid __attribute__((unused)),
                                 unsigned long uid)
{
    static char pathresult[MAX_MAILBOX_PATH+1];
    const char *root;

    if (!partition) return NULL;

    root = config_partitiondir(partition);
    if (!root) return NULL;

    if (!mboxname) {
        xstrncpy(pathresult, root, MAX_MAILBOX_PATH);
        return pathresult;
    }

    mboxname_hash(pathresult, MAX_MAILBOX_PATH, root, mboxname);

    if (uid) {
        int len = strlen(pathresult);
        snprintf(pathresult + len, MAX_MAILBOX_PATH - len, "/%lu.", uid);
    }
    pathresult[MAX_MAILBOX_PATH] = '\0';

    if (strlen(pathresult) == MAX_MAILBOX_PATH)
        return NULL;

    return pathresult;
}

/* note: mboxname must be internal */
EXPORTED char *mboxname_archivepath(const char *partition,
                                    const char *mboxname,
                                    const char *uniqueid __attribute__((unused)),
                                    unsigned long uid)
{
    static char pathresult[MAX_MAILBOX_PATH+1];
    const char *root;

    if (!partition) return NULL;

    root = config_archivepartitiondir(partition);
    if (!root) root = config_partitiondir(partition);
    if (!root) return NULL;

    /* XXX - dedup with datapath above - but make sure to keep the results
     * in separate buffers and/or audit the callers */
    if (!mboxname) {
        xstrncpy(pathresult, root, MAX_MAILBOX_PATH);
        return pathresult;
    }

    mboxname_hash(pathresult, MAX_MAILBOX_PATH, root, mboxname);

    if (uid) {
        int len = strlen(pathresult);
        snprintf(pathresult + len, MAX_MAILBOX_PATH - len, "/%lu.", uid);
    }
    pathresult[MAX_MAILBOX_PATH] = '\0';

    if (strlen(pathresult) == MAX_MAILBOX_PATH)
        return NULL;

    return pathresult;
}

char *mboxname_lockpath(const char *mboxname)
{
    return mboxname_lockpath_suffix(mboxname, ".lock");
}

char *mboxname_lockpath_suffix(const char *mboxname,
                               const char *suffix)
{
    static char lockresult[MAX_MAILBOX_PATH+1];
    char basepath[MAX_MAILBOX_PATH+1];
    const char *root = config_getstring(IMAPOPT_MBOXNAME_LOCKPATH);
    int len;

    if (!root) {
        snprintf(basepath, MAX_MAILBOX_PATH, "%s/lock", config_dir);
        root = basepath;
    }

    mboxname_hash(lockresult, MAX_MAILBOX_PATH, root, mboxname);

    len = strlen(lockresult);
    snprintf(lockresult + len, MAX_MAILBOX_PATH - len, "%s", suffix);
    lockresult[MAX_MAILBOX_PATH] = '\0';

    if (strlen(lockresult) == MAX_MAILBOX_PATH)
        return NULL;

    return lockresult;
}

EXPORTED char *mboxname_metapath(const char *partition,
                                 const char *mboxname,
                                 const char *uniqueid __attribute__((unused)),
                                 int metafile,
                                 int isnew)
{
    static char metaresult[MAX_MAILBOX_PATH];
    int metaflag = 0;
    int archiveflag = 0;
    const char *root = NULL;
    const char *filename = NULL;
    char confkey[256];

    if (!partition) return NULL;

    *confkey = '\0';

    switch (metafile) {
    case META_HEADER:
        snprintf(confkey, 256, "metadir-header-%s", partition);
        metaflag = IMAP_ENUM_METAPARTITION_FILES_HEADER;
        filename = FNAME_HEADER;
        break;
    case META_INDEX:
        snprintf(confkey, 256, "metadir-index-%s", partition);
        metaflag = IMAP_ENUM_METAPARTITION_FILES_INDEX;
        filename = FNAME_INDEX;
        break;
    case META_CACHE:
        snprintf(confkey, 256, "metadir-cache-%s", partition);
        metaflag = IMAP_ENUM_METAPARTITION_FILES_CACHE;
        filename = FNAME_CACHE;
        break;
    case META_EXPUNGE:
        /* not movable, it's only old */
        metaflag = IMAP_ENUM_METAPARTITION_FILES_EXPUNGE;
        filename = FNAME_EXPUNGE;
        break;
    case META_SQUAT:
        snprintf(confkey, 256, "metadir-squat-%s", partition);
        metaflag = IMAP_ENUM_METAPARTITION_FILES_SQUAT;
        filename = FNAME_SQUAT;
        break;
    case META_ANNOTATIONS:
        snprintf(confkey, 256, "metadir-index-%s", partition);
        metaflag = IMAP_ENUM_METAPARTITION_FILES_ANNOTATIONS;
        filename = FNAME_ANNOTATIONS;
        break;
#ifdef WITH_DAV
    case META_DAV:
        snprintf(confkey, 256, "metadir-dav-%s", partition);
        metaflag = IMAP_ENUM_METAPARTITION_FILES_DAV;
        filename = FNAME_DAV;
        break;
#endif
    case META_ARCHIVECACHE:
        snprintf(confkey, 256, "metadir-archivecache-%s", partition);
        metaflag = IMAP_ENUM_METAPARTITION_FILES_ARCHIVECACHE;
        filename = FNAME_CACHE;
        archiveflag = 1;
        break;
    case 0:
        break;
    default:
        fatal("Unknown meta file requested", EX_SOFTWARE);
    }

    if (*confkey)
        root = config_getoverflowstring(confkey, NULL);

    if (!root && (!metaflag || (config_metapartition_files & metaflag)))
        root = config_metapartitiondir(partition);

    if (!root && archiveflag)
        root = config_archivepartitiondir(partition);

    if (!root)
        root = config_partitiondir(partition);

    if (!root)
        return NULL;

    if (!mboxname) {
        xstrncpy(metaresult, root, MAX_MAILBOX_PATH);
        return metaresult;
    }

    mboxname_hash(metaresult, MAX_MAILBOX_PATH, root, mboxname);

    if (filename) {
        int len = strlen(metaresult);
        if (isnew)
            snprintf(metaresult + len, MAX_MAILBOX_PATH - len, "%s.NEW", filename);
        else
            snprintf(metaresult + len, MAX_MAILBOX_PATH - len, "%s", filename);
    }

    if (strlen(metaresult) >= MAX_MAILBOX_PATH)
        return NULL;

    return metaresult;
}

EXPORTED void mboxname_todeleted(const char *name, char *result, int withtime)
{
    int domainlen = 0;
    char *p;
    const char *deletedprefix = config_getstring(IMAPOPT_DELETEDPREFIX);

    xstrncpy(result, name, MAX_MAILBOX_BUFFER);

    if (config_virtdomains && (p = strchr(name, '!')))
        domainlen = p - name + 1;

    if (withtime) {
        struct timeval tv;
        gettimeofday( &tv, NULL );
        snprintf(result+domainlen, MAX_MAILBOX_BUFFER-domainlen, "%s.%s.%X",
                 deletedprefix, name+domainlen, (unsigned) tv.tv_sec);
    } else {
        snprintf(result+domainlen, MAX_MAILBOX_BUFFER-domainlen, "%s.%s",
                 deletedprefix, name+domainlen);
    }
}

EXPORTED int mboxname_make_parent(char *name)
{
    int domainlen = 0;
    char *p;

    if (config_virtdomains && (p = strchr(name, '!')))
        domainlen = p - name + 1;

    if (!name[0] || !strcmp(name+domainlen, "user"))
        return 0;                               /* stop now */

    p = strrchr(name, '.');

    if (p && (p - name > domainlen))            /* don't split subdomain */
        *p = '\0';
    else if (!name[domainlen])                  /* server entry */
        name[0] = '\0';
    else                                        /* domain entry */
        name[domainlen] = '\0';

    return 1;
}

EXPORTED int mboxname_contains_parent(const char *mboxname, const char *prev)
{
    /* no names, definitely can't be parent! */
    if (!mboxname) return 0;
    if (!prev) return 0;

    char *parent = xstrdup(mboxname);

    /* this mailbox is just "user"? prev will always contain that */
    if (!mboxname_make_parent(parent)) {
        free(parent);
        return 1;
    }

    if (mboxname_is_prefix(prev, parent)) {
        /* it's not different?  Great - there's no missing intermediate */
        free(parent);
        return 1;
    }

    /* OK, it doesn't contain the parent for sure */
    free(parent);
    return 0;
}

/* NOTE: caller must free, which is different from almost every
 * other interface in the whole codebase.  Grr */
EXPORTED char *mboxname_conf_getpath(const mbname_t *mbname, const char *suffix)
{
    char *fname = NULL;
    char c[2], d[2];

    if (mbname->domain) {
        if (mbname->localpart) {
            if (suffix) {
                fname = strconcat(config_dir,
                                  FNAME_DOMAINDIR,
                                  dir_hash_b(mbname->domain, config_fulldirhash, d),
                                  "/", mbname->domain,
                                  FNAME_USERDIR,
                                  dir_hash_b(mbname->localpart, config_fulldirhash, c),
                                  "/", mbname->localpart, ".", suffix,
                                  (char *)NULL);
            }
            else {
                fname = strconcat(config_dir,
                                  FNAME_DOMAINDIR,
                                  dir_hash_b(mbname->domain, config_fulldirhash, d),
                                  "/", mbname->domain,
                                  FNAME_USERDIR,
                                  dir_hash_b(mbname->localpart, config_fulldirhash, c),
                                  (char *)NULL);
            }
        }
        else {
            if (suffix) {
                fname = strconcat(config_dir,
                                  FNAME_DOMAINDIR,
                                  dir_hash_b(mbname->domain, config_fulldirhash, d),
                                  "/", mbname->domain,
                                  "/", FNAME_SHAREDPREFIX, ".", suffix,
                                  (char *)NULL);
            }
            else {
                fname = strconcat(config_dir,
                                  FNAME_DOMAINDIR,
                                  dir_hash_b(mbname->domain, config_fulldirhash, d),
                                  "/", mbname->domain,
                                  (char *)NULL);
            }
        }
    }
    else {
        if (mbname->localpart) {
            if (suffix) {
                fname = strconcat(config_dir,
                                  FNAME_USERDIR,
                                  dir_hash_b(mbname->localpart, config_fulldirhash, c),
                                  "/", mbname->localpart, ".", suffix,
                                  (char *)NULL);
            }
            else {
                fname = strconcat(config_dir,
                                  FNAME_USERDIR,
                                  dir_hash_b(mbname->localpart, config_fulldirhash, c),
                                  (char *)NULL);
            }
        }
        else {
            if (suffix) {
                fname = strconcat(config_dir,
                                  "/", FNAME_SHAREDPREFIX, ".", suffix,
                                  (char *)NULL);
            }
            else {
                fname = xstrdup(config_dir);
            }
        }
    }

    return fname;
}

/* ========================= COUNTERS ============================ */

static bit64 mboxname_readval_old(const char *mboxname, const char *metaname)
{
    bit64 fileval = 0;
    mbname_t *mbname = NULL;
    char *fname = NULL;
    const char *base = NULL;
    size_t len = 0;
    int fd = -1;

    mbname = mbname_from_intname(mboxname);

    fname = mboxname_conf_getpath(mbname, metaname);
    if (!fname) goto done;

    fd = open(fname, O_RDONLY);

    /* read the value - note: we don't care if it's being rewritten,
     * we'll still get a consistent read on either the old or new
     * value */
    if (fd != -1) {
        struct stat sbuf;
        if (fstat(fd, &sbuf)) {
            syslog(LOG_ERR, "IOERROR: failed to stat fd %s: %m", fname);
            goto done;
        }
        if (sbuf.st_size) {
            map_refresh(fd, 1, &base, &len, sbuf.st_size, metaname, mboxname);
            parsenum(base, NULL, sbuf.st_size, &fileval);
            map_free(&base, &len);
        }
    }

 done:
    if (fd != -1) close(fd);
    mbname_free(&mbname);
    free(fname);
    return fileval;
}

#define MV_VERSION 5

#define MV_OFF_GENERATION 0
#define MV_OFF_VERSION 4
#define MV_OFF_HIGHESTMODSEQ 8
#define MV_OFF_MAILMODSEQ 16
#define MV_OFF_CALDAVMODSEQ 24
#define MV_OFF_CARDDAVMODSEQ 32
#define MV_OFF_NOTESMODSEQ 40
#define MV_OFF_MAILFOLDERSMODSEQ 48
#define MV_OFF_CALDAVFOLDERSMODSEQ 56
#define MV_OFF_CARDDAVFOLDERSMODSEQ 64
#define MV_OFF_NOTESFOLDERSMODSEQ 72
#define MV_OFF_QUOTAMODSEQ 80
#define MV_OFF_RACLMODSEQ 88
#define MV_OFF_SUBMISSIONMODSEQ 96
#define MV_OFF_SUBMISSIONFOLDERSMODSEQ 104
#define MV_OFF_UIDVALIDITY 112
#define MV_OFF_CRC 116
#define MV_LENGTH 120

/* NOTE: you need a MV_LENGTH byte base here */
static int mboxname_buf_to_counters(const char *base, size_t len, struct mboxname_counters *vals)
{
    vals->generation = ntohl(*((uint32_t *)(base)));
    vals->version = ntohl(*((uint32_t *)(base+4)));

    /* dodgy broken version storage in v0 code, it could be anything */
    if (len == 48) vals->version = 0;

    switch (vals->version) {
    case 0:
        if (len != 48) return IMAP_MAILBOX_CHECKSUM;
        if (crc32_map(base, 44) != ntohl(*((uint32_t *)(base+44))))
            return IMAP_MAILBOX_CHECKSUM;

        vals->highestmodseq = ntohll(*((uint64_t *)(base+8)));
        vals->mailmodseq = ntohll(*((uint64_t *)(base+16)));
        vals->caldavmodseq = ntohll(*((uint64_t *)(base+24)));
        vals->carddavmodseq = ntohll(*((uint64_t *)(base+32)));
        vals->notesmodseq = 0;
        vals->mailfoldersmodseq = 0;
        vals->caldavfoldersmodseq = 0;
        vals->carddavfoldersmodseq = 0;
        vals->notesfoldersmodseq = 0;
        vals->uidvalidity = ntohl(*((uint32_t *)(base+40)));
        break;

    case 1:
        if (len != 56) return IMAP_MAILBOX_CHECKSUM;
        if (crc32_map(base, 52) != ntohl(*((uint32_t *)(base+52))))
            return IMAP_MAILBOX_CHECKSUM;

        vals->highestmodseq = ntohll(*((uint64_t *)(base+8)));
        vals->mailmodseq = ntohll(*((uint64_t *)(base+16)));
        vals->caldavmodseq = ntohll(*((uint64_t *)(base+24)));
        vals->carddavmodseq = ntohll(*((uint64_t *)(base+32)));
        vals->notesmodseq = ntohll(*((uint64_t *)(base+40)));
        vals->mailfoldersmodseq = 0;
        vals->caldavfoldersmodseq = 0;
        vals->carddavfoldersmodseq = 0;
        vals->notesfoldersmodseq = 0;
        vals->uidvalidity = ntohl(*((uint32_t *)(base+48)));
        break;

    case 2:
        if (len != 64) return IMAP_MAILBOX_CHECKSUM;
        if (crc32_map(base, 60) != ntohl(*((uint32_t *)(base+60))))
            return IMAP_MAILBOX_CHECKSUM;

        vals->highestmodseq = ntohll(*((uint64_t *)(base+8)));
        vals->mailmodseq = ntohll(*((uint64_t *)(base+16)));
        vals->caldavmodseq = ntohll(*((uint64_t *)(base+24)));
        vals->carddavmodseq = ntohll(*((uint64_t *)(base+32)));
        vals->notesmodseq = ntohll(*((uint64_t *)(base+40)));
        vals->mailfoldersmodseq = ntohll(*((uint32_t *)(base+48)));
        vals->caldavfoldersmodseq = 0;
        vals->carddavfoldersmodseq = 0;
        vals->notesfoldersmodseq = 0;
        vals->uidvalidity = ntohl(*((uint32_t *)(base+56)));
        break;

    case 3:
        if (len != 88) return IMAP_MAILBOX_CHECKSUM;
        if (crc32_map(base, 84) != ntohl(*((uint32_t *)(base+84))))
            return IMAP_MAILBOX_CHECKSUM;

        vals->highestmodseq = ntohll(*((uint64_t *)(base+8)));
        vals->mailmodseq = ntohll(*((uint64_t *)(base+16)));
        vals->caldavmodseq = ntohll(*((uint64_t *)(base+24)));
        vals->carddavmodseq = ntohll(*((uint64_t *)(base+32)));
        vals->notesmodseq = ntohll(*((uint64_t *)(base+40)));
        vals->mailfoldersmodseq = ntohll(*((uint64_t *)(base+48)));
        vals->caldavfoldersmodseq = ntohll(*((uint64_t *)(base+56)));
        vals->carddavfoldersmodseq = ntohll(*((uint64_t *)(base+64)));
        vals->notesfoldersmodseq = ntohll(*((uint64_t *)(base+72)));
        vals->uidvalidity = ntohl(*((uint32_t *)(base+80)));
        break;

    case 4:
        if (len != 104) return IMAP_MAILBOX_CHECKSUM;
        if (crc32_map(base, 100) != ntohl(*((uint32_t *)(base+100))))
            return IMAP_MAILBOX_CHECKSUM;

        vals->highestmodseq = ntohll(*((uint64_t *)(base+8)));
        vals->mailmodseq = ntohll(*((uint64_t *)(base+16)));
        vals->caldavmodseq = ntohll(*((uint64_t *)(base+24)));
        vals->carddavmodseq = ntohll(*((uint64_t *)(base+32)));
        vals->notesmodseq = ntohll(*((uint64_t *)(base+40)));
        vals->mailfoldersmodseq = ntohll(*((uint64_t *)(base+48)));
        vals->caldavfoldersmodseq = ntohll(*((uint64_t *)(base+56)));
        vals->carddavfoldersmodseq = ntohll(*((uint64_t *)(base+64)));
        vals->notesfoldersmodseq = ntohll(*((uint64_t *)(base+72)));
        vals->quotamodseq = ntohll(*((uint64_t *)(base+80)));
        vals->raclmodseq = ntohll(*((uint64_t *)(base+88)));
        vals->uidvalidity = ntohl(*((uint32_t *)(base+96)));
        break;

    case 5:
        if (len != 120) return IMAP_MAILBOX_CHECKSUM;
        if (crc32_map(base, 116) != ntohl(*((uint32_t *)(base+116))))
            return IMAP_MAILBOX_CHECKSUM;

        vals->highestmodseq = ntohll(*((uint64_t *)(base+8)));
        vals->mailmodseq = ntohll(*((uint64_t *)(base+16)));
        vals->caldavmodseq = ntohll(*((uint64_t *)(base+24)));
        vals->carddavmodseq = ntohll(*((uint64_t *)(base+32)));
        vals->notesmodseq = ntohll(*((uint64_t *)(base+40)));
        vals->mailfoldersmodseq = ntohll(*((uint64_t *)(base+48)));
        vals->caldavfoldersmodseq = ntohll(*((uint64_t *)(base+56)));
        vals->carddavfoldersmodseq = ntohll(*((uint64_t *)(base+64)));
        vals->notesfoldersmodseq = ntohll(*((uint64_t *)(base+72)));
        vals->quotamodseq = ntohll(*((uint64_t *)(base+80)));
        vals->raclmodseq = ntohll(*((uint64_t *)(base+88)));
        vals->submissionmodseq = ntohll(*((uint64_t *)(base+96)));
        vals->submissionfoldersmodseq = ntohll(*((uint64_t *)(base+104)));
        vals->uidvalidity = ntohl(*((uint32_t *)(base+112)));
        break;

    default:
        return IMAP_MAILBOX_BADFORMAT;
    }

    return 0;
}

/* NOTE: you need a MV_LENGTH buffer to write into, aligned on 8 byte boundaries */
static void mboxname_counters_to_buf(const struct mboxname_counters *vals, char *base)
{
    *((uint32_t *)(base+MV_OFF_GENERATION)) = htonl(vals->generation);
    *((uint32_t *)(base+MV_OFF_VERSION)) = htonl(MV_VERSION);
    align_htonll(base+MV_OFF_HIGHESTMODSEQ, vals->highestmodseq);
    align_htonll(base+MV_OFF_MAILMODSEQ, vals->mailmodseq);
    align_htonll(base+MV_OFF_CALDAVMODSEQ, vals->caldavmodseq);
    align_htonll(base+MV_OFF_CARDDAVMODSEQ, vals->carddavmodseq);
    align_htonll(base+MV_OFF_NOTESMODSEQ, vals->notesmodseq);
    align_htonll(base+MV_OFF_MAILFOLDERSMODSEQ, vals->mailfoldersmodseq);
    align_htonll(base+MV_OFF_CALDAVFOLDERSMODSEQ, vals->caldavfoldersmodseq);
    align_htonll(base+MV_OFF_CARDDAVFOLDERSMODSEQ, vals->carddavfoldersmodseq);
    align_htonll(base+MV_OFF_NOTESFOLDERSMODSEQ, vals->notesfoldersmodseq);
    *((uint32_t *)(base+MV_OFF_UIDVALIDITY)) = htonl(vals->uidvalidity);
    align_htonll(base+MV_OFF_QUOTAMODSEQ, vals->quotamodseq);
    align_htonll(base+MV_OFF_RACLMODSEQ, vals->raclmodseq);
    align_htonll(base+MV_OFF_SUBMISSIONMODSEQ, vals->submissionmodseq);
    align_htonll(base+MV_OFF_SUBMISSIONFOLDERSMODSEQ, vals->submissionfoldersmodseq);
    *((uint32_t *)(base+MV_OFF_CRC)) = htonl(crc32_map(base, MV_OFF_CRC));
}

/* XXX - inform about errors?  Any error causes the value of at least
   last+1 to be returned.  An error only on writing causes
   max(last, fileval) + 1 to still be returned */
static int mboxname_load_counters(const char *mboxname, struct mboxname_counters *vals, int *fdp)
{
    int fd = -1;
    char *fname = NULL;
    struct stat sbuf, fbuf;
    const char *base = NULL;
    size_t len = 0;
    mbname_t *mbname = NULL;
    int r = 0;

    memset(vals, 0, sizeof(struct mboxname_counters));

    mbname = mbname_from_intname(mboxname);

    fname = mboxname_conf_getpath(mbname, "counters");
    if (!fname) {
        r = IMAP_MAILBOX_BADNAME;
        goto done;
    }

    /* get a blocking lock on fd */
    for (;;) {
        fd = open(fname, O_RDWR | O_CREAT, 0644);
        if (fd == -1) {
            /* OK to not exist - try creating the directory first */
            if (cyrus_mkdir(fname, 0755)) goto done;
            fd = open(fname, O_RDWR | O_CREAT, 0644);
        }
        if (fd == -1) {
            syslog(LOG_ERR, "IOERROR: failed to create %s: %m", fname);
            goto done;
        }
        if (lock_blocking(fd, fname)) {
            syslog(LOG_ERR, "IOERROR: failed to lock %s: %m", fname);
            goto done;
        }
        if (fstat(fd, &sbuf)) {
            syslog(LOG_ERR, "IOERROR: failed to stat fd %s: %m", fname);
            goto done;
        }
        if (stat(fname, &fbuf)) {
            syslog(LOG_ERR, "IOERROR: failed to stat file %s: %m", fname);
            goto done;
        }
        if (sbuf.st_ino == fbuf.st_ino) break;
        lock_unlock(fd, fname);
        close(fd);
        fd = -1;
    }

    if (fd < 0) {
        r = IMAP_IOERROR;
        goto done;
    }

    if (sbuf.st_size >= 8) {
        /* read the old value */
        map_refresh(fd, 1, &base, &len, sbuf.st_size, "counters", mboxname);
        if (len >= 8) {
            r = mboxname_buf_to_counters(base, len, vals);
        }
        map_free(&base, &len);
    }
    else {
        /* going to have to read the old files */
        vals->mailmodseq = vals->caldavmodseq = vals->carddavmodseq =
            vals->highestmodseq = mboxname_readval_old(mboxname, "modseq");
        vals->uidvalidity = mboxname_readval_old(mboxname, "uidvalidity");
    }

done:
    if (r) {
        if (fd != -1) {
            lock_unlock(fd, fname);
            close(fd);
        }
    }
    else {
        /* maintain the lock until we're done */
        *fdp = fd;
    }
    mbname_free(&mbname);
    free(fname);
    return r;
}

static int mboxname_set_counters(const char *mboxname, struct mboxname_counters *vals, int fd)
{
    char *fname = NULL;
    mbname_t *mbname = NULL;
    char buf[MV_LENGTH];
    char newfname[MAX_MAILBOX_PATH];
    int newfd = -1;
    int n = 0;
    int r = 0;

    mbname = mbname_from_intname(mboxname);

    fname = mboxname_conf_getpath(mbname, "counters");
    if (!fname) {
        r = IMAP_MAILBOX_BADNAME;
        goto done;
    }

    snprintf(newfname, MAX_MAILBOX_PATH, "%s.NEW", fname);
    newfd = open(newfname, O_CREAT | O_TRUNC | O_WRONLY, 0644);
    if (newfd == -1) {
        r = IMAP_IOERROR;
        syslog(LOG_ERR, "IOERROR: failed to open for write %s: %m", newfname);
        goto done;
    }

    /* it's a new generation! */
    vals->generation++;

    mboxname_counters_to_buf(vals, buf);
    n = retry_write(newfd, buf, MV_LENGTH);
    if (n < 0) {
        r = IMAP_IOERROR;
        syslog(LOG_ERR, "IOERROR: failed to write %s: %m", newfname);
        goto done;
    }

    if (fdatasync(newfd)) {
        r = IMAP_IOERROR;
        syslog(LOG_ERR, "IOERROR: failed to fdatasync %s: %m", newfname);
        goto done;
    }

    close(newfd);
    newfd = -1;

    if (rename(newfname, fname)) {
        r = IMAP_IOERROR;
        syslog(LOG_ERR, "IOERROR: failed to rename %s: %m", newfname);
        goto done;
    }

 done:
    if (newfd != -1) close(newfd);
    if (fd != -1) {
        lock_unlock(fd, fname);
        close(fd);
    }
    mbname_free(&mbname);
    free(fname);

    return r;
}

static int mboxname_unload_counters(int fd)
{
    lock_unlock(fd, NULL);
    close(fd);
    return 0;
}

EXPORTED int mboxname_read_counters(const char *mboxname, struct mboxname_counters *vals)
{
    int r = 0;
    mbname_t *mbname = NULL;
    struct stat sbuf;
    char *fname = NULL;
    const char *base = NULL;
    size_t len = 0;
    int fd = -1;

    memset(vals, 0, sizeof(struct mboxname_counters));

    mbname = mbname_from_intname(mboxname);

    fname = mboxname_conf_getpath(mbname, "counters");
    if (!fname) {
        r = IMAP_MAILBOX_BADNAME;
        goto done;
    }

    fd = open(fname, O_RDONLY);

    /* if no file, import from the old files potentially, and write a file regardless */
    if (fd < 0) {
        /* race => multiple rewrites, won't hurt too much */
        r = mboxname_load_counters(mboxname, vals, &fd);
        if (r) goto done;
        r = mboxname_set_counters(mboxname, vals, fd);
        fd = -1;
        if (r) goto done;
        free(fname);
        fname = mboxname_conf_getpath(mbname, "modseq");
        if (fname) unlink(fname);
        free(fname);
        fname = mboxname_conf_getpath(mbname, "uidvalidity");
        if (fname) unlink(fname);
        goto done;
    }

    if (fstat(fd, &sbuf)) {
        syslog(LOG_ERR, "IOERROR: failed to stat fd %s: %m", fname);
        r = IMAP_IOERROR;
        goto done;
    }

    if (sbuf.st_size >= 8) {
        map_refresh(fd, 1, &base, &len, sbuf.st_size, "counters", mboxname);
        if (len >= 8)
            r = mboxname_buf_to_counters(base, len, vals);
        map_free(&base, &len);
    }

 done:
    if (fd != -1) close(fd);
    mbname_free(&mbname);
    free(fname);
    return r;
}

enum domodseq { MAILBOXMODSEQ, QUOTAMODSEQ, RACLMODSEQ };

static modseq_t mboxname_domodseq(const char *mboxname,
                                  modseq_t last,
                                  enum domodseq domodseq,
                                  int mbtype,   // for MAILBOXMODSEQ
                                  int dofolder, // for MAILBOXMODSEQ
                                  modseq_t add)
{
    struct mboxname_counters counters;
    struct mboxname_counters oldcounters;
    modseq_t *typemodseqp = NULL;
    modseq_t *foldersmodseqp = NULL;
    int fd = -1;

    if (!config_getswitch(IMAPOPT_CONVERSATIONS))
        return last + add;

    /* XXX error handling */
    if (mboxname_load_counters(mboxname, &counters, &fd))
        return last + add;

    oldcounters = counters;

    if (domodseq == MAILBOXMODSEQ) {
        if (mboxname_isaddressbookmailbox(mboxname, mbtype)) {
            typemodseqp = &counters.carddavmodseq;
            foldersmodseqp = &counters.carddavfoldersmodseq;
        }
        else if (mboxname_iscalendarmailbox(mboxname, mbtype)) {
            typemodseqp = &counters.caldavmodseq;
            foldersmodseqp = &counters.caldavfoldersmodseq;
        }
        else if (mboxname_isnotesmailbox(mboxname, mbtype)) {
            typemodseqp = &counters.notesmodseq;
            foldersmodseqp = &counters.notesfoldersmodseq;
        }
        else if (mboxname_issubmissionmailbox(mboxname, mbtype)) {
            typemodseqp = &counters.submissionmodseq;
            foldersmodseqp = &counters.submissionfoldersmodseq;
        }
        else {
            typemodseqp = &counters.mailmodseq;
            foldersmodseqp = &counters.mailfoldersmodseq;
        }
    }
    else if (domodseq == QUOTAMODSEQ) {
        typemodseqp = &counters.quotamodseq;
        dofolder = 0;
    }
    else if (domodseq == RACLMODSEQ) {
        typemodseqp = &counters.raclmodseq;
        dofolder = 0;
    }

    /* make sure all counters are at least the old value */
    if (counters.highestmodseq < last)
        counters.highestmodseq = last;
    if (*typemodseqp < last)
        *typemodseqp = last;
    if (dofolder && *foldersmodseqp < last)
        *foldersmodseqp = last;

    /* if adding, bring all counters up to the overall highest modseq */
    if (add) {
        counters.highestmodseq += add;
        *typemodseqp = counters.highestmodseq;
        if (dofolder) *foldersmodseqp = counters.highestmodseq;
    }

    if (memcmp(&counters, &oldcounters, sizeof(struct mboxname_counters)))
        mboxname_set_counters(mboxname, &counters, fd);
    else
        mboxname_unload_counters(fd);

    return counters.highestmodseq;
}

EXPORTED modseq_t mboxname_nextmodseq(const char *mboxname, modseq_t last, int mbtype, int dofolder)
{
    return mboxname_domodseq(mboxname, last, MAILBOXMODSEQ, mbtype, dofolder, 1);
}

EXPORTED modseq_t mboxname_setmodseq(const char *mboxname, modseq_t last, int mbtype, int dofolder)
{
    return mboxname_domodseq(mboxname, last, MAILBOXMODSEQ, mbtype, dofolder, 0);
}

EXPORTED modseq_t mboxname_readquotamodseq(const char *mboxname)
{
    struct mboxname_counters counters;

    if (!config_getswitch(IMAPOPT_CONVERSATIONS))
        return 0;

    if (mboxname_read_counters(mboxname, &counters))
        return 0;

    return counters.quotamodseq;
}

EXPORTED modseq_t mboxname_nextquotamodseq(const char *mboxname, modseq_t last)
{
    return mboxname_domodseq(mboxname, last, QUOTAMODSEQ, 0, 0, 1);
}

EXPORTED modseq_t mboxname_setquotamodseq(const char *mboxname, modseq_t last)
{
    return mboxname_domodseq(mboxname, last, QUOTAMODSEQ, 0, 0, 0);
}

EXPORTED modseq_t mboxname_readraclmodseq(const char *mboxname)
{
    struct mboxname_counters counters;

    if (!config_getswitch(IMAPOPT_CONVERSATIONS))
        return 0;

    if (!mboxname_isusermailbox(mboxname, /*isinbox*/1))
        return 0;  // raclmodseq is only defined on user inboxes

    if (mboxname_read_counters(mboxname, &counters))
        return 0;

    return counters.raclmodseq;
}

EXPORTED modseq_t mboxname_nextraclmodseq(const char *mboxname, modseq_t last)
{
    return mboxname_domodseq(mboxname, last, RACLMODSEQ, 0, 0, 1);
}

EXPORTED modseq_t mboxname_setraclmodseq(const char *mboxname, modseq_t last)
{
    return mboxname_domodseq(mboxname, last, RACLMODSEQ, 0, 0, 0);
}

EXPORTED uint32_t mboxname_readuidvalidity(const char *mboxname)
{
    struct mboxname_counters counters;

    if (!config_getswitch(IMAPOPT_CONVERSATIONS))
        return 0;

    if (mboxname_read_counters(mboxname, &counters))
        return 0;

    return counters.uidvalidity;
}

EXPORTED uint32_t mboxname_nextuidvalidity(const char *mboxname, uint32_t last)
{
    struct mboxname_counters counters;
    int fd = -1;

    if (!config_getswitch(IMAPOPT_CONVERSATIONS))
        return last + 1;

    /* XXX error handling */
    if (mboxname_load_counters(mboxname, &counters, &fd))
        return last + 1;

    if (counters.uidvalidity < last)
        counters.uidvalidity = last;

    counters.uidvalidity++;

    /* always set, because we always increased */
    mboxname_set_counters(mboxname, &counters, fd);

    return counters.uidvalidity;
}

EXPORTED uint32_t mboxname_setuidvalidity(const char *mboxname, uint32_t val)
{
    struct mboxname_counters counters;
    int fd = -1;
    int dirty = 0;

    if (!config_getswitch(IMAPOPT_CONVERSATIONS))
        return val;

    /* XXX error handling */
    if (mboxname_load_counters(mboxname, &counters, &fd))
        return val;

    if (counters.uidvalidity < val) {
        counters.uidvalidity = val;
        dirty = 1;
    }

    if (dirty)
        mboxname_set_counters(mboxname, &counters, fd);
    else
        mboxname_unload_counters(fd);

    return val;
}

EXPORTED char *mboxname_common_ancestor(const char *mboxname1, const char *mboxname2)
{
    mbname_t *mbname1 = mbname_from_intname(mboxname1);
    mbname_t *mbname2 = mbname_from_intname(mboxname2);
    char *ancestor = NULL;

    if (!mbname_same_userid(mbname1, mbname2))
        goto done;

    const strarray_t *boxes1 = mbname_boxes(mbname1);
    const strarray_t *boxes2 = mbname_boxes(mbname2);
    int len = boxes1->count < boxes2->count ? boxes1->count : boxes2->count;
    int i;
    for (i = 0; i < len - 1; i++) {
        if (strcmp(strarray_nth(boxes1, i), strarray_nth(boxes2, i)))
            break;
    }
    if (i > 0) {
        mbname_truncate_boxes(mbname1, i);
        ancestor = xstrdup(mbname_intname(mbname1));
    }

done:
    mbname_free(&mbname1);
    mbname_free(&mbname2);
    return ancestor;
}
