/* mbdump.c -- Mailbox dump 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>

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <syslog.h>
#include <sys/types.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <ctype.h>
#include <dirent.h>
#include <utime.h>

#include "annotate.h"
#ifdef WITH_DAV
#include "dav_util.h"
#endif
#include "exitcodes.h"
#include "global.h"
#include "map.h"
#include "mappedfile.h"
#include "mbdump.h"
#include "mboxkey.h"
#include "mboxlist.h"
#include "quota.h"
#include "retry.h"
#include "seen.h"
#include "sequence.h"
#include "xmalloc.h"
#include "xstrlcpy.h"
#include "user.h"
#include "util.h"
#include "index.h"

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

static int dump_file(int first, int sync,
                     struct protstream *pin, struct protstream *pout,
                     const char *filename, const char *ftag,
                     const char *fbase, unsigned long flen);

static void downgrade_header(struct index_header *i, char *buf, int version,
                             int header_size, int record_size)
                             __attribute__((noinline));
static void header_set_num_records(char *buf, unsigned int nrecords)
                             __attribute__((noinline));

/* support for downgrading index files on copying back to an
 * earlier version of Cyrus */
static void downgrade_header(struct index_header *i, char *buf, int version,
                             int header_size, int record_size)
{
    unsigned UP_version = version;
    unsigned UP_validopts = 15;
    unsigned UP_num_records = i->exists;

    memset(buf, 0, INDEX_HEADER_SIZE);

    if (version < 10)
        UP_validopts = 3;
    /* was just called POP3_NEW_UIDL back then, single value */
    if (version < 8)
        UP_validopts = 1;

    /* has to match cyrus.cache still */
    *((bit32 *)(buf+OFFSET_GENERATION_NO)) = htonl(i->generation_no);
    *((bit32 *)(buf+OFFSET_FORMAT)) = htonl(i->format);
    /* version is changed of course */
    *((bit32 *)(buf+OFFSET_MINOR_VERSION)) = htonl(UP_version);
    *((bit32 *)(buf+OFFSET_START_OFFSET)) = htonl(header_size);
    *((bit32 *)(buf+OFFSET_RECORD_SIZE)) = htonl(record_size);
    /* NUM_RECORDS replaces "exists" in the older headers */
    *((bit32 *)(buf+OFFSET_NUM_RECORDS)) = htonl(UP_num_records);
    *((bit32 *)(buf+OFFSET_LAST_APPENDDATE)) = htonl(i->last_appenddate);
    *((bit32 *)(buf+OFFSET_LAST_UID)) = htonl(i->last_uid);

    /* quotas may be 64bit now */
    *((bit64 *)(buf+OFFSET_QUOTA_MAILBOX_USED)) = htonll(i->quota_mailbox_used);

    *((bit32 *)(buf+OFFSET_POP3_LAST_LOGIN)) = htonl(i->pop3_last_login);
    *((bit32 *)(buf+OFFSET_UIDVALIDITY)) = htonl(i->uidvalidity);
    *((bit32 *)(buf+OFFSET_DELETED)) = htonl(i->deleted);
    *((bit32 *)(buf+OFFSET_ANSWERED)) = htonl(i->answered);
    *((bit32 *)(buf+OFFSET_FLAGGED)) = htonl(i->flagged);
    *((bit32 *)(buf+OFFSET_MAILBOX_OPTIONS)) = htonl(i->options & UP_validopts);
    *((bit32 *)(buf+OFFSET_LEAKED_CACHE)) = htonl(i->leaked_cache_records);
    /* don't put values in the "SPARE" spots */
    if (version < 8) return;
    align_htonll(buf+OFFSET_HIGHESTMODSEQ, i->highestmodseq);
}

static void downgrade_record(const struct index_record *record, char *buf,
                             int version)
{
    int n;
    unsigned UP_content_offset = record->header_size;
    unsigned UP_validflags = 15;
    unsigned UP_modseqbase = OFFSET_MODSEQ;

    /* GUID was UUID and only 12 bytes long in versions 8 and 9.
     * it doesn't hurt to write it to the buffer in older versions,
     * because it won't be written out to file anyway due to the
     * record_size value. */
    if (version < 10)
        UP_modseqbase = OFFSET_MESSAGE_GUID+12;

    *((bit32 *)(buf+OFFSET_UID)) = htonl(record->uid);
    *((bit32 *)(buf+OFFSET_INTERNALDATE)) = htonl(record->internaldate);
    *((bit32 *)(buf+OFFSET_SENTDATE)) = htonl(record->sentdate);
    *((bit32 *)(buf+OFFSET_SIZE)) = htonl(record->size);
    *((bit32 *)(buf+OFFSET_HEADER_SIZE)) = htonl(record->header_size);
    /* content_offset in previous versions, identical to header_size */
    *((bit32 *)(buf+OFFSET_GMTIME)) = htonl(UP_content_offset);
    *((bit32 *)(buf+OFFSET_CACHE_OFFSET)) = htonl(record->cache_offset);
    *((bit32 *)(buf+OFFSET_LAST_UPDATED)) = htonl(record->last_updated);
    *((bit32 *)(buf+OFFSET_SYSTEM_FLAGS))
        = htonl(record->system_flags & UP_validflags);
    for (n = 0; n < MAX_USER_FLAGS/32; n++) {
        *((bit32 *)(buf+OFFSET_USER_FLAGS+4*n)) = htonl(record->user_flags[n]);
    }
    *((bit32 *)(buf+OFFSET_CONTENT_LINES)) = htonl(record->content_lines);
    *((bit32 *)(buf+OFFSET_CACHE_VERSION)) = htonl(record->cache_version);
    message_guid_export(&record->guid, buf+OFFSET_MESSAGE_GUID);
    *((bit64 *)(buf+UP_modseqbase)) = htonll(record->modseq);
}

