/* lcb_indexw.c -- replication-based backup api - index writing functions
 *
 * Copyright (c) 1994-2015 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 <assert.h>
#include <syslog.h>

#include "lib/xmalloc.h"

#include "imap/dlist.h"
#include "imap/imap_err.h"

#include "backup/backup.h"

#define LIBCYRUS_BACKUP_SOURCE /* this file is part of libcyrus_backup */
#include "backup/lcb_internal.h"
#include "backup/lcb_sqlconsts.h"

static int _index_expunge(struct backup *backup, struct dlist *dl,
                          time_t ts, off_t dl_offset);
static int _index_mailbox(struct backup *backup, struct dlist *dl,
                          time_t ts, off_t dl_offset);
static int _index_unmailbox(struct backup *backup, struct dlist *dl,
                            time_t ts, off_t dl_offset);
static int _index_message(struct backup *backup, struct dlist *dl,
                          time_t ts, off_t dl_offset, size_t dl_len);
static int _index_rename(struct backup *backup, struct dlist *dl,
                         time_t ts, off_t dl_offset);
static int _index_seen(struct backup *backup, struct dlist *dl,
                       time_t ts, off_t dl_offset);
static int _index_sub(struct backup *backup, struct dlist *dl,
                      time_t ts, off_t dl_offset);
static int _index_sieve(struct backup *backup, struct dlist *dl,
                        time_t ts, off_t dl_offset);

HIDDEN int backup_index(struct backup *backup, struct dlist *dlist,
                        time_t ts, off_t start, size_t len)
{
    int r = IMAP_PROTOCOL_ERROR;

    if (strcmp(dlist->name, "EXPUNGE") == 0)
        r = _index_expunge(backup, dlist, ts, start);
    else if (strcmp(dlist->name, "MAILBOX") == 0)
        r = _index_mailbox(backup, dlist, ts, start);
    else if (strcmp(dlist->name, "UNMAILBOX") == 0)
        r = _index_unmailbox(backup, dlist, ts, start);
    else if (strcmp(dlist->name, "MESSAGE") == 0)
        r = _index_message(backup, dlist, ts, start, len);
    else if (strcmp(dlist->name, "RENAME") == 0)
        r = _index_rename(backup, dlist, ts, start);
    else if (strcmp(dlist->name, "RESERVE") == 0)
        r = 0; /* nothing to index for a reserve, just return success */
    else if (strcmp(dlist->name, "SEEN") == 0)
        r = _index_seen(backup, dlist, ts, start);
    else if (strcmp(dlist->name, "SUB") == 0)
        r = _index_sub(backup, dlist, ts, start);
    else if (strcmp(dlist->name, "UNSUB") == 0)
        r = _index_sub(backup, dlist, ts, start);
    else if (strcmp(dlist->name, "SIEVE") == 0)
        r = _index_sieve(backup, dlist, ts, start);
    else if (strcmp(dlist->name, "UNSIEVE") == 0)
        r = _index_sieve(backup, dlist, ts, start);
    else if (config_debug) {
        struct buf tmp = BUF_INITIALIZER;
        dlist_printbuf(dlist, 1, &tmp);
        syslog(LOG_DEBUG, "ignoring unrecognised dlist: %s\n", buf_cstring(&tmp));
        buf_free(&tmp);
    }

    return r;
}

