/* caldav_alarm.c -- implementation of global CalDAV alarm database
 *
 * Copyright (c) 1994-2012 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 <syslog.h>
#include <string.h>

#include <libical/ical.h>

#include "caldav_alarm.h"
#include "cyrusdb.h"
#include "exitcodes.h"
#include "httpd.h"
#include "http_dav.h"
#include "ical_support.h"
#include "libconfig.h"
#include "mboxevent.h"
#include "mboxname.h"
#include "util.h"
#include "xstrlcat.h"
#include "xmalloc.h"

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

struct caldav_alarm_data {
    char *mboxname;
    uint32_t imap_uid;
    time_t nextcheck;
};

void caldav_alarm_fini(struct caldav_alarm_data *alarmdata)
{
    free(alarmdata->mboxname);
    alarmdata->mboxname = NULL;
}

struct get_alarm_rock {
    const char *mboxname;
    uint32_t imap_uid;  // for logging
    icaltimezone *floatingtz;
    time_t last;
    time_t now;
    time_t nextcheck;
};

static struct namespace caldav_alarm_namespace;
icaltimezone *utc_zone = NULL;

EXPORTED int caldav_alarm_init(void)
{
    int r;

    utc_zone = icaltimezone_get_utc_timezone();

    /* Set namespace -- force standard (internal) */
    if ((r = mboxname_init_namespace(&caldav_alarm_namespace, 1))) {
        syslog(LOG_ERR, "%s", error_message(r));
        fatal(error_message(r), EC_CONFIG);
    }

    return sqldb_init();
}


EXPORTED int caldav_alarm_done(void)
{
    return sqldb_done();
}


#define CMD_CREATE                                      \
    "CREATE TABLE IF NOT EXISTS events ("               \
    " mboxname TEXT NOT NULL,"                          \
    " imap_uid INTEGER NOT NULL,"                       \
    " nextcheck INTEGER NOT NULL,"                      \
    " PRIMARY KEY (mboxname, imap_uid)"                 \
    ");"                                                \
    "CREATE INDEX IF NOT EXISTS checktime ON events (nextcheck);"


#define DBVERSION 2

/* the command loop will do the upgrade and then drop the old tables.
 * Sadly there's no other way to do it without creating a lock inversion! */
#define CMD_UPGRADEv2 CMD_CREATE

static struct sqldb_upgrade upgrade[] = {
    { 2, CMD_UPGRADEv2, NULL },
    /* always finish with an empty row */
    { 0, NULL, NULL }
};

static sqldb_t *my_alarmdb;
int refcount;
static struct mboxlock *my_alarmdb_lock;

/* get a database handle to the alarm db */
static sqldb_t *caldav_alarm_open()
{
    /* already running?  Bonus */
    if (refcount) {
        refcount++;
        return my_alarmdb;
    }

    /* we need exclusivity! */
    int r = mboxname_lock("$CALDAVALARMDB", &my_alarmdb_lock, LOCK_EXCLUSIVE);
    if (r) {
        syslog(LOG_ERR, "DBERROR: failed to lock $CALDAVALARMDB");
        return NULL;
    }

    // XXX - config option?
    char *dbfilename = strconcat(config_dir, "/caldav_alarm.sqlite3", NULL);
    my_alarmdb = sqldb_open(dbfilename, CMD_CREATE, DBVERSION, upgrade);

    if (!my_alarmdb) {
        syslog(LOG_ERR, "DBERROR: failed to open %s", dbfilename);
        mboxname_release(&my_alarmdb_lock);
    }

    free(dbfilename);
    refcount = 1;
    return my_alarmdb;
}

/* close this handle */
static int caldav_alarm_close(sqldb_t *alarmdb)
{
    assert(my_alarmdb == alarmdb);

    if (--refcount) return 0;

    sqldb_close(&my_alarmdb);
    mboxname_release(&my_alarmdb_lock);

    return 0;
}

/*
 * Extract data from the given ical object
 */
