/* jmap_vacation.c -- Routines for handling JMAP vacation responses
 *
 * Copyright (c) 1994-2019 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 <ctype.h>
#include <string.h>
#include <syslog.h>
#include <assert.h>
#include <errno.h>

#include "hash.h"
#include "http_jmap.h"
#include "json_support.h"
#include "map.h"
#include "sieve/sieve_interface.h"
#include "sieve/bc_parse.h"
#include "sync_support.h"
#include "user.h"
#include "util.h"

static int jmap_vacation_get(jmap_req_t *req);
static int jmap_vacation_set(jmap_req_t *req);

jmap_method_t jmap_vacation_methods_standard[] = {
    {
        "VacationResponse/get",
        JMAP_URN_VACATION,
        &jmap_vacation_get,
        /*flags*/0
    },
    {
        "VacationResponse/set",
        JMAP_URN_VACATION,
        &jmap_vacation_set,
        JMAP_READ_WRITE
    },
    { NULL, NULL, NULL, 0}
};

jmap_method_t jmap_vacation_methods_nonstandard[] = {
    { NULL, NULL, NULL, 0}
};

static int sieve_vacation_enabled = 0;

HIDDEN void jmap_vacation_init(jmap_settings_t *settings)
{
    if (!config_getswitch(IMAPOPT_JMAP_VACATION)) return;

#ifdef USE_SIEVE
    unsigned long config_ext = config_getbitfield(IMAPOPT_SIEVE_EXTENSIONS);
    unsigned long required =
        IMAP_ENUM_SIEVE_EXTENSIONS_VACATION   |
        IMAP_ENUM_SIEVE_EXTENSIONS_RELATIONAL |
        IMAP_ENUM_SIEVE_EXTENSIONS_DATE;

    sieve_vacation_enabled = ((config_ext & required) == required);
#endif /* USE_SIEVE */

    if (!sieve_vacation_enabled) return;

    jmap_method_t *mp;
    for (mp = jmap_vacation_methods_standard; mp->name; mp++) {
        hash_insert(mp->name, mp, &settings->methods);
    }

    json_object_set_new(settings->server_capabilities,
            JMAP_URN_VACATION, json_object());

    if (config_getswitch(IMAPOPT_JMAP_NONSTANDARD_EXTENSIONS)) {
        for (mp = jmap_vacation_methods_nonstandard; mp->name; mp++) {
            hash_insert(mp->name, mp, &settings->methods);
        }
    }

}

HIDDEN void jmap_vacation_capabilities(json_t *account_capabilities)
{
    if (!sieve_vacation_enabled) return;

    json_object_set_new(account_capabilities, JMAP_URN_VACATION, json_object());
}

/* VacationResponse/get method */
static const jmap_property_t vacation_props[] = {
    {
        "id",
        NULL,
        JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE | JMAP_PROP_ALWAYS_GET
    },
    {
        "isEnabled",
        NULL,
        0
    },
    {
        "fromDate",
        NULL,
        0
    },
    {
        "toDate",
        NULL,
        0
    },
    {
        "subject",
        NULL,
        0
    },
    {
        "textBody",
        NULL,
        0
    },
    {
        "htmlBody",
        NULL,
        0
    },

    { NULL, NULL, 0 }
};

#define SCRIPT_NAME      "jmap_vacation"
#define SCRIPT_SUFFIX    ".script"
#define BYTECODE_SUFFIX  ".bc"
#define DEFAULTBC_NAME   "defaultbc"

#define STATUS_ACTIVE    (1<<0)
#define STATUS_CUSTOM    (1<<1)
#define STATUS_ENABLE    (1<<2)

#define SCRIPT_HEADER    "/* Generated by Cyrus JMAP - DO NOT EDIT\r\n\r\n"

#define DEFAULT_MESSAGE  "I'm away at the moment." \
    "  I'll read your message and get back to you as soon as I can."

#define NO_INCLUDE_ERROR "Can not enable the vacation response" \
    " because the active Sieve script does not" \
    " properly include the 'jmap_vacation' script."

static char *vacation_state(const char *userid)
{
    const char *sieve_dir = user_sieve_path(userid);
    char *bcname = strconcat(sieve_dir, "/" SCRIPT_NAME BYTECODE_SUFFIX, NULL);
    struct buf buf = BUF_INITIALIZER;
    struct stat sbuf;
    time_t state = 0;

    if (!stat(bcname, &sbuf)) state = sbuf.st_mtime;
    free(bcname);

    buf_printf(&buf, "%ld", state);

    return buf_release(&buf);
}