static int _index_expunge(struct backup *backup, struct dlist *dl,
                          time_t ts, off_t dl_offset)
{
    syslog(LOG_DEBUG, "indexing EXPUNGE at " OFF_T_FMT "...\n", dl_offset);

    const char *mboxname;
    const char *uniqueid;
    struct dlist *uidl;
    struct dlist *di;
    struct backup_mailbox *mailbox = NULL;
    int r = 0;

    if (!dlist_getatom(dl, "MBOXNAME", &mboxname))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getatom(dl, "UNIQUEID", &uniqueid))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getlist(dl, "UID", &uidl))
        return IMAP_PROTOCOL_BAD_PARAMETERS;

    mbname_t *mbname = mbname_from_intname(mboxname);
    mailbox = backup_get_mailbox_by_name(backup, mbname, 0);
    mbname_free(&mbname);

    if (!mailbox)
        return IMAP_MAILBOX_NONEXISTENT;

    /* verify that uniqueid matches */
    if (strcmp(mailbox->uniqueid, uniqueid) != 0) {
        syslog(LOG_ERR, "%s: uniqueid mismatch for %s: %s on wire, %s in index",
                        __func__, mboxname, uniqueid, mailbox->uniqueid);
        r = IMAP_PROTOCOL_BAD_PARAMETERS;
    }

    for (di = uidl->head; di && !r; di = di->next) {
        struct sqldb_bindval bval[] = {
            { ":mailbox_id",    SQLITE_INTEGER, { .i = mailbox->id } },
            { ":uid",           SQLITE_INTEGER, { .i = dlist_num(di) } },
            { ":expunged",      SQLITE_INTEGER, { .i = ts } },
        };

        r = sqldb_exec(backup->db, backup_index_mailbox_message_expunge_sql,
                       bval, NULL, NULL);
        if (r) r = IMAP_INTERNAL;
    }

    backup_mailbox_free(&mailbox);
    return r;
}

static int _get_magic_flags(struct dlist *flags, int *is_expunged)
{
    struct dlist *found_expunged = NULL;
    struct dlist *di;
    int found = 0;

    assert(strcmp(flags->name, "FLAGS") == 0);

    for (di = flags->head; di; di = di->next) {
        if (strcmp(di->sval, "\\Expunged") == 0) {
            if (is_expunged) *is_expunged = 1;
            found_expunged = di;
            found++;
        }
    }

    if (found_expunged) {
        dlist_unstitch(flags, found_expunged);
        dlist_free(&found_expunged);
    }

    return found;
}