static int send_alarm(struct get_alarm_rock *rock,
                      icalcomponent *comp, icalcomponent *alarm,
                      icaltimetype start, icaltimetype end, icaltimetype alarmtime)
{
    char *userid = mboxname_to_userid(rock->mboxname);
    struct buf calname = BUF_INITIALIZER;

    /* get the display name annotation */
    const char *displayname_annot = DAV_ANNOT_NS "<" XML_NS_DAV ">displayname";
    annotatemore_lookupmask(rock->mboxname, displayname_annot, userid, &calname);
    if (!calname.len) buf_setcstr(&calname, strrchr(rock->mboxname, '.') + 1);

    struct mboxevent *event = mboxevent_new(EVENT_CALENDAR_ALARM);
    icalproperty *prop;
    icalvalue *val;

    FILL_STRING_PARAM(event, EVENT_CALENDAR_ALARM_TIME,
                      xstrdup(icaltime_as_ical_string(alarmtime)));

    prop = icalcomponent_get_first_property(alarm, ICAL_ACTION_PROPERTY);
    val = icalproperty_get_value(prop);
    enum icalproperty_action action = icalvalue_get_action(val);
    if (action == ICAL_ACTION_DISPLAY) {
        FILL_STRING_PARAM(event, EVENT_CALENDAR_ACTION, xstrdup("display"));
    }
    else {
        FILL_STRING_PARAM(event, EVENT_CALENDAR_ACTION, xstrdup("email"));
    }

    FILL_STRING_PARAM(event, EVENT_CALENDAR_USER_ID, xstrdup(userid));
    FILL_STRING_PARAM(event, EVENT_CALENDAR_CALENDAR_NAME, buf_release(&calname));

    prop = icalcomponent_get_first_property(comp, ICAL_UID_PROPERTY);
    FILL_STRING_PARAM(event, EVENT_CALENDAR_UID,
                      xstrdup(prop ? icalproperty_get_value_as_string(prop) : ""));

    prop = icalcomponent_get_first_property(comp, ICAL_SUMMARY_PROPERTY);
    FILL_STRING_PARAM(event, EVENT_CALENDAR_SUMMARY,
                      xstrdup(prop ? icalproperty_get_value_as_string(prop) : ""));

    prop = icalcomponent_get_first_property(comp, ICAL_DESCRIPTION_PROPERTY);
    FILL_STRING_PARAM(event, EVENT_CALENDAR_DESCRIPTION,
                      xstrdup(prop ? icalproperty_get_value_as_string(prop) : ""));

    prop = icalcomponent_get_first_property(comp, ICAL_LOCATION_PROPERTY);
    FILL_STRING_PARAM(event, EVENT_CALENDAR_LOCATION,
                      xstrdup(prop ? icalproperty_get_value_as_string(prop) : ""));

    prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
    FILL_STRING_PARAM(event, EVENT_CALENDAR_ORGANIZER,
                      xstrdup(prop ? icalproperty_get_value_as_string(prop) : ""));

    const char *timezone = NULL;
    if (!icaltime_is_date(start) && icaltime_is_utc(start))
        timezone = "UTC";
    else if (icaltime_get_timezone(start))
        timezone = icaltime_get_tzid(start);
    else if (rock->floatingtz)
        timezone = icaltimezone_get_tzid(rock->floatingtz);
    else
        timezone = "[floating]";
    FILL_STRING_PARAM(event, EVENT_CALENDAR_TIMEZONE,
                      xstrdup(timezone));
    FILL_STRING_PARAM(event, EVENT_CALENDAR_START,
                      xstrdup(icaltime_as_ical_string(start)));
    FILL_STRING_PARAM(event, EVENT_CALENDAR_END,
                      xstrdup(icaltime_as_ical_string(end)));
    FILL_UNSIGNED_PARAM(event, EVENT_CALENDAR_ALLDAY,
                        icaltime_is_date(start) ? 1 : 0);

    strarray_t *recipients = strarray_new();
    for (prop = icalcomponent_get_first_property(alarm, ICAL_ATTENDEE_PROPERTY);
         prop;
         prop = icalcomponent_get_next_property(alarm, ICAL_ATTENDEE_PROPERTY)) {
        const char *email = icalproperty_get_value_as_string(prop);
        if (!email)
            continue;
        strarray_append(recipients, email);
    }
    FILL_ARRAY_PARAM(event, EVENT_CALENDAR_ALARM_RECIPIENTS, recipients);

    strarray_t *attendee_names = strarray_new();
    strarray_t *attendee_emails = strarray_new();
    strarray_t *attendee_status = strarray_new();
    for (prop = icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY);
         prop;
         prop = icalcomponent_get_next_property(comp, ICAL_ATTENDEE_PROPERTY)) {
        const char *email = icalproperty_get_value_as_string(prop);
        if (!email)
            continue;
        strarray_append(attendee_emails, email);

        const char *name = icalproperty_get_parameter_as_string(prop, "CN");
        strarray_append(attendee_names, name ? name : "");

        const char *partstat = icalproperty_get_parameter_as_string(prop, "PARTSTAT");
        strarray_append(attendee_status, partstat ? partstat : "");
    }
    FILL_ARRAY_PARAM(event, EVENT_CALENDAR_ATTENDEE_NAMES, attendee_names);
    FILL_ARRAY_PARAM(event, EVENT_CALENDAR_ATTENDEE_EMAILS, attendee_emails);
    FILL_ARRAY_PARAM(event, EVENT_CALENDAR_ATTENDEE_STATUS, attendee_status);

    mboxevent_notify(&event);
    mboxevent_free(&event);

    strarray_free(recipients);
    strarray_free(attendee_names);
    strarray_free(attendee_emails);
    strarray_free(attendee_status);

    buf_free(&calname);
    free(userid);

    return 0;
}