static json_t *vacation_read(const char *userid, unsigned *status)
{
    const char *sieve_dir = user_sieve_path(userid);
    char *scriptname = strconcat(sieve_dir, "/" SCRIPT_NAME SCRIPT_SUFFIX, NULL);
    json_t *vacation = NULL;
    int fd;

    /* Parse JMAP from vacation script */
    if ((fd = open(scriptname, O_RDONLY)) != -1) {
        const char *base = NULL, *json;
        size_t len = 0;

        map_refresh(fd, 1, &base, &len, MAP_UNKNOWN_LEN, scriptname, NULL);
        json = strstr(base, SCRIPT_HEADER);
        if (json) {
            json_error_t jerr;

            json += strlen(SCRIPT_HEADER);
            vacation = json_loadb(json, len - (json - base),
                                  JSON_DISABLE_EOF_CHECK, &jerr);
        }
        map_free(&base, &len);
        close(fd);
    }

    free(scriptname);

    if (vacation) {
        int isEnabled =
            json_boolean_value(json_object_get(vacation, "isEnabled"));
        int isActive = 0;

#ifdef USE_SIEVE
        /* Check if vacation script is really active */
        char *defaultbc = strconcat(sieve_dir, "/" DEFAULTBC_NAME, NULL);
        char *activebc =  sieve_getdefaultbcfname(defaultbc);

        if (activebc) {
            const char *filename = activebc + strlen(sieve_dir) + 1;

            if (!strcmp(filename, SCRIPT_NAME BYTECODE_SUFFIX)) {
                /* Vacation script itself is active */
                isActive = 1;
            }
            else if ((fd = open(activebc, O_RDONLY)) != -1) {
                /* Parse active bytecode to see if vacation script is included */
                bytecode_input_t *bc = NULL;
                const char *base = NULL;
                size_t len = 0;
                int i, version, requires;

                if (status) *status |= STATUS_CUSTOM;

                map_refresh(fd, 1, &base, &len, MAP_UNKNOWN_LEN, activebc, NULL);
                bc = (bytecode_input_t *) base;

                i = bc_header_parse(bc, &version, &requires);
                while (i > 0 && i < (int) len) {
                    commandlist_t cmd;

                    i = bc_action_parse(bc, i, version, &cmd);
                    if (cmd.type == B_INCLUDE &&
                        cmd.u.inc.location == B_PERSONAL &&
                        !strcmp(cmd.u.inc.script, SCRIPT_NAME)) {
                        /* Found it! */
                        isActive = 1;
                        break;
                    }
                    else if (cmd.type == B_IF) {
                        /* Skip over test */
                        i = cmd.u.i.testend;
                    }
                }

                map_free(&base, &len);
                close(fd);
            }
        }

        free(activebc);
        free(defaultbc);
#endif /* USE_SIEVE */

        isEnabled = isActive && isEnabled;
        json_object_set_new(vacation, "isEnabled", json_boolean(isEnabled));

        if (status && isActive) *status |= STATUS_ACTIVE;
    }
    else {
        /* Build empty response */
        vacation = json_pack("{ s:s s:b s:n s:n s:n s:s s:n }",
                             "id", "singleton", "isEnabled", 0,
                             "fromDate", "toDate", "subject",
                             "textBody", DEFAULT_MESSAGE, "htmlBody");
    }

    return vacation;
}

static void vacation_get(const char *userid, struct jmap_get *get)
{
    /* Read script */
    json_t *vacation = vacation_read(userid, NULL);

    /* Strip unwanted properties */
    if (!jmap_wantprop(get->props, "isEnabled"))
        json_object_del(vacation, "isEnabled");
    if (!jmap_wantprop(get->props, "fromDate"))
        json_object_del(vacation, "fromDate");
    if (!jmap_wantprop(get->props, "toDate"))
        json_object_del(vacation, "toDate");
    if (!jmap_wantprop(get->props, "subject"))
        json_object_del(vacation, "subject");
    if (!jmap_wantprop(get->props, "textBody"))
        json_object_del(vacation, "textBody");
    if (!jmap_wantprop(get->props, "htmlBody"))
        json_object_del(vacation, "htmlBody");

    /* Add object to list */
    json_array_append_new(get->list, vacation);
}