static void header_set_num_records(char *buf, unsigned int nrecords)
{
    *((bit32 *)(buf+OFFSET_NUM_RECORDS)) = htonl(nrecords);
}

/* create a downgraded index file in cyrus.index.  We don't copy back
 * expunged messages, sorry */
static int dump_index(struct mailbox *mailbox, int oldversion,
                      struct seqset *expunged_seq, int first, int sync,
                      struct protstream *pin, struct protstream *pout)
{
    char oldname[MAX_MAILBOX_PATH];
    const char *fname;
    int oldindex_fd = -1;
    indexbuffer_t headerbuf;
    indexbuffer_t recordbuf;
    char *hbuf = (char *)headerbuf.buf;
    char *rbuf = (char *)recordbuf.buf;
    int header_size;
    int record_size;
    int n, r;

    if (oldversion == 6) {
        header_size = 76;
        record_size = 60;
    }
    else if (oldversion == 7) {
        header_size = 76;
        record_size = 72;
    }
    else if (oldversion == 8) {
        header_size = 92;
        record_size = 80;
    }
    else if (oldversion == 9) {
        header_size = 96;
        record_size = 80;
    }
    else if (oldversion == 10) {
        header_size = 96;
        record_size = 88;
    }
    else {
        return IMAP_MAILBOX_BADFORMAT;
    }

    fname = mailbox_meta_fname(mailbox, META_INDEX);
    snprintf(oldname, MAX_MAILBOX_PATH, "%s.OLD", fname);

    oldindex_fd = open(oldname, O_RDWR|O_TRUNC|O_CREAT, 0666);
    if (oldindex_fd == -1) goto fail;

    downgrade_header(&mailbox->i, hbuf, oldversion,
                     header_size, record_size);

    /* Write header - everything we'll say */
    n = retry_write(oldindex_fd, hbuf, header_size);
    if (n == -1) goto fail;

    struct mailbox_iter *iter = mailbox_iter_init(mailbox, 0, ITER_SKIP_UNLINKED);
    const message_t *msg;
    while ((msg = mailbox_iter_step(iter))) {
        const struct index_record *record = msg_record(msg);
        /* we have to make sure expunged records don't get the
         * file copied, or a reconstruct could bring them back
         * to life!  It we're not creating an expunged file... */
        if (record->system_flags & FLAG_EXPUNGED) {
            if (oldversion < 9) seqset_add(expunged_seq, record->uid, 1);
            continue;
        }
        /* not making sure exists matches, we do trust a bit */
        downgrade_record(record, rbuf, oldversion);
        n = retry_write(oldindex_fd, rbuf, record_size);
        if (n == -1) break;
    }
    mailbox_iter_done(&iter);
    if (n == -1) goto fail;

    close(oldindex_fd);
    r = dump_file(first, sync, pin, pout, oldname, "cyrus.index", NULL, 0);
    unlink(oldname);
    if (r) return r;

    /* create cyrus.expunge */
    if (oldversion > 8 && mailbox->i.num_records > mailbox->i.exists) {
        int nexpunge = mailbox->i.num_records - mailbox->i.exists;
        fname = mailbox_meta_fname(mailbox, META_EXPUNGE);
        snprintf(oldname, MAX_MAILBOX_PATH, "%s.OLD", fname);

        oldindex_fd = open(oldname, O_RDWR|O_TRUNC|O_CREAT, 0666);
        if (oldindex_fd == -1) goto fail;

        header_set_num_records(hbuf, nexpunge);

        /* Write header - everything we'll say */
        n = retry_write(oldindex_fd, hbuf, header_size);
        if (n == -1) goto fail;

        iter = mailbox_iter_init(mailbox, 0, ITER_SKIP_UNLINKED);
        while ((msg = mailbox_iter_step(iter))) {
            const struct index_record *record = msg_record(msg);
            /* ignore non-expunged records */
            if (!(record->system_flags & FLAG_EXPUNGED))
                continue;
            downgrade_record(record, rbuf, oldversion);
            n = retry_write(oldindex_fd, rbuf, record_size);
            if (n == -1) goto fail;
        }

        close(oldindex_fd);
        r = dump_file(first, sync, pin, pout, oldname, "cyrus.expunge", NULL, 0);
        unlink(oldname);
        if (r) return r;
    }

    return 0;

fail:
    if (oldindex_fd != -1)
        close(oldindex_fd);
    unlink(oldname);

    return IMAP_IOERROR;
}