static int process_alarm_cb(icalcomponent *comp, icaltimetype start,
                            icaltimetype end, void *rock)
{
    struct get_alarm_rock *data = (struct get_alarm_rock *)rock;

    icalcomponent *alarm;
    icalproperty *prop;
    icalvalue *val;

    int alarmno = 0;

    for (alarm = icalcomponent_get_first_component(comp, ICAL_VALARM_COMPONENT);
         alarm;
         alarm = icalcomponent_get_next_component(comp, ICAL_VALARM_COMPONENT)) {

        alarmno++;

        prop = icalcomponent_get_first_property(alarm, ICAL_ACTION_PROPERTY);
        if (!prop) {
            /* no action, invalid alarm, skip */
            continue;
        }

        val = icalproperty_get_value(prop);
        enum icalproperty_action action = icalvalue_get_action(val);
        if (!(action == ICAL_ACTION_DISPLAY || action == ICAL_ACTION_EMAIL)) {
            /* we only want DISPLAY and EMAIL, skip */
            continue;
        }

        prop = icalcomponent_get_first_property(alarm, ICAL_TRIGGER_PROPERTY);
        if (!prop) {
            /* no trigger, invalid alarm, skip */
            continue;
        }

        val = icalproperty_get_value(prop);

        struct icaltriggertype trigger = icalvalue_get_trigger(val);
        /* XXX validate trigger */

        icaltimetype alarmtime = icaltime_null_time();
        if (icalvalue_isa(val) == ICAL_DURATION_VALUE) {
            icalparameter *param = icalproperty_get_first_parameter(prop, ICAL_RELATED_PARAMETER);
            icaltimetype base = icaltime_null_time();
            if (param && icalparameter_get_related(param) == ICAL_RELATED_END) {
                base = end;
            }
            else {
                base = start;
            }
            base.is_date = 0; /* need an actual time for triggers */
            alarmtime = icaltime_add(base, trigger.duration);
        }
        else {
            /* absolute */
            alarmtime = trigger.time;
        }
        alarmtime.is_date = 0;

        time_t check = icaltime_to_timet(alarmtime, data->floatingtz);

        /* skip already sent alarms */
        if (check <= data->last) {
            continue;
        }

        if (check <= data->now) {
            prop = icalcomponent_get_first_property(comp, ICAL_SUMMARY_PROPERTY);
            const char *summary = prop ? icalproperty_get_value_as_string(prop) : "[no summary]";
            int age = data->now - check;
            if (age > 7200) { // more than 2 hours stale?  Just log it
                syslog(LOG_ERR, "suppressing alarm aged %d seconds at %s for %s %u - %s(%d) %s",
                       age,
                       icaltime_as_ical_string(alarmtime),
                       data->mboxname, data->imap_uid,
                       icaltime_as_ical_string(start), alarmno,
                       summary);
            }
            else {
                syslog(LOG_NOTICE, "sending alarm at %s for %s %u - %s(%d) %s",
                       icaltime_as_ical_string(alarmtime),
                       data->mboxname, data->imap_uid,
                       icaltime_as_ical_string(start), alarmno,
                       summary);
                send_alarm(data, comp, alarm, start, end, alarmtime);
            }
        }

        else if (!data->nextcheck || check < data->nextcheck) {
            data->nextcheck = check;
        }

        /* alarms can't be more than a week either side of the event start, so if we're
         * past 2 months, then just check again in a month */
        if (check > data->now + 86400*60) {
            time_t next = data->now + 86400*30;
            if (!data->nextcheck || next < data->nextcheck)
                data->nextcheck = next;
            return 0;
        }
    }

    return 1; /* keep going */
}