static int jmap_vacation_get(jmap_req_t *req)
{
    struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
    struct jmap_get get;
    json_t *err = NULL;

    /* Parse request */
    jmap_get_parse(req, &parser, vacation_props, /*allow_null_ids*/1,
                   NULL, NULL, &get, &err);
    if (err) {
        jmap_error(req, err);
        goto done;
    }

    /* Does the client request specific responses? */
    if (JNOTNULL(get.ids)) {
        json_t *jval;
        size_t i;

        json_array_foreach(get.ids, i, jval) {
            const char *id = json_string_value(jval);

            if (!strcmp(id, "singleton"))
                vacation_get(req->accountid, &get);
            else
                json_array_append(get.not_found, jval);
        }
    }
    else vacation_get(req->accountid, &get);

    /* Build response */
    get.state = vacation_state(req->accountid);
    jmap_ok(req, jmap_get_reply(&get));

done:
    jmap_parser_fini(&parser);
    jmap_get_fini(&get);

    return 0;
}

static void vacation_update(const char *userid, const char *id,
                            json_t *patch, struct jmap_set *set)
{
    /* Parse and validate properties. */
    unsigned status = 0;
    json_t *vacation = vacation_read(userid, &status);
    json_t *prop, *jerr, *invalid = json_pack("[]");
    int r;

    prop = json_object_get(patch, "isEnabled");
    if (!json_is_boolean(prop))
        json_array_append_new(invalid, json_string("isEnabled"));
    else if (json_is_true(prop) &&
             !json_equal(prop, json_object_get(vacation, "isEnabled"))) {
        /* isEnabled changing from false to true */
        status |= STATUS_ENABLE;
    }

    prop = json_object_get(patch, "fromDate");
    if (JNOTNULL(prop) && !json_is_utcdate(prop))
        json_array_append_new(invalid, json_string("fromDate"));

    prop = json_object_get(patch, "toDate");
    if (JNOTNULL(prop) && !json_is_utcdate(prop))
        json_array_append_new(invalid, json_string("toDate"));

    prop = json_object_get(patch, "subject");
    if (JNOTNULL(prop) && !json_is_string(prop))
        json_array_append_new(invalid, json_string("subject"));

    prop = json_object_get(patch, "textBody");
    if (JNOTNULL(prop) && !json_is_string(prop))
        json_array_append_new(invalid, json_string("textBody"));

    prop = json_object_get(patch, "htmlBody");
    if (JNOTNULL(prop) && !json_is_string(prop))
        json_array_append_new(invalid, json_string("htmlBody"));

    /* Report any property errors and bail out. */
    if (json_array_size(invalid)) {
        jerr = json_pack("{s:s, s:o}",
                         "type", "invalidProperties", "properties", invalid);
        json_object_set_new(set->not_updated, id, jerr);
        json_decref(vacation);
        return;
    }
    json_decref(invalid);

    if (status == (STATUS_ENABLE | STATUS_CUSTOM)) {
        /* Custom script with no include -- fail */
        jerr = json_pack("{s:s, s:s}",
                         "type", "forbidden", "description", NO_INCLUDE_ERROR);
        json_object_set_new(set->not_updated, id, jerr);
        json_decref(vacation);
        return;
    }

    /* Update VacationResponse object */

    json_t *new_vacation = jmap_patchobject_apply(vacation, patch, NULL);
    json_decref(vacation);
    vacation = new_vacation;

    /* Dump VacationResponse JMAP object in a comment */
    size_t size = json_dumpb(vacation, NULL, 0, JSON_COMPACT);
    struct buf data = BUF_INITIALIZER;

    buf_setcstr(&data, SCRIPT_HEADER);
    buf_ensure(&data, size);
    json_dumpb(vacation,
               (char *) buf_base(&data) + buf_len(&data), size, JSON_COMPACT);
    buf_truncate(&data, buf_len(&data) + size);
    buf_appendcstr(&data, "\r\n\r\n*/\r\n\r\n");

    /* Create actual sieve rule */
    int isEnabled = json_boolean_value(json_object_get(vacation, "isEnabled"));
    const char *fromDate =
        json_string_value(json_object_get(vacation, "fromDate"));
    const char *toDate =
        json_string_value(json_object_get(vacation, "toDate"));
    const char *subject =
        json_string_value(json_object_get(vacation, "subject"));
    const char *textBody =
        json_string_value(json_object_get(vacation, "textBody"));
    const char *htmlBody =
        json_string_value(json_object_get(vacation, "htmlBody"));

    /* Add required extensions */
    buf_printf(&data, "require [ \"vacation\"%s ];\r\n\r\n",
               (fromDate || toDate) ? ", \"date\", \"relational\"" : "");

    /* Add isEnabled and date tests */
    buf_printf(&data, "if allof (%s", isEnabled ? "true" : "false");
    if (fromDate) {
        buf_printf(&data, ",\r\n%10scurrentdate :zone \"+0000\""
                   " :value \"ge\" \"iso8601\" \"%s\"", "", fromDate);
    }
    if (toDate) {
        buf_printf(&data, ",\r\n%10scurrentdate :zone \"+0000\""
                   " :value \"lt\" \"iso8601\" \"%s\"", "", toDate);
    }
    buf_appendcstr(&data, ")\r\n{\r\n");

    /* Add vacation action */
    buf_appendcstr(&data, "  vacation");
    if (subject) buf_printf(&data, " :subject \"%s\"", subject);
    /* XXX  Need to add :addresses */
    /* XXX  Should we add :fcc ? */

    if (htmlBody) {
        const char *boundary = makeuuid();
        char *text = NULL;

        if (!textBody) textBody = text = charset_extract_plain(htmlBody);

        buf_appendcstr(&data, " :mime text:\r\n");
        buf_printf(&data,
                   "Content-Type: multipart/alternative; boundary=%s\r\n"
                   "\r\n--%s\r\n", boundary, boundary);
        buf_appendcstr(&data,
                       "Content-Type: text/plain; charset=utf-8\r\n\r\n");
        buf_printf(&data, "%s\r\n\r\n--%s\r\n", textBody, boundary);
        buf_appendcstr(&data,
                       "Content-Type: text/html; charset=utf-8\r\n\r\n");
        buf_printf(&data, "%s\r\n\r\n--%s--\r\n", htmlBody, boundary);
        free(text);
    }
    else {
        buf_printf(&data, " text:\r\n%s",
                   textBody ? textBody : DEFAULT_MESSAGE);
    }
    buf_appendcstr(&data, "\r\n.\r\n;\r\n}\r\n");

    /* Store script */
    r = sync_sieve_upload(userid, SCRIPT_NAME SCRIPT_SUFFIX,
                          time(NULL), buf_base(&data), buf_len(&data));
    buf_free(&data);
    json_decref(vacation);

    const char *err = NULL;
    if (r) err = "Failed to update vacation response";
    else if (status == STATUS_ENABLE) {
        /* Activate vacation script */
        r = sync_sieve_activate(userid, SCRIPT_NAME BYTECODE_SUFFIX);
        if (r) err = "Failed to enable vacation response";
    }

    if (r) {
        /* Failure to upload or activate */
        jerr = json_pack("{s:s s:s}", "type", "serverError", "description", err);
        json_object_set_new(set->not_updated, id, jerr);
        r = 0;
    }
    else {
        /* Report vacation as updated. */
        json_object_set_new(set->updated, id, json_null());
    }
}