static int _index_mailbox(struct backup *backup, struct dlist *dl,
                          time_t ts, off_t dl_offset)
{
    syslog(LOG_DEBUG, "indexing MAILBOX at " OFF_T_FMT "...\n", dl_offset);

    const char *uniqueid = NULL;
    const char *mboxname = NULL;
    const char *mboxtype = NULL;
    uint32_t last_uid = 0;
    modseq_t highestmodseq = 0;
    uint32_t recentuid = 0;
    time_t recenttime = 0;
    time_t last_appenddate = 0;
    time_t pop3_last_login = 0;
    time_t pop3_show_after = 0;
    uint32_t uidvalidity = 0;
    const char *partition = NULL;
    const char *acl = NULL;
    const char *options = NULL;
    struct synccrcs synccrcs = { 0, 0 };
    const char *quotaroot = NULL;
    modseq_t xconvmodseq = 0;
    struct dlist *annotations = NULL;
    struct buf annotations_buf = BUF_INITIALIZER;
    struct dlist *record = NULL;
    int r;

    if (!dlist_getatom(dl, "UNIQUEID", &uniqueid))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getatom(dl, "MBOXNAME", &mboxname))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getnum32(dl, "LAST_UID", &last_uid))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getnum64(dl, "HIGHESTMODSEQ", &highestmodseq))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getnum32(dl, "RECENTUID", &recentuid))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getdate(dl, "RECENTTIME", &recenttime))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getdate(dl, "LAST_APPENDDATE", &last_appenddate))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getdate(dl, "POP3_LAST_LOGIN", &pop3_last_login))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getnum32(dl, "UIDVALIDITY", &uidvalidity))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getatom(dl, "PARTITION", &partition))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getatom(dl, "ACL", &acl))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getatom(dl, "OPTIONS", &options))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getlist(dl, "RECORD", &record))
        return IMAP_PROTOCOL_BAD_PARAMETERS;

    /* optional */
    dlist_getlist(dl, "ANNOTATIONS", &annotations);
    dlist_getdate(dl, "POP3_SHOW_AFTER", &pop3_show_after);
    dlist_getatom(dl, "MBOXTYPE", &mboxtype);
    dlist_getnum64(dl, "XCONVMODSEQ", &xconvmodseq);

    /* CRCs */
    dlist_getnum32(dl, "SYNC_CRC", &synccrcs.basic);
    dlist_getnum32(dl, "SYNC_CRC_ANNOT", &synccrcs.annot);

    /* if we can't start a transaction, bail out before allocating anything else */
    r = sqldb_begin(backup->db, __func__);
    if (r) return IMAP_INTERNAL;

    if (annotations) {
        dlist_printbuf(annotations, 0, &annotations_buf);
    }

    struct sqldb_bindval mbox_bval[] = {
        { ":last_chunk_id",     SQLITE_INTEGER, { .i = backup->append_state->chunk_id } },
        { ":uniqueid",          SQLITE_TEXT,    { .s = uniqueid } },
        { ":mboxname",          SQLITE_TEXT,    { .s = mboxname } },
        { ":mboxtype",          SQLITE_TEXT,    { .s = mboxtype } },
        { ":last_uid",          SQLITE_INTEGER, { .i = last_uid } },
        { ":highestmodseq",     SQLITE_INTEGER, { .i = highestmodseq } },
        { ":recentuid",         SQLITE_INTEGER, { .i = recentuid } },
        { ":recenttime",        SQLITE_INTEGER, { .i = recenttime } },
        { ":last_appenddate",   SQLITE_INTEGER, { .i = last_appenddate } },
        { ":pop3_last_login",   SQLITE_INTEGER, { .i = pop3_last_login } },
        { ":pop3_show_after",   SQLITE_INTEGER, { .i = pop3_show_after } },
        { ":uidvalidity",       SQLITE_INTEGER, { .i = uidvalidity } },
        { ":partition",         SQLITE_TEXT,    { .s = partition } },
        { ":acl",               SQLITE_TEXT,    { .s = acl } },
        { ":options",           SQLITE_TEXT,    { .s = options } },
        { ":sync_crc",          SQLITE_INTEGER, { .i = synccrcs.basic } },
        { ":sync_crc_annot",    SQLITE_INTEGER, { .i = synccrcs.annot } },
        { ":quotaroot",         SQLITE_TEXT,    { .s = quotaroot } },
        { ":xconvmodseq",       SQLITE_INTEGER, { .i = xconvmodseq } },
        { ":annotations",       SQLITE_TEXT,    { .s = buf_cstring(&annotations_buf) } },
        { ":deleted",           SQLITE_NULL,    { .s = NULL } },
        { NULL,                 SQLITE_NULL,    { .s = NULL      } },
    };

    r = sqldb_exec(backup->db, backup_index_mailbox_update_sql,
                   mbox_bval, NULL, NULL);

    if (!r && sqldb_changes(backup->db) == 0) {
        r = sqldb_exec(backup->db, backup_index_mailbox_insert_sql,
                       mbox_bval, NULL, NULL);
        if (r) {
            syslog(LOG_DEBUG, "%s: something went wrong: %i insert %s\n",
                              __func__, r, mboxname);
        }
    }
    else if (r) {
        syslog(LOG_DEBUG, "%s: something went wrong: %i update %s\n",
                          __func__, r, mboxname);
    }

    buf_free(&annotations_buf);

    if (r) goto error;

    if (record->head) {
        int mailbox_id = backup_get_mailbox_id(backup, uniqueid);
        struct dlist *ki = NULL;

        for (ki = record->head; ki; ki = ki->next) {
            uint32_t uid = 0;
            modseq_t modseq = 0;
            uint32_t last_updated = 0;
            struct dlist *flags = NULL;
            struct buf flags_buf = BUF_INITIALIZER;
            uint32_t internaldate;
            const char *guid;
            struct dlist *annotations = NULL;
            struct buf annotations_buf = BUF_INITIALIZER;
            int message_id = -1;
            time_t expunged = 0;

            if (!dlist_getnum32(ki, "UID", &uid))
                goto error;
            if (!dlist_getnum64(ki, "MODSEQ", &modseq))
                goto error;
            if (!dlist_getnum32(ki, "LAST_UPDATED", &last_updated))
                goto error;
            if (!dlist_getnum32(ki, "INTERNALDATE", &internaldate))
                goto error;
            if (!dlist_getatom(ki, "GUID", &guid))
                goto error;

            /* XXX should this search for guid+size rather than just guid? */
            message_id = backup_get_message_id(backup, guid);
            if (message_id == -1) {
                syslog(LOG_DEBUG, "%s: something went wrong: %i %s %s\n",
                                  __func__, r, mboxname, guid);
                goto error;
            }
            if (message_id == 0) {
                /* can't link this record, we don't have a message */
                /* possibly we're in compact, and have deleted it? */
                /* XXX this should probably be an error too, but can't be until compact is smarter */
                syslog(LOG_DEBUG, "%s: skipping %s record for %s: message not found\n",
                                  __func__, mboxname, guid);
                continue;
            }

            dlist_getlist(ki, "FLAGS", &flags);
            if (flags) {
                int is_expunged = 0;

                _get_magic_flags(flags, &is_expunged);

                if (is_expunged) {
                    syslog(LOG_DEBUG, "%s: found expunge flag for message %s\n",
                                      __func__, guid);
                    expunged = ts;
                }

                dlist_printbuf(flags, 0, &flags_buf);
                syslog(LOG_DEBUG, "%s: found flags for message %s: %s\n",
                                  __func__, guid, buf_cstring(&flags_buf));
            }

            dlist_getlist(ki, "ANNOTATIONS", &annotations);
            if (annotations) {
                dlist_printbuf(annotations, 0, &annotations_buf);
            }

            struct sqldb_bindval record_bval[] = {
                { ":mailbox_id",        SQLITE_INTEGER, { .i = mailbox_id } },
                { ":message_id",        SQLITE_INTEGER, { .i = message_id } },
                { ":last_chunk_id",     SQLITE_INTEGER, { .i = backup->append_state->chunk_id } },
                { ":uid",               SQLITE_INTEGER, { .i = uid } },
                { ":modseq",            SQLITE_INTEGER, { .i = modseq } },
                { ":last_updated",      SQLITE_INTEGER, { .i = last_updated } },
                { ":flags",             SQLITE_TEXT,    { .s = buf_cstring(&flags_buf) } },
                { ":internaldate",      SQLITE_INTEGER, { .i = internaldate } },
                { ":annotations",       SQLITE_TEXT,    { .s = buf_cstring(&annotations_buf) } },
                { ":expunged",          SQLITE_NULL,    { .s = NULL      } },
                { NULL,                 SQLITE_NULL,    { .s = NULL      } },
            };

            /* provide an expunged value if we have one */
            if (expunged) {
                struct sqldb_bindval *expunged_bval = &record_bval[9];
                assert(strcmp(expunged_bval->name, ":expunged") == 0);
                expunged_bval->type = SQLITE_INTEGER;
                expunged_bval->val.i = expunged;
            }

            r = sqldb_exec(backup->db, backup_index_mailbox_message_update_sql,
                           record_bval, NULL, NULL);

            if (!r && sqldb_changes(backup->db) == 0) {
                r = sqldb_exec(backup->db, backup_index_mailbox_message_insert_sql,
                               record_bval, NULL, NULL);
                if (r) {
                    syslog(LOG_DEBUG, "%s: something went wrong: %i insert %s %s\n",
                                      __func__, r, mboxname, guid);
                }
            }
            else if (r) {
                syslog(LOG_DEBUG, "%s: something went wrong: %i update %s %s\n",
                                  __func__, r, mboxname, guid);
            }

            buf_free(&annotations_buf);
            buf_free(&flags_buf);

            if (r) goto error;
        }
    }

    syslog(LOG_DEBUG, "%s: committing index change: %s\n", __func__, mboxname);
    sqldb_commit(backup->db, __func__);
    return 0;