#define CMD_REPLACE                              \
    "REPLACE INTO events"                        \
    " ( mboxname, imap_uid, nextcheck )"         \
    " VALUES"                                    \
    " ( :mboxname, :imap_uid, :nextcheck )"      \
    ";"

#define CMD_DELETE                               \
    "DELETE FROM events"                         \
    " WHERE mboxname = :mboxname"                \
    "   AND imap_uid = :imap_uid"                \
    ";"

static int update_alarmdb(const char *mboxname, uint32_t imap_uid, time_t nextcheck)
{
    struct sqldb_bindval bval[] = {
        { ":mboxname",  SQLITE_TEXT,    { .s = mboxname  } },
        { ":imap_uid",  SQLITE_INTEGER, { .i = imap_uid  } },
        { ":nextcheck", SQLITE_INTEGER, { .i = nextcheck } },
        { NULL,         SQLITE_NULL,    { .s = NULL      } }
    };

    sqldb_t *alarmdb = caldav_alarm_open();
    if (!alarmdb) return -1;
    int rc = SQLITE_OK;

    if (nextcheck)
        rc = sqldb_exec(alarmdb, CMD_REPLACE, bval, NULL, NULL);
    else
        rc = sqldb_exec(alarmdb, CMD_DELETE, bval, NULL, NULL);

    caldav_alarm_close(alarmdb);

    if (rc == SQLITE_OK) return 0;

    /* failed? */
    return -1;
}

static icaltimezone *get_floatingtz(struct mailbox *mailbox)
{
    icaltimezone *floatingtz = NULL;

    struct buf buf = BUF_INITIALIZER;
    const char *annotname = DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-timezone";
    if (!annotatemore_lookup(mailbox->name, annotname, /*userid*/"", &buf)) {
        icalcomponent *comp = NULL;
        comp = icalparser_parse_string(buf_cstring(&buf));
        icalcomponent *subcomp = icalcomponent_get_first_component(comp, ICAL_VTIMEZONE_COMPONENT);
        if (subcomp) {
            floatingtz = icaltimezone_new();
            icalcomponent_remove_component(comp, subcomp);
            icaltimezone_set_component(floatingtz, subcomp);
        }
        icalcomponent_free(comp);
    }
    buf_free(&buf);

    return floatingtz;
}

static int has_alarms(icalcomponent *ical)
{
    icalcomponent *comp = icalcomponent_get_first_real_component(ical);
    icalcomponent_kind kind = icalcomponent_isa(comp);
    for (; comp; comp = icalcomponent_get_next_component(ical, kind)) {
        if (icalcomponent_get_first_component(comp, ICAL_VALARM_COMPONENT))
            return 1;
    }
    return 0;
}

static time_t process_alarms(const char *mboxname, uint32_t imap_uid, icaltimezone *floatingtz,
                             icalcomponent *ical, time_t lastrun, time_t runtime)
{
    /* we don't send alarms for anything except VEVENTS */
    icalcomponent *comp = icalcomponent_get_first_real_component(ical);
    icalcomponent_kind kind = icalcomponent_isa(comp);
    if (kind != ICAL_VEVENT_COMPONENT)
        return 0;

    struct get_alarm_rock rock = { mboxname, imap_uid, floatingtz, lastrun, runtime, 0 };
    struct icalperiodtype range = icalperiodtype_null_period();
    icalcomponent_myforeach(ical, range, floatingtz, process_alarm_cb, &rock);
    return rock.nextcheck;
}

struct lastalarm_data {
    time_t lastrun;
    time_t nextcheck;
};

static int read_lastalarm(struct mailbox *mailbox, const struct index_record *record,
                          struct lastalarm_data *data)
{
    int r = IMAP_NOTFOUND;
    memset(data, 0, sizeof(struct lastalarm_data));