/* is this the active script? */
static int sieve_isactive(const char *sievepath, const char *name)
{
    char filename[1024];
    char linkname[1024];
    char activelink[1024];
    char *file, *link;
    int len;

    snprintf(filename, sizeof(filename), "%s/%s", sievepath, name);
    snprintf(linkname, sizeof(linkname), "%s/defaultbc", sievepath);

    len = readlink(linkname, activelink, sizeof(activelink)-1);
    if(len < 0) {
        if(errno != ENOENT) syslog(LOG_ERR, "readlink(defaultbc): %m");
        return 0;
    }

    activelink[len] = '\0';

    /* Only compare the part of the file after the last /,
     * since that is what timsieved does */
    file = strrchr(filename, '/');
    link = strrchr(activelink, '/');
    if(!file) file = filename;
    else file++;
    if(!link) link = activelink;
    else link++;

    if (!strcmp(file, link)) {
        return 1;
    } else {
        return 0;
    }
}

struct dump_annotation_rock
{
    struct protstream *pout;
    const char *tag;
};

static int dump_annotations(const char *mailbox __attribute__((unused)),
                            uint32_t uid  __attribute__((unused)),
                            const char *entry,
                            const char *userid,
                            const struct buf *value, void *rock)
{
    struct dump_annotation_rock *ctx = (struct dump_annotation_rock *)rock;

    /* "A-" userid entry */
    /* entry is delimited by its leading / */
    char *ename;
    static const char contenttype[] = "text/plain"; /* fake */

    /* Transfer all attributes for this annotation, don't transfer size
     * separately since that can be implicitly determined */

    prot_putc(' ', ctx->pout);
    ename = strconcat("A-", userid, entry, (char *)NULL);
    prot_printliteral(ctx->pout, ename, strlen(ename));
    free(ename);

    prot_printf(ctx->pout, " (%ld ", 0L);  /* was modifiedsince */
    prot_printliteral(ctx->pout, value->s, value->len);
    prot_putc(' ', ctx->pout);
    prot_printliteral(ctx->pout, contenttype, strlen(contenttype));
    prot_putc(')', ctx->pout);

    return 0;
}

static int dump_file(int first, int sync,
                     struct protstream *pin, struct protstream *pout,
                     const char *filename, const char *ftag,
                     const char *fbase, unsigned long flen)
{
    int filefd;
    const char *base;
    size_t len;
    struct stat sbuf;
    int c;

    /* map file */
    syslog(LOG_DEBUG, "wanting to dump %s", filename);
    if (fbase) {
        /* already mapped */
        base = fbase;

        /* we need to check real file size since actual mmap size may be larger */
        if (stat(filename, &sbuf) == -1) {
            syslog(LOG_ERR, "IOERROR: stat on %s: %m", filename);
            fatal("can't stat message file", EC_OSFILE);
        }
        if ((unsigned long)sbuf.st_size > flen) {
           syslog(LOG_ERR, "IOERROR: size mismatch on %s", filename);
           return IMAP_SYS_ERROR;
        }
        len = sbuf.st_size;
    }
    else {
        filefd = open(filename, O_RDONLY, 0666);
        if (filefd == -1) {
            /* If an optional file doesn't exist, skip it */
            if (errno == ENOENT) return 0;
            syslog(LOG_ERR, "IOERROR: open on %s: %m", filename);
            return IMAP_SYS_ERROR;
        }

        if (fstat(filefd, &sbuf) == -1) {
            syslog(LOG_ERR, "IOERROR: fstat on %s: %m", filename);
            fatal("can't fstat message file", EC_OSFILE);
        }

        base = NULL;
        len = 0;

        map_refresh(filefd, 1, &base, &len, sbuf.st_size, filename, NULL);

        close(filefd);
    }

    /* send: name, size, and contents */
    if (first) {
        prot_printf(pout, " {" SIZE_T_FMT "}\r\n", strlen(ftag));

        if (sync) {
            /* synchronize */
            c = prot_getc(pin);
            eatline(pin, c); /* We eat it no matter what */
            if (c != '+') {
                /* Synchronization Failure, Abort! */
                syslog(LOG_ERR, "Sync Error: expected '+' got '%c'",c);
                return IMAP_SERVER_UNAVAILABLE;
            }
        }

        prot_printf(pout, "%s {%lu%s}\r\n",
                    ftag, (long unsigned)len, (sync ? "+" : ""));
    } else {
        prot_printf(pout, " {" SIZE_T_FMT "%s}\r\n%s {%lu%s}\r\n",
                    strlen(ftag), (sync ? "+" : ""),
                    ftag, (long unsigned)len, (sync ? "+" : ""));
    }
    prot_write(pout, base, len);
    if (!fbase) map_free(&base, &len);

    return 0;
}

struct data_file {
    int metaname;
    char *fname;
};

/* even though 2.4.x doesn't use cyrus.expunge, we need to be aware
 * that it may exist so XFER from any 2.3.x server will work
 */
static struct data_file data_files[] = {
    { META_HEADER,  "cyrus.header"  },
    { META_INDEX,   "cyrus.index"   },
    { META_CACHE,   "cyrus.cache"   },
    { META_EXPUNGE, "cyrus.expunge" },
#ifdef WITH_DAV
    { META_DAV,     "cyrus.dav"     },
#endif
    { 0, NULL }
};

enum { SEEN_DB = 0, SUBS_DB = 1, MBOXKEY_DB = 2, DAV_DB = 3 };
static int NUM_USER_DATA_FILES = 4;

