/* 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 "sync_support.h"
#include "user.h"
#include "util.h"
#ifdef USE_SIEVE
#include "sieve/sieve_interface.h"
#include "sieve/bc_parse.h"
#endif
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;
}