    const char *annotname = DAV_ANNOT_NS "lastalarm";
    struct buf annot_buf = BUF_INITIALIZER;
    annotatemore_msg_lookup(mailbox->name, record->uid, annotname, "", &annot_buf);

    if (annot_buf.len) {
        char *base = (char *)buf_cstring(&annot_buf);
        data->lastrun = strtoul(base, &base, 10);
        if (*base == ' ') base++;
        data->nextcheck = strtoul(base, &base, 10);
        r = 0;
    }

    buf_free(&annot_buf);
    return r;
}

/* add a calendar alarm */
EXPORTED int caldav_alarm_add_record(struct mailbox *mailbox, const struct index_record *record,
                                     icalcomponent *ical)
{
    if (!has_alarms(ical)) return 0;
    // we need to skip silent records (replication) because the lastalarm annotation won't be
    // set yet, so it will all break :(  Instead, we have an explicit touch on the record which
    // is done after the annotations are written, and processes the alarms if needed then, and
    // regardless will always update the alarmdb
    if (record->silent) return 0;
    int rc = 0;

    /* XXX - we COULD cache this in the mailbox object so it doesn't get read multiple times,
     * but this is really rare - only dav_reconstruct maybe */
    icaltimezone *floatingtz = get_floatingtz(mailbox);

    struct lastalarm_data data;
    if (read_lastalarm(mailbox, record, &data))
        data.lastrun = record->internaldate;
    data.nextcheck = process_alarms(mailbox->name, record->uid, floatingtz,
                                    ical, data.lastrun, data.lastrun);
    rc = update_alarmdb(mailbox->name, record->uid, data.nextcheck);

    if (floatingtz) icaltimezone_free(floatingtz, 1);

    return rc;
}

EXPORTED int caldav_alarm_touch_record(struct mailbox *mailbox, const struct index_record *record)
{
    struct lastalarm_data data;
    if (!read_lastalarm(mailbox, record, &data))
        return update_alarmdb(mailbox->name, record->uid, data.nextcheck);
    return 0;
}

/* delete all alarms matching the event */
EXPORTED int caldav_alarm_delete_record(const char *mboxname, uint32_t imap_uid)
{
    return update_alarmdb(mboxname, imap_uid, 0);
}

#define CMD_DELETEMAILBOX       \
    "DELETE FROM events WHERE"  \
    " mboxname = :mboxname"     \
    ";"

/* delete all alarms matching the event */
EXPORTED int caldav_alarm_delete_mailbox(const char *mboxname)
{
    struct sqldb_bindval bval[] = {
        { ":mboxname",  SQLITE_TEXT, { .s = mboxname  } },
        { NULL,         SQLITE_NULL, { .s = NULL      } }
    };

    sqldb_t *alarmdb = caldav_alarm_open();
    int rc = sqldb_exec(alarmdb, CMD_DELETEMAILBOX, bval, NULL, NULL);
    caldav_alarm_close(alarmdb);

    return rc;
}

#define CMD_DELETEUSER          \
    "DELETE FROM events WHERE"  \
    " mboxname LIKE :prefix"     \
    ";"

/* delete all alarms matching the event */
EXPORTED int caldav_alarm_delete_user(const char *userid)
{
    mbname_t *mbname = mbname_from_userid(userid);
    const char *mboxname = mbname_intname(mbname);
    char *prefix = strconcat(mboxname, ".%", (char *)NULL);
    mbname_free(&mbname);

    struct sqldb_bindval bval[] = {
        { ":prefix",    SQLITE_TEXT, { .s = prefix  } },
        { NULL,         SQLITE_NULL, { .s = NULL    } }
    };

    sqldb_t *alarmdb = caldav_alarm_open();
    int rc = sqldb_exec(alarmdb, CMD_DELETEUSER, bval, NULL, NULL);
    caldav_alarm_close(alarmdb);

    free(prefix);

    return rc;
}

#define CMD_SELECT_ALARMS                                                \
    "SELECT mboxname, imap_uid, nextcheck"                               \
    " FROM events WHERE"                                                 \
    " nextcheck < :before"                                               \
    " ORDER BY mboxname, imap_uid"                                       \
    ";"