EXPORTED int dump_mailbox(const char *tag, struct mailbox *mailbox, uint32_t uid_start,
                 int oldversion,
                 struct protstream *pin, struct protstream *pout,
                 struct auth_state *auth_state __attribute((unused)))
{
    DIR *mbdir = NULL;
    int r = 0;
    struct dirent *next = NULL;
    char filename[MAX_MAILBOX_PATH + 1024];
    const char *fname;
    int first = 1;
    int i;
    char *userid = NULL;
    struct quota q;
    struct data_file *df;
    struct seqset *expunged_seq = NULL;
    const char *dirpath = mailbox_datapath(mailbox, 0);

    /* XXX - archivepath */

    mbdir = opendir(dirpath);
    if (!mbdir && errno == EACCES) {
        syslog(LOG_ERR,
               "could not dump mailbox %s (permission denied)", mailbox->name);
        return IMAP_PERMISSION_DENIED;
    } else if (!mbdir) {
        syslog(LOG_ERR,
               "could not dump mailbox %s (unknown error)", mailbox->name);
        return IMAP_SYS_ERROR;
    }

    /* after this point we have to both close the directory and unlock
     * the mailbox */

    if (tag) prot_printf(pout, "%s DUMP ", tag);
    (void)prot_putc('(', pout);

    /* The first member is either a number (if it is a quota root), or NIL
     * (if it isn't) */
    {
        quota_init(&q, mailbox->name);
        r = quota_read(&q, NULL, 0);

        if (!r) {
            prot_printf(pout, QUOTA_T_FMT, q.limits[QUOTA_STORAGE]);
        } else {
            prot_printf(pout, "NIL");
            if (r == IMAP_QUOTAROOT_NONEXISTENT) r = 0;
            /* do not send other quota data later */
            quota_free(&q);
        }
    }

    /* Dump cyrus data files */
    for (df = data_files; df->metaname; df++) {
        const char *fbase = NULL;
        unsigned long flen = 0;

        switch (df->metaname) {
        case META_INDEX:
            if (mailbox->index_base) {
                fbase = mailbox->index_base;
                flen = mailbox->index_len;
            }
            break;

        case META_CACHE:
            {
                /* XXX - multi-cache-file support */
                struct mappedfile *cachefile = ptrarray_nth(&mailbox->caches, 0);
                if (cachefile) {
                    fbase = mappedfile_base(cachefile);
                    flen = mappedfile_size(cachefile);
                }
            }
            break;

        default:
            break;
        }

        if (df->metaname == META_INDEX && oldversion < MAILBOX_MINOR_VERSION) {
            expunged_seq = seqset_init(mailbox->i.last_uid, SEQ_SPARSE);
            syslog(LOG_NOTICE, "%s downgrading index to version %d for XFER",
                   mailbox->name, oldversion);

            r = dump_index(mailbox, oldversion, expunged_seq,
                           first, !tag, pin, pout);
            if (r) goto done;

        } else {
            fname = mailbox_meta_fname(mailbox, df->metaname);
            r = dump_file(first, !tag, pin, pout, fname, df->fname, fbase, flen);
            if (r) goto done;
        }

        first = 0;
    }

    /* Dump message files */
    while ((next = readdir(mbdir)) != NULL) {
        char *name = next->d_name;  /* Alias */
        char *p = name;
        char *fullpath;
        uint32_t uid;

        /* special case for '.' (well, it gets '..' too) */
        if (name[0] == '.') continue;

        /* skip non-message files */
        while (*p && Uisdigit(*p)) p++;
        if (p[0] != '.' || p[1] != '\0') continue;

        /* ensure (number) is >= our target uid */
        uid = atoi(name);
        if (uid < uid_start) continue;

        /* ensure uid is not in the expunged records that got skipped
         * in a downgraded index file */
        if (seqset_ismember(expunged_seq, uid))
           continue;

        /* construct path/filename */
        fullpath = strconcat(dirpath, "/", name, (char *)NULL);
        r = dump_file(0, !tag, pin, pout, fullpath, name, NULL, 0);
        free(fullpath);

        if (r) goto done;
    }

    closedir(mbdir);
    mbdir = NULL;

    /* Dump annotations */
    {
        struct dump_annotation_rock actx;
        actx.tag = tag;
        actx.pout = pout;
        annotatemore_findall(mailbox->name, 0, "*", dump_annotations,
                             (void *) &actx);
    }