error:
    syslog(LOG_DEBUG, "%s: rolling back index change: %s\n", __func__, mboxname);
    sqldb_rollback(backup->db, __func__);

    return IMAP_INTERNAL;
}

static int _index_unmailbox(struct backup *backup, struct dlist *dl,
                            time_t ts, off_t dl_offset)
{
    syslog(LOG_DEBUG, "indexing UNMAILBOX at " OFF_T_FMT "...\n", dl_offset);

    const char *mboxname = dl->sval;

    struct sqldb_bindval bval[] = {
        { ":mboxname",  SQLITE_TEXT,    { .s = mboxname  } },
        { ":deleted",   SQLITE_INTEGER, { .i = ts        } },
        { NULL,         SQLITE_NULL,    { .s = NULL      } },
    };

    int r = sqldb_exec(backup->db, backup_index_mailbox_delete_sql, bval, NULL,
                        NULL);
    if (r) {
        syslog(LOG_DEBUG, "%s: something went wrong: %i %s\n",
                          __func__, r, mboxname);
    }

    return r ? IMAP_INTERNAL : 0;
}

static int _index_message(struct backup *backup, struct dlist *dl,
                          time_t ts, off_t dl_offset, size_t dl_len)
{
    syslog(LOG_DEBUG, "indexing MESSAGE at " OFF_T_FMT " (" SIZE_T_FMT " bytes)...\n", dl_offset, dl_len);
    (void) ts;

    struct dlist *di;
    int r = 0;

    /* n.b. APPLY MESSAGE contains a list of messages, not just one */
    for (di = dl->head; di && !r; di = di->next) {
        struct message_guid *guid = NULL;
        const char *partition = NULL;
        unsigned long size = 0;

        if (!dlist_tofile(di, &partition, &guid, &size, NULL))
            continue;

        struct sqldb_bindval bval[] = {
            { ":guid",      SQLITE_TEXT,    { .s = message_guid_encode(guid) } },
            { ":partition", SQLITE_TEXT,    { .s = partition } },
            { ":chunk_id",  SQLITE_INTEGER, { .i = backup->append_state->chunk_id } },
            { ":offset",    SQLITE_INTEGER, { .i = dl_offset } },
            { ":size",      SQLITE_INTEGER, { .i = size      } },
            { NULL,         SQLITE_NULL,    { .s = NULL      } },
        };

        int r = sqldb_exec(backup->db, backup_index_message_insert_sql, bval, NULL,
                           NULL);
        if (r) {
            syslog(LOG_DEBUG, "%s: something went wrong: %i %s\n",
                   __func__, r, message_guid_encode(guid));
        }
    }

    return r ? IMAP_INTERNAL : 0;
}