static int alarm_read_cb(sqlite3_stmt *stmt, void *rock)
{
    ptrarray_t *target = (ptrarray_t *)rock;

    struct caldav_alarm_data *data = xzmalloc(sizeof(struct caldav_alarm_data));

    data->mboxname    = xstrdup((const char *) sqlite3_column_text(stmt, 0));
    data->imap_uid    = sqlite3_column_int(stmt, 1);
    data->nextcheck   = sqlite3_column_int(stmt, 2);

    ptrarray_append(target, data);

    return 0;
}

static void process_one_record(struct mailbox *mailbox, uint32_t imap_uid,
                               icaltimezone *floatingtz, time_t runtime)
{
    int rc;
    icalcomponent *ical = NULL;
    struct buf msg_buf = BUF_INITIALIZER;

    syslog(LOG_DEBUG, "processing alarms for mailbox %s uid %u",
           mailbox->name, imap_uid);

    struct index_record record;
    memset(&record, 0, sizeof(struct index_record));
    rc = mailbox_find_index_record(mailbox, imap_uid, &record);
    if (rc == IMAP_NOTFOUND) {
        /* no record, no worries */
        goto done_item;
    }
    if (rc) {
        /* XXX no index record? item deleted or transient error? */
        goto done_item;
    }
    if (record.system_flags & FLAG_EXPUNGED) {
        /* no longer exists?  nothing to do */
        goto done_item;
    }

    rc = mailbox_map_record(mailbox, &record, &msg_buf);
    if (rc) {
        /* XXX no message? index is wrong? yikes */
        goto done_item;
    }

    ical = icalparser_parse_string(buf_cstring(&msg_buf) + record.header_size);

    if (!ical) {
        /* XXX log error */
        goto done_item;
    }

    struct lastalarm_data data;
    if (read_lastalarm(mailbox, &record, &data))
        data.lastrun = record.internaldate;

    if (runtime > data.nextcheck)
        data.nextcheck = process_alarms(mailbox->name, record.uid, floatingtz, ical, data.lastrun, runtime);

    struct buf annot_buf = BUF_INITIALIZER;
    buf_printf(&annot_buf, "%ld %ld", runtime, data.nextcheck);
    const char *annotname = DAV_ANNOT_NS "lastalarm";
    mailbox_annotation_write(mailbox, record.uid, annotname, "", &annot_buf);
    buf_free(&annot_buf);

done_item:
    buf_free(&msg_buf);
    if (ical) icalcomponent_free(ical);
}

static void process_records(ptrarray_t *list, time_t runtime)
{
    struct mailbox *mailbox = NULL;
    int rc;
    int i;
    icaltimezone *floatingtz = NULL;

    for (i = 0; i < list->count; i++) {
        struct caldav_alarm_data *data = ptrarray_nth(list, i);

        if (mailbox && !strcmp(mailbox->name, data->mboxname)) {
            /* woot, reuse mailbox */
        }
        else {
            if (floatingtz) icaltimezone_free(floatingtz, 1);
            floatingtz = NULL;
            mailbox_close(&mailbox);
            rc = mailbox_open_iwl(data->mboxname, &mailbox);
            if (rc == IMAP_MAILBOX_NONEXISTENT) {
                /* mailbox was deleted or something, nothing we can do */
                data->nextcheck = 0;
                continue;
            }
            if (rc) {
                /* transient open error, don't delete this alarm */
                continue;
            }
            floatingtz = get_floatingtz(mailbox);
        }
        process_one_record(mailbox, data->imap_uid, floatingtz, runtime);
    }

    if (floatingtz) icaltimezone_free(floatingtz, 1);
    mailbox_close(&mailbox);
}

/* process alarms with triggers before a given time */
EXPORTED int caldav_alarm_process(time_t runtime)
{
    syslog(LOG_DEBUG, "processing alarms");

    if (!runtime) {
        /* check 10s into the future - we run every 10, so that guarantees we will
         * deliver on or before the target time */
        runtime = time(NULL) + 10;
    }

    struct sqldb_bindval bval[] = {
        { ":before",    SQLITE_INTEGER, { .i = runtime  } },
        { NULL,         SQLITE_NULL,    { .s = NULL     } }
    };

    sqldb_t *alarmdb = caldav_alarm_open();
    if (!alarmdb)
        return HTTP_SERVER_ERROR;

    ptrarray_t list = PTRARRAY_INITIALIZER;

    int rc = sqldb_exec(alarmdb, CMD_SELECT_ALARMS, bval, &alarm_read_cb, &list);

    caldav_alarm_close(alarmdb);

    process_records(&list, runtime);

    int i;
    for (i = 0; i < list.count; i++) {
        struct caldav_alarm_data *data = ptrarray_nth(&list, i);
        caldav_alarm_fini(data);
        free(data);
    }
    ptrarray_fini(&list);

    return rc;
}