    /* Dump user files if this is an inbox */
    if (mboxname_isusermailbox(mailbox->name, 1)) {
        userid = mboxname_to_userid(mailbox->name);
        int sieve_usehomedir = config_getswitch(IMAPOPT_SIEVEUSEHOMEDIR);
        char *fname = NULL, *ftag = NULL;

        /* Dump seen and subs files */
        for (i = 0; i< NUM_USER_DATA_FILES; i++) {

            /* construct path/filename */
            switch (i) {
            case SEEN_DB:
                fname = seen_getpath(userid);
                ftag = "SEEN";
                break;
            case SUBS_DB:
                fname = user_hash_subs(userid);
                ftag = "SUBS";
                break;
            case MBOXKEY_DB:
                fname = mboxkey_getpath(userid);
                ftag = "MBOXKEY";
                break;
            case DAV_DB: {
#ifdef WITH_DAV
                struct buf dav_file = BUF_INITIALIZER;

                dav_getpath_byuserid(&dav_file, userid);
                fname = (char *) buf_cstring(&dav_file);
                ftag = "DAV";
#else
                continue;
#endif
                break;
            }
            default:
                fatal("unknown user data file", EC_OSFILE);
            }

            r = dump_file(0, !tag, pin, pout, fname, ftag, NULL, 0);
            free(fname);
            if (r) goto done;
        }

        /* Dump sieve files
         *
         * xxx can't use home directories currently
         * (it makes almost no sense in the conext of a murder) */
        if (!sieve_usehomedir) {
            const char *sieve_path = user_sieve_path(userid);
            mbdir = opendir(sieve_path);

            if (!mbdir) {
                if (errno != ENOENT)
                    syslog(LOG_ERR,
                           "could not dump sieve scripts in %s: %m)", sieve_path);
            } else {
                char tag_fname[2048];

                while((next = readdir(mbdir)) != NULL) {
                    int length=strlen(next->d_name);
                    /* 7 == strlen(".script"); 3 == strlen(".bc") */
                    if ((length >= 7 &&
                         !strcmp(next->d_name + (length - 7), ".script")) ||
                        (length >= 3 &&
                         !strcmp(next->d_name + (length - 3), ".bc")))
                    {
                        /* create tagged name */
                        if(sieve_isactive(sieve_path, next->d_name)) {
                            snprintf(tag_fname, sizeof(tag_fname),
                                     "SIEVED-%s", next->d_name);
                        } else {
                            snprintf(tag_fname, sizeof(tag_fname),
                                     "SIEVE-%s", next->d_name);
                        }

                        /* construct path/filename */
                        snprintf(filename, sizeof(filename), "%s/%s",
                                 sieve_path, next->d_name);

                        /* dump file */
                        r = dump_file(0, !tag, pin, pout, filename, tag_fname, NULL, 0);
                        if (r) goto done;
                    }
                }

                closedir(mbdir);
                mbdir = NULL;
            }
        } /* end if !sieve_userhomedir */

    } /* end if user INBOX */

    /* Dump quota data */
    if (q.root) {
        const char *ename = "X-QUOTA";
        int res;

        /* ensure there is data to transmit */
        for (res = 0; res < QUOTA_NUMRESOURCES; res++) {
            /* STORAGE quota was already sent */
            if ((res != QUOTA_STORAGE) && (q.limits[res] >= 0)) {
                break;
            }
        }

        if (res < QUOTA_NUMRESOURCES) {
            int sent = 0;

            prot_putc(' ', pout);
            prot_printliteral(pout, ename, strlen(ename));
            prot_printf(pout, " (");

            for (res = 0; res < QUOTA_NUMRESOURCES; res++) {
                if (q.limits[res] < 0) {
                    continue;
                }
                if (sent++) {
                    prot_putc(' ', pout);
                }
                prot_printliteral(pout, quota_names[res], strlen(quota_names[res]));
                prot_putc(' ', pout);
                prot_printf(pout, QUOTA_T_FMT, q.limits[res]);
            }
            prot_putc(')', pout);
        }
        quota_free(&q);
    }

 done:

    prot_printf(pout,")\r\n");

    prot_flush(pout);

    if (mbdir) closedir(mbdir);
    seqset_free(expunged_seq);
    free(userid);

    return r;
}

static int cleanup_seen_cb(const mbentry_t *mbentry, void *rock)
{
    struct seen *seendb = (struct seen *)rock;
    int r;
    struct seqset *seq = NULL;
    struct mailbox *mailbox = NULL;
    const message_t *msg;
    struct seendata sd = SEENDATA_INITIALIZER;

    r = mailbox_open_iwl(mbentry->name, &mailbox);
    if (r) goto done;

    /* read in the seen data from the seendb */
    r = seen_read(seendb, mailbox->uniqueid, &sd);
    if (r) goto done;

    seq = seqset_parse(sd.seenuids, NULL, sd.lastuid);
    seen_freedata(&sd);

    /* update all the seen records */
    struct mailbox_iter *iter = mailbox_iter_init(mailbox, 0, ITER_SKIP_EXPUNGED);
    while ((msg = mailbox_iter_step(iter))) {
        const struct index_record *record = msg_record(msg);
        if (record->system_flags & FLAG_SEEN)
            continue; /* no need to rewrite */
        if (!seqset_ismember(seq, record->uid))
            continue;
        struct index_record copy = *record;
        copy.system_flags |= FLAG_SEEN;
        r = mailbox_rewrite_index_record(mailbox, &copy);
        if (r) break;
    }
    mailbox_iter_done(&iter);

    /* and the header values */
    mailbox_index_dirty(mailbox);
    if (mailbox->i.recentuid < sd.lastuid)
        mailbox->i.recentuid = sd.lastuid;
    if (mailbox->i.recenttime < sd.lastread)
        mailbox->i.recenttime = sd.lastread;

 done:
    seqset_free(seq);
    mailbox_close(&mailbox);
    return r;
}