static int jmap_vacation_set(struct jmap_req *req)
{
    struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
    struct jmap_set set;
    json_t *jerr = NULL;
    int r = 0;

    /* Parse arguments */
    jmap_set_parse(req, &parser, vacation_props, NULL, NULL, &set, &jerr);
    if (jerr) {
        jmap_error(req, jerr);
        goto done;
    }

    set.old_state = vacation_state(req->accountid);

    if (set.if_in_state && strcmp(set.if_in_state, set.old_state)) {
        jmap_error(req, json_pack("{s:s}", "type", "stateMismatch"));
        goto done;
    }


    /* create */
    const char *key;
    json_t *arg;
    json_object_foreach(set.create, key, arg) {
        jerr= json_pack("{s:s}", "type", "singleton");
        json_object_set_new(set.not_created, key, jerr);
    }


    /* update */
    const char *uid;
    json_object_foreach(set.update, uid, arg) {

        /* Validate uid */
        if (!uid) {
            continue;
        }
        if (strcmp(uid, "singleton")) {
            jerr = json_pack("{s:s}", "type", "notFound");
            json_object_set_new(set.not_updated, uid, jerr);
            continue;
        }

        vacation_update(req->accountid, uid, arg, &set);
    }


    /* destroy */
    size_t index;
    json_t *juid;

    json_array_foreach(set.destroy, index, juid) {
        json_t *err= json_pack("{s:s}", "type", "singleton");
        json_object_set_new(set.not_destroyed, json_string_value(juid), err);
    }

    set.new_state = vacation_state(req->accountid);
    jmap_ok(req, jmap_set_reply(&set));

done:
    jmap_parser_fini(&parser);
    jmap_set_fini(&set);
    return r;
}