static int _index_rename(struct backup *backup, struct dlist *dl,
                         time_t ts, off_t dl_offset)
{
    syslog(LOG_DEBUG, "indexing RENAME at " OFF_T_FMT "\n", dl_offset);
    (void) ts;

    const char *uniqueid = NULL;
    const char *oldmboxname = NULL;
    const char *newmboxname = NULL;
    const char *partition = NULL;
    uint32_t uidvalidity = 0;
    int r = 0;

    /* XXX use uniqueid once sync proto includes it (D73) */
    dlist_getatom(dl, "UNIQUEID", &uniqueid);
    (void) uniqueid; /* silence unused warning */

    if (!dlist_getatom(dl, "OLDMBOXNAME", &oldmboxname))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getatom(dl, "NEWMBOXNAME", &newmboxname))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getatom(dl, "PARTITION", &partition))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getnum32(dl, "UIDVALIDITY", &uidvalidity))
        return IMAP_PROTOCOL_BAD_PARAMETERS;

    struct sqldb_bindval mbox_bval[] = {
//        { ":uniqueid",          SQLITE_TEXT,    { .s = uniqueid } },
        { ":oldmboxname",       SQLITE_TEXT,    { .s = oldmboxname } },
        { ":newmboxname",       SQLITE_TEXT,    { .s = newmboxname } },
        { ":partition",         SQLITE_TEXT,    { .s = partition } },
        { ":uidvalidity",       SQLITE_INTEGER, { .i = uidvalidity } },
        { NULL,                 SQLITE_NULL,    { .s = NULL      } },
    };

    r = sqldb_exec(backup->db, backup_index_mailbox_rename_sql,
                       mbox_bval, NULL, NULL);

    if (r) {
        syslog(LOG_DEBUG, "%s: something went wrong: %i rename %s => %s\n",
               __func__, r, oldmboxname, newmboxname);
    }

    return r ? IMAP_INTERNAL : 0;
}