static int upgrade_read_cb(sqlite3_stmt *stmt, void *rock)
{
    strarray_t *target = (strarray_t *)rock;

    strarray_append(target, (const char *) sqlite3_column_text(stmt, 0));

    return 0;
}

#define CMD_READ_OLDALARMS "SELECT DISTINCT mailbox FROM alarms;"

EXPORTED int caldav_alarm_upgrade()
{
    syslog(LOG_DEBUG, "checking if alarm database needs upgrading");

    struct mailbox *mailbox = NULL;

    strarray_t mailboxes = STRARRAY_INITIALIZER;

    sqldb_t *alarmdb = caldav_alarm_open();
    if (!alarmdb) return HTTP_SERVER_ERROR;
    int rc = sqldb_exec(alarmdb, "SELECT DISTINCT mailbox FROM alarms;",
                        NULL, &upgrade_read_cb, &mailboxes);
    caldav_alarm_close(alarmdb);

    time_t runtime = time(NULL);
    const char *annotname = DAV_ANNOT_NS "lastalarm";
    struct buf annot_buf = BUF_INITIALIZER;

    int i;
    for (i = 0; i < strarray_size(&mailboxes); i++) {
        const char *mboxname = strarray_nth(&mailboxes, i);
        syslog(LOG_DEBUG, "UPDATING calalarm database for %s", mboxname);
        rc = mailbox_open_iwl(mboxname, &mailbox);
        if (rc) continue;

        sqldb_t *alarmdb = caldav_alarm_open();
        /* clean up any existing alarms for this mailbox */
        struct sqldb_bindval bval[] = {
            { ":mboxname",  SQLITE_TEXT, { .s = mboxname  } },
            { NULL,         SQLITE_NULL, { .s = NULL      } }
        };
        rc = sqldb_exec(alarmdb, CMD_DELETEMAILBOX, bval, NULL, NULL);
        caldav_alarm_close(alarmdb);
        if (rc) continue;

        icaltimezone *floatingtz = get_floatingtz(mailbox);

        /* add alarms for all records */
        struct mailbox_iter *iter = mailbox_iter_init(mailbox, 0, ITER_SKIP_EXPUNGED);
        const message_t *msg;
        while ((msg = mailbox_iter_step(iter))) {
            const struct index_record *record = msg_record(msg);
            struct buf msg_buf = BUF_INITIALIZER;
            rc = mailbox_map_record(mailbox, record, &msg_buf);
            if (rc) continue;
            icalcomponent *ical = icalparser_parse_string(buf_cstring(&msg_buf) + record->header_size);
            if (ical) {
                if (has_alarms(ical)) {
                    time_t nextcheck = process_alarms(mailbox->name, record->uid, floatingtz, ical, runtime, runtime);
                    buf_reset(&annot_buf);
                    buf_printf(&annot_buf, "%ld %ld", runtime, nextcheck);
                    mailbox_annotation_write(mailbox, record->uid, annotname, "", &annot_buf);
                }
                icalcomponent_free(ical);
            }
            buf_free(&msg_buf);
        }
        mailbox_iter_done(&iter);
        mailbox_close(&mailbox);

        if (floatingtz) icaltimezone_free(floatingtz, 1);
    }

    strarray_fini(&mailboxes);

    alarmdb = caldav_alarm_open();
    if (!alarmdb) return HTTP_SERVER_ERROR;
    sqldb_exec(alarmdb, "DROP TABLE alarm_recipients;", NULL, NULL, NULL);
    sqldb_exec(alarmdb, "DROP TABLE alarms;", NULL, NULL, NULL);
    caldav_alarm_close(alarmdb);

    buf_free(&annot_buf);

    return rc;
}