static int cleanup_seen_subfolders(const char *mbname)
{
    char *userid = mboxname_to_userid(mbname);
    struct seen *seendb = NULL;
    char buf[MAX_MAILBOX_NAME];
    int r;

    /* not owned by a user */
    if (!userid) return 0;

    /* no need to do inbox, it will have upgraded OK, just
     * the subfolders */

    snprintf(buf, sizeof(buf), "%s.", mbname);

    r = seen_open(userid, SEEN_SILENT, &seendb);
    if (!r) mboxlist_allmbox(buf, cleanup_seen_cb, seendb, /*incdel*/0);
    seen_close(&seendb);

    free(userid);

    return 0;
}

EXPORTED int undump_mailbox(const char *mbname,
                   struct protstream *pin, struct protstream *pout,
                   struct auth_state *auth_state __attribute((unused)))
{
    struct buf file, data;
    int c;
    int r = 0;
    int curfile = -1;
    struct mailbox *mailbox = NULL;
    const char *sieve_path = NULL;
    int sieve_usehomedir = config_getswitch(IMAPOPT_SIEVEUSEHOMEDIR);
    char *userid = NULL;
    int first_annotation = 1;
    char *annotation = NULL;
    struct buf content = BUF_INITIALIZER;
    char *seen_file = NULL;
    char *mboxkey_file = NULL;
    quota_t old_quota_usage[QUOTA_NUMRESOURCES];
    int res;
    quota_t newquotas[QUOTA_NUMRESOURCES];
    quota_t quotalimit = -1;
    annotate_state_t *astate = NULL;

    memset(&file, 0, sizeof(file));
    memset(&data, 0, sizeof(data));

    /* Set a Quota (may be -1 for "unlimited") */
    for (res = 0; res < QUOTA_NUMRESOURCES; res++) {
       newquotas[res] = QUOTA_UNLIMITED;
    }

    if (mboxname_isusermailbox(mbname, 1)) {
        userid = mboxname_to_userid(mbname);
        if(!sieve_usehomedir) {
            sieve_path = user_sieve_path(userid);
        }
    }

    c = getword(pin, &data);

    /* we better be in a list now */
    if (c != '(' || data.s[0]) {
        buf_free(&data);
        free(userid);
        eatline(pin, c);
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    }

    /* We should now have a number or a NIL */
    c = getword(pin, &data);
    if (!strcmp(data.s, "NIL")) {
        /* Remove any existing quotaroot */
        mboxlist_unsetquota(mbname);
    } else if (sscanf(data.s, QUOTA_T_FMT, &quotalimit) == 1) {
        /* quota will actually be applied later */
        newquotas[QUOTA_STORAGE] = quotalimit;
    } else {
        /* Huh? */
        buf_free(&data);
        free(userid);
        eatline(pin, c);
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    }

    if(c != ' ' && c != ')') {
        buf_free(&data);
        free(userid);
        eatline(pin, c);
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    } else if(c == ')') {
        goto done;
    }

    r = mailbox_open_exclusive(mbname, &mailbox);
    if (r == IMAP_MAILBOX_NONEXISTENT) {
        mbentry_t *mbentry = NULL;
        r = mboxlist_lookup(mbname, &mbentry, NULL);
        if (!r) r = mailbox_create(mbname, mbentry->mbtype,
                                   mbentry->partition, mbentry->acl,
                                   mbentry->uniqueid, 0, 0, 0, &mailbox);
        mboxlist_entry_free(&mbentry);
    }
    if(r) goto done;

    /* track quota use */
    mailbox_get_usage(mailbox, old_quota_usage);

    astate = annotate_state_new();