static int _index_seen(struct backup *backup, struct dlist *dl,
                       time_t ts, off_t dl_offset)
{
    syslog(LOG_DEBUG, "indexing %s at " OFF_T_FMT "\n", dl->name, dl_offset);
    (void) ts;

    const char *uniqueid;
    time_t lastread;
    uint32_t lastuid;
    time_t lastchange;
    const char *seenuids;
    int r;

    if (!dlist_getatom(dl, "UNIQUEID", &uniqueid))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getdate(dl, "LASTREAD", &lastread))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getnum32(dl, "LASTUID", &lastuid))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getdate(dl, "LASTCHANGE", &lastchange))
        return IMAP_PROTOCOL_BAD_PARAMETERS;
    if (!dlist_getatom(dl, "SEENUIDS", &seenuids))
        return IMAP_PROTOCOL_BAD_PARAMETERS;

    struct sqldb_bindval bval[] =  {
        { ":last_chunk_id", SQLITE_INTEGER, { .i = backup->append_state->chunk_id } },
        { ":uniqueid",      SQLITE_TEXT,    { .s = uniqueid } },
        { ":lastread",      SQLITE_INTEGER, { .i = lastread } },
        { ":lastuid",       SQLITE_INTEGER, { .i = lastuid } },
        { ":lastchange",    SQLITE_INTEGER, { .i = lastchange } },
        { ":seenuids",      SQLITE_TEXT,    { .s = seenuids } },
        { NULL,             SQLITE_NULL,    { .s = NULL } },
    };

    r = sqldb_exec(backup->db, backup_index_seen_update_sql, bval,
                   NULL, NULL);

    if (!r && sqldb_changes(backup->db) == 0) {
        r = sqldb_exec(backup->db, backup_index_seen_insert_sql, bval,
                       NULL, NULL);
        if (r) {
            syslog(LOG_DEBUG, "%s: something went wrong: %i insert seen %s\n",
                   __func__, r, uniqueid);
        }
    }
    else if (r) {
        syslog(LOG_DEBUG, "%s: something went wrong: %i update seen %s",
               __func__, r, uniqueid);
    }

    return r ? IMAP_INTERNAL : 0;
}

static int _index_sub(struct backup *backup, struct dlist *dl,
                      time_t ts, off_t dl_offset)
{
    syslog(LOG_DEBUG, "indexing %s at " OFF_T_FMT "\n", dl->name, dl_offset);

    const char *mboxname = NULL;
    int r;

    if (!dlist_getatom(dl, "MBOXNAME", &mboxname))
        return IMAP_PROTOCOL_BAD_PARAMETERS;

    struct sqldb_bindval bval[] = {
        { ":last_chunk_id", SQLITE_INTEGER, { .i = backup->append_state->chunk_id } },
        { ":mboxname",      SQLITE_TEXT,    { .s = mboxname } },
        { ":unsubscribed",  SQLITE_NULL,    { .s = NULL } },
        { NULL,             SQLITE_NULL,    { .s = NULL } },
    };

    /* set the unsubscribed time if this is an UNSUB */
    if (!strcmp(dl->name, "UNSUB")) {
        syslog(LOG_DEBUG, "setting unsubscribed to %ld for %s", ts, mboxname);
        struct sqldb_bindval *unsubscribed_bval = &bval[2];
        assert(strcmp(unsubscribed_bval->name, ":unsubscribed") == 0);
        unsubscribed_bval->type = SQLITE_INTEGER;
        unsubscribed_bval->val.i = ts;
    }

    r = sqldb_exec(backup->db, backup_index_subscription_update_sql, bval,
                   NULL, NULL);

    if (!r && sqldb_changes(backup->db) == 0) {
        r = sqldb_exec(backup->db, backup_index_subscription_insert_sql, bval,
                       NULL, NULL);
        if (r) {
            syslog(LOG_DEBUG, "%s: something went wrong: %i insert subscription %s\n",
                   __func__, r, mboxname);
        }
    }
    else if (r) {
        syslog(LOG_DEBUG, "%s: something went wrong: %i update subscription %s",
               __func__, r, mboxname);
    }

    return r ? IMAP_INTERNAL : 0;
}

static int _index_sieve(struct backup *backup, struct dlist *dl,
                        time_t ts, off_t dl_offset)
{
    syslog(LOG_DEBUG, "indexing %s at " OFF_T_FMT "\n", dl->name, dl_offset);

    const char *filename;
    int r;

    if (!dlist_getatom(dl, "FILENAME", &filename))
        return IMAP_PROTOCOL_BAD_PARAMETERS;

    struct sqldb_bindval bval[] = {
        { ":chunk_id",      SQLITE_INTEGER, { .i = backup->append_state->chunk_id } },
        { ":last_update",   SQLITE_NULL,    { .s = NULL } },
        { ":filename",      SQLITE_TEXT,    { .s = filename } },
        { ":guid",          SQLITE_NULL,    { .s = NULL } },
        { ":offset",        SQLITE_INTEGER, { .i = dl_offset } },
        { ":deleted",       SQLITE_INTEGER, { .i = ts } },
        { NULL,             SQLITE_NULL,    { .s = NULL } },
    };

    /* mark previous record(s) for this filename as deleted */
    r = sqldb_exec(backup->db, backup_index_sieve_delete_sql, bval,
                   NULL, NULL);

    if (r) {
        syslog(LOG_DEBUG, "%s: something went wrong: %i delete sieve %s",
               __func__, r, filename);
    }

    if (!r && strcmp(dl->name, "SIEVE") == 0) {
        time_t last_update;
        const char *content;
        size_t content_len;
        struct message_guid guid;

        if (!dlist_getdate(dl, "LAST_UPDATE", &last_update))
            return IMAP_PROTOCOL_BAD_PARAMETERS;
        if (!dlist_getmap(dl, "CONTENT", &content, &content_len))
            return IMAP_PROTOCOL_BAD_PARAMETERS;

        message_guid_generate(&guid, content, content_len);

        struct sqldb_bindval *last_update_bval = &bval[1];
        assert(strcmp(last_update_bval->name, ":last_update") == 0);
        last_update_bval->type = SQLITE_INTEGER;
        last_update_bval->val.i = last_update;

        struct sqldb_bindval *guid_bval = &bval[3];
        assert(strcmp(guid_bval->name, ":guid") == 0);
        guid_bval->type = SQLITE_TEXT;
        guid_bval->val.s = message_guid_encode(&guid);

        /* insert doesn't use :deleted, but clear it still just in case */
        struct sqldb_bindval *deleted_bval = &bval[5];
        assert(strcmp(deleted_bval->name, ":deleted") == 0);
        deleted_bval->type = SQLITE_NULL;
        deleted_bval->val.s = NULL;

        /* insert the new record for this filename */
        r = sqldb_exec(backup->db, backup_index_sieve_insert_sql, bval,
                    NULL, NULL);

        if (r) {
            syslog(LOG_DEBUG, "%s: something went wrong: %i insert sieve %s",
                __func__, r, filename);
        }
    }

    return r ? IMAP_INTERNAL : 0;
}