    while(1) {
        char fnamebuf[MAX_MAILBOX_PATH + 1024];
        int isnowait, sawdigit;
        unsigned long size;
        unsigned long cutoff = ULONG_MAX / 10;
        unsigned digit, cutlim = ULONG_MAX % 10;
        annotation = NULL;
        buf_reset(&content);
        seen_file = NULL;
        mboxkey_file = NULL;

        c = getastring(pin, pout, &file);
        if(c != ' ') {
            r = IMAP_PROTOCOL_ERROR;
            goto done;
        }

        if(!strncmp(file.s, "A-", 2)) {
            /* Annotation */
            int i;
            char *tmpuserid;

            /* is this the first annotation? re-read cyrus.header/index */
            if (first_annotation) {
                first_annotation = 0;
                mailbox_close(&mailbox);
                r = mailbox_open_exclusive(mbname, &mailbox);
                if (r) goto done;
                r = annotate_state_set_mailbox(astate, mailbox);
                if (r) goto done;
            }

            for(i=2; file.s[i]; i++) {
                if(file.s[i] == '/') break;
            }
            if(!file.s[i]) {
                r = IMAP_PROTOCOL_ERROR;
                goto done;
            }
            tmpuserid = xmalloc(i-2+1);

            memcpy(tmpuserid, &(file.s[2]), i-2);
            tmpuserid[i-2] = '\0';

            annotation = xstrdup(&(file.s[i]));

            if(prot_getc(pin) != '(') {
                r = IMAP_PROTOCOL_ERROR;
                free(tmpuserid);
                goto done;
            }

            /* Parse the modtime...and ignore it */
            c = getword(pin, &data);
            if (c != ' ')  {
                r = IMAP_PROTOCOL_ERROR;
                free(tmpuserid);
                goto done;
            }

            c = getbastring(pin, pout, &content);
            /* xxx binary */

            if(c != ' ') {
                r = IMAP_PROTOCOL_ERROR;
                free(tmpuserid);
                goto done;
            }

            /* got the contenttype...and ignore it */
            c = getastring(pin, pout, &data);

            if(c != ')') {
                r = IMAP_PROTOCOL_ERROR;
                free(tmpuserid);
                goto done;
            }

            annotate_state_write(astate, annotation, tmpuserid,
                                     &content);

            free(tmpuserid);
            free(annotation);
            annotation = NULL;
            buf_reset(&content);

            c = prot_getc(pin);
            if(c == ')') break; /* that was the last item */
            else if(c != ' ') {
                r = IMAP_PROTOCOL_ERROR;
                goto done;
            }

            continue;
        }
        else if (!strcmp(file.s, "X-QUOTA")) {
            /* Quota */
            if (prot_getc(pin) != '(') {
                r = IMAP_PROTOCOL_ERROR;
                goto done;
            }

            for (;;) {
                /* XXX - limit is actually stored in an int value */
                int32_t limit = 0;

                c = getastring(pin, pout, &content);
                if (c != ' ') {
                    r = IMAP_PROTOCOL_ERROR;
                    goto done;
                }
                /* ignore unknown resources */
                res = quota_name_to_resource(content.s);

                c = getint32(pin, &limit);
                if (res >= 0) {
                    newquotas[res] = limit;
                }

                if (c == ')') break;
                else if (c != ' ') {
                    r = IMAP_PROTOCOL_ERROR;
                    goto done;
                }
            }

            c = prot_getc(pin);
            if (c == ')') break; /* that was the last item */
            else if (c != ' ') {
                r = IMAP_PROTOCOL_ERROR;
                goto done;
            }

            continue;
        }

        /* read size of literal */
        c = prot_getc(pin);
        if (c != '{') {
            r = IMAP_PROTOCOL_ERROR;
            goto done;
        }

        size = isnowait = sawdigit = 0;
        while ((c = prot_getc(pin)) != EOF && isdigit(c)) {
            sawdigit = 1;
            digit = c - '0';
            /* check for overflow */
            if (size > cutoff || (size == cutoff && digit > cutlim)) {
                fatal("literal too big", EC_IOERR);
            }
            size = size*10 + digit;
        }
        if (c == '+') {
            isnowait++;
            c = prot_getc(pin);
        }
        if (c == '}') {
            c = prot_getc(pin);
            if (c == '\r') c = prot_getc(pin);
        }
        if (!sawdigit || c != '\n') {
            r = IMAP_PROTOCOL_ERROR;
            goto done;
        }

        if (!isnowait) {
            /* Tell client to send the message */
            prot_printf(pout, "+ go ahead\r\n");
            prot_flush(pout);
        }

        if (userid && !strcmp(file.s, "SUBS")) {
            /* overwriting this outright is absolutely what we want to do */
            char *s = user_hash_subs(userid);
            strlcpy(fnamebuf, s, sizeof(fnamebuf));
            free(s);
#ifdef WITH_DAV
        } else if (userid && !strcmp(file.s, "DAV")) {
            /* overwriting this outright is absolutely what we want to do */
            struct buf dav_file = BUF_INITIALIZER;
            dav_getpath_byuserid(&dav_file, userid);
            strlcpy(fnamebuf, buf_cstring(&dav_file), sizeof(fnamebuf));
            buf_free(&dav_file);
#endif
        } else if (userid && !strcmp(file.s, "SEEN")) {
            seen_file = seen_getpath(userid);

            snprintf(fnamebuf,sizeof(fnamebuf),"%s.%d",seen_file,getpid());
        } else if (userid && !strcmp(file.s, "MBOXKEY")) {
            mboxkey_file = mboxkey_getpath(userid);

            snprintf(fnamebuf,sizeof(fnamebuf),"%s.%d",mboxkey_file,getpid());
        } else if (userid && !strncmp(file.s, "SIEVE", 5)) {
            int isdefault = !strncmp(file.s, "SIEVED", 6);
            char *realname;
            int ret;

            /* skip prefixes */
            if(isdefault) realname = file.s + 7;
            else realname = file.s + 6;

            if(sieve_usehomedir) {
                /* xxx! */
                syslog(LOG_ERR,
                       "dropping sieve file %s since this host is " \
                       "configured for sieve_usehomedir",
                       realname);
                continue;
            } else {
                if(snprintf(fnamebuf, sizeof(fnamebuf),
                            "%s/%s", sieve_path, realname) == -1) {
                    r = IMAP_PROTOCOL_ERROR;
                    goto done;
                } else if(isdefault) {
                    char linkbuf[2048];

                    snprintf(linkbuf, sizeof(linkbuf), "%s/defaultbc",
                             sieve_path);
                    ret = symlink(realname, linkbuf);
                    if(ret && errno == ENOENT) {
                        if(cyrus_mkdir(linkbuf, 0750) == 0) {
                            ret = symlink(realname, linkbuf);
                        }
                    }
                    if(ret) {
                        syslog(LOG_ERR, "symlink(%s, %s): %m", realname,
                               linkbuf);
                        /* Non-fatal,
                           let's get the file transferred if we can */
                    }

                }
            }
        } else {
            struct data_file *df;
            const char *path = NULL;

            /* see if its one of our datafiles */
            for (df = data_files; df->fname && strcmp(df->fname, file.s); df++);
            if (df->metaname) {
                path = mailbox_meta_fname(mailbox, df->metaname);
            } else {
                uint32_t uid;
                const char *ptr = NULL;
                if (!parseuint32(file.s, &ptr, &uid)) {
                    /* is it really a data file? */
                    if (ptr && ptr[0] == '.' && ptr[1] == '\0')
                        path = mboxname_datapath(mailbox->part, mailbox->name, mailbox->uniqueid, uid);
                }
            }
            if (!path) {
                r = IMAP_PROTOCOL_ERROR;
                goto done;
            }
            strncpy(fnamebuf, path, MAX_MAILBOX_PATH);
        }

        /* if we haven't opened it, do so */
        curfile = open(fnamebuf, O_WRONLY|O_TRUNC|O_CREAT, 0640);
        if(curfile == -1 && errno == ENOENT) {
            if(cyrus_mkdir(fnamebuf, 0750) == 0) {
                curfile = open(fnamebuf, O_WRONLY|O_TRUNC|O_CREAT, 0640);
            }
        }

        if(curfile == -1) {
            syslog(LOG_ERR, "IOERROR: creating %s: %m", fnamebuf);
            r = IMAP_IOERROR;
            goto done;
        }

        /* write data to file */
        while (size) {
            char buf[4096+1];
            int n = prot_read(pin, buf, size > 4096 ? 4096 : size);
            if (!n) {
                syslog(LOG_ERR,
                       "IOERROR: reading message: unexpected end of file");
                r = IMAP_IOERROR;
                goto done;
            }

            size -= n;

            if (write(curfile, buf, n) != n) {
                syslog(LOG_ERR, "IOERROR: writing %s: %m", fnamebuf);
                r = IMAP_IOERROR;
                goto done;
            }
        }

        close(curfile);

        /* we were operating on the seen state, so merge it and cleanup */
        if (seen_file) {
            struct seen *seendb = NULL;
            r = seen_open(userid, SEEN_CREATE, &seendb);
            if (!r) r = seen_merge(seendb, fnamebuf);
            seen_close(&seendb);

            free(seen_file);
            seen_file = NULL;
            unlink(fnamebuf);

            if (r) goto done;
        }
        /* we were operating on the seen state, so merge it and cleanup */
        else if (mboxkey_file) {
            mboxkey_merge(fnamebuf, mboxkey_file);
            free(mboxkey_file);
            mboxkey_file = NULL;

            unlink(fnamebuf);
        }

        c = prot_getc(pin);
        if (c == ')') break;
        if (c != ' ') {
            r = IMAP_PROTOCOL_ERROR;
            goto done;
        }
    }

    /* patch up seen data for subfolders */
    if (userid) {
        r = cleanup_seen_subfolders(mbname);
        /* XXX - patch up seen data here */
    }

 done:
    /* eat the rest of the line, we have atleast a \r\n coming */
    eatline(pin, c);
    buf_free(&file);
    buf_free(&data);

    if (r)
        annotate_state_abort(&mailbox->annot_state);

    if (curfile >= 0) close(curfile);
    /* we fiddled the files under the hood, so we can't do anything
     * BUT close it */
    mailbox_close(&mailbox);

    /* time to apply quota (mailbox must be unlocked) */
    if (!r) {
        for (res = 0; res < QUOTA_NUMRESOURCES; res++) {
            if (newquotas[res] != QUOTA_UNLIMITED) {
                break;
            }
        }
        if (res < QUOTA_NUMRESOURCES) {
            mboxlist_setquotas(mbname, newquotas, 0);
        }
    }

    /* let's make sure the modification times are right */
    if (!r) {
        struct utimbuf settime;
        const char *fname;

        /* cheeky - we're not actually changing anything real */
        r = mailbox_open_irl(mbname, &mailbox);
        if (r) {
            r = 0; /* no point throwing errors */
            goto done2;
        }

        /* update the quota if necessary */
        if (mailbox->quotaroot) {
            quota_t quota_usage[QUOTA_NUMRESOURCES];
            int res;
            int changed = 0;

            mailbox_get_usage(mailbox, quota_usage);
            for (res = 0; res < QUOTA_NUMRESOURCES; res++) {
                quota_usage[res] -= old_quota_usage[res];
                if (quota_usage[res] != 0) {
                    changed++;
                }
            }

            if (changed) {
                r = quota_update_useds(mailbox->quotaroot, quota_usage, 0);
                if (r) goto done2;
            }
        }

        struct mailbox_iter *iter = mailbox_iter_init(mailbox, 0, ITER_SKIP_UNLINKED);
        const message_t *msg;
        while ((msg = mailbox_iter_step(iter))) {
            const struct index_record *record = msg_record(msg);
            fname = mailbox_record_fname(mailbox, record);
            settime.actime = settime.modtime = record->internaldate;
            if (utime(fname, &settime) == -1) {
                r = IMAP_IOERROR;
                goto done2;
            }
        }
        mailbox_iter_done(&iter);
    }

 done2:
    /* just in case we failed during the modifications, close again */
    mailbox_close(&mailbox);
    if (!r)
        r = annotate_state_commit(&astate);
    else
        annotate_state_abort(&astate);
    free(annotation);
    buf_free(&content);
    free(seen_file);
    free(mboxkey_file);
    free(userid);

    return r;
}
