1 /* jmap_vacation.c -- Routines for handling JMAP vacation responses
2 *
3 * Copyright (c) 1994-2019 Carnegie Mellon University. All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in
14 * the documentation and/or other materials provided with the
15 * distribution.
16 *
17 * 3. The name "Carnegie Mellon University" must not be used to
18 * endorse or promote products derived from this software without
19 * prior written permission. For permission or any legal
20 * details, please contact
21 * Carnegie Mellon University
22 * Center for Technology Transfer and Enterprise Creation
23 * 4615 Forbes Avenue
24 * Suite 302
25 * Pittsburgh, PA 15213
26 * (412) 268-7393, fax: (412) 268-7395
27 * innovation@andrew.cmu.edu
28 *
29 * 4. Redistributions of any form whatsoever must retain the following
30 * acknowledgment:
31 * "This product includes software developed by Computing Services
32 * at Carnegie Mellon University (http://www.cmu.edu/computing/)."
33 *
34 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
35 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
36 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
37 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
38 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
39 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
40 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
41 *
42 */
43
44 #include <config.h>
45
46 #ifdef HAVE_UNISTD_H
47 #include <unistd.h>
48 #endif
49 #include <ctype.h>
50 #include <string.h>
51 #include <syslog.h>
52 #include <assert.h>
53 #include <errno.h>
54
55 #include "hash.h"
56 #include "http_jmap.h"
57 #include "json_support.h"
58 #include "map.h"
59 #include "sieve/sieve_interface.h"
60 #include "sieve/bc_parse.h"
61 #include "sync_support.h"
62 #include "user.h"
63 #include "util.h"
64
65 static int jmap_vacation_get(jmap_req_t *req);
66 static int jmap_vacation_set(jmap_req_t *req);
67
68 jmap_method_t jmap_vacation_methods_standard[] = {
69 {
70 "VacationResponse/get",
71 JMAP_URN_VACATION,
72 &jmap_vacation_get,
73 JMAP_SHARED_CSTATE
74 },
75 {
76 "VacationResponse/set",
77 JMAP_URN_VACATION,
78 &jmap_vacation_set,
79 JMAP_SHARED_CSTATE
80 },
81 { NULL, NULL, NULL, 0}
82 };
83
84 jmap_method_t jmap_vacation_methods_nonstandard[] = {
85 { NULL, NULL, NULL, 0}
86 };
87
88 static int sieve_vacation_enabled = 0;
89
jmap_vacation_init(jmap_settings_t * settings)90 HIDDEN void jmap_vacation_init(jmap_settings_t *settings)
91 {
92 if (!config_getswitch(IMAPOPT_JMAP_VACATION)) return;
93
94 #ifdef USE_SIEVE
95 unsigned long config_ext = config_getbitfield(IMAPOPT_SIEVE_EXTENSIONS);
96 unsigned long required =
97 IMAP_ENUM_SIEVE_EXTENSIONS_VACATION |
98 IMAP_ENUM_SIEVE_EXTENSIONS_RELATIONAL |
99 IMAP_ENUM_SIEVE_EXTENSIONS_DATE;
100
101 sieve_vacation_enabled = ((config_ext & required) == required);
102 #endif /* USE_SIEVE */
103
104 if (!sieve_vacation_enabled) return;
105
106 jmap_method_t *mp;
107 for (mp = jmap_vacation_methods_standard; mp->name; mp++) {
108 hash_insert(mp->name, mp, &settings->methods);
109 }
110
111 json_object_set_new(settings->server_capabilities,
112 JMAP_URN_VACATION, json_object());
113
114 if (config_getswitch(IMAPOPT_JMAP_NONSTANDARD_EXTENSIONS)) {
115 for (mp = jmap_vacation_methods_nonstandard; mp->name; mp++) {
116 hash_insert(mp->name, mp, &settings->methods);
117 }
118 }
119
120 }
121
jmap_vacation_capabilities(json_t * account_capabilities)122 HIDDEN void jmap_vacation_capabilities(json_t *account_capabilities)
123 {
124 if (!sieve_vacation_enabled) return;
125
126 json_object_set_new(account_capabilities, JMAP_URN_VACATION, json_object());
127 }
128
129 /* VacationResponse/get method */
130 static const jmap_property_t vacation_props[] = {
131 {
132 "id",
133 NULL,
134 JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE | JMAP_PROP_ALWAYS_GET
135 },
136 {
137 "isEnabled",
138 NULL,
139 0
140 },
141 {
142 "fromDate",
143 NULL,
144 0
145 },
146 {
147 "toDate",
148 NULL,
149 0
150 },
151 {
152 "subject",
153 NULL,
154 0
155 },
156 {
157 "textBody",
158 NULL,
159 0
160 },
161 {
162 "htmlBody",
163 NULL,
164 0
165 },
166
167 { NULL, NULL, 0 }
168 };
169
170 #define SCRIPT_NAME "jmap_vacation"
171 #define SCRIPT_SUFFIX ".script"
172 #define BYTECODE_SUFFIX ".bc"
173 #define DEFAULTBC_NAME "defaultbc"
174
175 #define STATUS_ACTIVE (1<<0)
176 #define STATUS_CUSTOM (1<<1)
177 #define STATUS_ENABLE (1<<2)
178
179 #define SCRIPT_HEADER "/* Generated by Cyrus JMAP - DO NOT EDIT\r\n\r\n"
180
181 #define DEFAULT_MESSAGE "I'm away at the moment." \
182 " I'll read your message and get back to you as soon as I can."
183
184 #define NO_INCLUDE_ERROR "Can not enable the vacation response" \
185 " because the active Sieve script does not" \
186 " properly include the 'jmap_vacation' script."
187
vacation_state(const char * userid)188 static char *vacation_state(const char *userid)
189 {
190 const char *sieve_dir = user_sieve_path(userid);
191 char *bcname = strconcat(sieve_dir, "/" SCRIPT_NAME BYTECODE_SUFFIX, NULL);
192 struct buf buf = BUF_INITIALIZER;
193 struct stat sbuf;
194 time_t state = 0;
195
196 if (!stat(bcname, &sbuf)) state = sbuf.st_mtime;
197 free(bcname);
198
199 buf_printf(&buf, "%ld", state);
200
201 return buf_release(&buf);
202 }
203
vacation_read(const char * userid,unsigned * status)204 static json_t *vacation_read(const char *userid, unsigned *status)
205 {
206 const char *sieve_dir = user_sieve_path(userid);
207 char *scriptname = strconcat(sieve_dir, "/" SCRIPT_NAME SCRIPT_SUFFIX, NULL);
208 json_t *vacation = NULL;
209 int fd;
210
211 /* Parse JMAP from vacation script */
212 if ((fd = open(scriptname, O_RDONLY)) != -1) {
213 const char *base = NULL, *json;
214 size_t len = 0;
215
216 map_refresh(fd, 1, &base, &len, MAP_UNKNOWN_LEN, scriptname, NULL);
217 json = strstr(base, SCRIPT_HEADER);
218 if (json) {
219 json_error_t jerr;
220
221 json += strlen(SCRIPT_HEADER);
222 vacation = json_loadb(json, len - (json - base),
223 JSON_DISABLE_EOF_CHECK, &jerr);
224 }
225 map_free(&base, &len);
226 close(fd);
227 }
228
229 free(scriptname);
230
231 if (vacation) {
232 int isEnabled =
233 json_boolean_value(json_object_get(vacation, "isEnabled"));
234 int isActive = 0;
235
236 #ifdef USE_SIEVE
237 /* Check if vacation script is really active */
238 char *defaultbc = strconcat(sieve_dir, "/" DEFAULTBC_NAME, NULL);
239 char *activebc = sieve_getdefaultbcfname(defaultbc);
240
241 if (activebc) {
242 const char *filename = activebc + strlen(sieve_dir) + 1;
243
244 if (!strcmp(filename, SCRIPT_NAME BYTECODE_SUFFIX)) {
245 /* Vacation script itself is active */
246 isActive = 1;
247 }
248 else if ((fd = open(activebc, O_RDONLY)) != -1) {
249 /* Parse active bytecode to see if vacation script is included */
250 bytecode_input_t *bc = NULL;
251 const char *base = NULL;
252 size_t len = 0;
253 int i, version, requires;
254
255 if (status) *status |= STATUS_CUSTOM;
256
257 map_refresh(fd, 1, &base, &len, MAP_UNKNOWN_LEN, activebc, NULL);
258 bc = (bytecode_input_t *) base;
259
260 i = bc_header_parse(bc, &version, &requires);
261 while (i > 0 && i < (int) len) {
262 commandlist_t cmd;
263
264 i = bc_action_parse(bc, i, version, &cmd);
265 if (cmd.type == B_INCLUDE &&
266 cmd.u.inc.location == B_PERSONAL &&
267 !strcmp(cmd.u.inc.script, SCRIPT_NAME)) {
268 /* Found it! */
269 isActive = 1;
270 break;
271 }
272 else if (cmd.type == B_IF) {
273 /* Skip over test */
274 i = cmd.u.i.testend;
275 }
276 }
277
278 map_free(&base, &len);
279 close(fd);
280 }
281 }
282
283 free(activebc);
284 free(defaultbc);
285 #endif /* USE_SIEVE */
286
287 isEnabled = isActive && isEnabled;
288 json_object_set_new(vacation, "isEnabled", json_boolean(isEnabled));
289
290 if (status && isActive) *status |= STATUS_ACTIVE;
291 }
292 else {
293 /* Build empty response */
294 vacation = json_pack("{ s:s s:b s:n s:n s:n s:s s:n }",
295 "id", "singleton", "isEnabled", 0,
296 "fromDate", "toDate", "subject",
297 "textBody", DEFAULT_MESSAGE, "htmlBody");
298 }
299
300 return vacation;
301 }
302
vacation_get(const char * userid,struct jmap_get * get)303 static void vacation_get(const char *userid, struct jmap_get *get)
304 {
305 /* Read script */
306 json_t *vacation = vacation_read(userid, NULL);
307
308 /* Strip unwanted properties */
309 if (!jmap_wantprop(get->props, "isEnabled"))
310 json_object_del(vacation, "isEnabled");
311 if (!jmap_wantprop(get->props, "fromDate"))
312 json_object_del(vacation, "fromDate");
313 if (!jmap_wantprop(get->props, "toDate"))
314 json_object_del(vacation, "toDate");
315 if (!jmap_wantprop(get->props, "subject"))
316 json_object_del(vacation, "subject");
317 if (!jmap_wantprop(get->props, "textBody"))
318 json_object_del(vacation, "textBody");
319 if (!jmap_wantprop(get->props, "htmlBody"))
320 json_object_del(vacation, "htmlBody");
321
322 /* Add object to list */
323 json_array_append_new(get->list, vacation);
324 }
325
jmap_vacation_get(jmap_req_t * req)326 static int jmap_vacation_get(jmap_req_t *req)
327 {
328 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
329 struct jmap_get get;
330 json_t *err = NULL;
331
332 /* Parse request */
333 jmap_get_parse(req, &parser, vacation_props, /*allow_null_ids*/1,
334 NULL, NULL, &get, &err);
335 if (err) {
336 jmap_error(req, err);
337 goto done;
338 }
339
340 /* Does the client request specific responses? */
341 if (JNOTNULL(get.ids)) {
342 json_t *jval;
343 size_t i;
344
345 json_array_foreach(get.ids, i, jval) {
346 const char *id = json_string_value(jval);
347
348 if (!strcmp(id, "singleton"))
349 vacation_get(req->accountid, &get);
350 else
351 json_array_append(get.not_found, jval);
352 }
353 }
354 else vacation_get(req->accountid, &get);
355
356 /* Build response */
357 get.state = vacation_state(req->accountid);
358 jmap_ok(req, jmap_get_reply(&get));
359
360 done:
361 jmap_parser_fini(&parser);
362 jmap_get_fini(&get);
363
364 return 0;
365 }
366
vacation_update(const char * userid,const char * id,json_t * patch,struct jmap_set * set)367 static void vacation_update(const char *userid, const char *id,
368 json_t *patch, struct jmap_set *set)
369 {
370 /* Parse and validate properties. */
371 unsigned status = 0;
372 json_t *vacation = vacation_read(userid, &status);
373 json_t *prop, *jerr, *invalid = json_pack("[]");
374 int r;
375
376 prop = json_object_get(patch, "isEnabled");
377 if (!json_is_boolean(prop))
378 json_array_append_new(invalid, json_string("isEnabled"));
379 else if (json_is_true(prop) &&
380 !json_equal(prop, json_object_get(vacation, "isEnabled"))) {
381 /* isEnabled changing from false to true */
382 status |= STATUS_ENABLE;
383 }
384
385 prop = json_object_get(patch, "fromDate");
386 if (JNOTNULL(prop) && !json_is_utcdate(prop))
387 json_array_append_new(invalid, json_string("fromDate"));
388
389 prop = json_object_get(patch, "toDate");
390 if (JNOTNULL(prop) && !json_is_utcdate(prop))
391 json_array_append_new(invalid, json_string("toDate"));
392
393 prop = json_object_get(patch, "subject");
394 if (JNOTNULL(prop) && !json_is_string(prop))
395 json_array_append_new(invalid, json_string("subject"));
396
397 prop = json_object_get(patch, "textBody");
398 if (JNOTNULL(prop) && !json_is_string(prop))
399 json_array_append_new(invalid, json_string("textBody"));
400
401 prop = json_object_get(patch, "htmlBody");
402 if (JNOTNULL(prop) && !json_is_string(prop))
403 json_array_append_new(invalid, json_string("htmlBody"));
404
405 /* Report any property errors and bail out. */
406 if (json_array_size(invalid)) {
407 jerr = json_pack("{s:s, s:o}",
408 "type", "invalidProperties", "properties", invalid);
409 json_object_set_new(set->not_updated, id, jerr);
410 json_decref(vacation);
411 return;
412 }
413 json_decref(invalid);
414
415 if (status == (STATUS_ENABLE | STATUS_CUSTOM)) {
416 /* Custom script with no include -- fail */
417 jerr = json_pack("{s:s, s:s}",
418 "type", "forbidden", "description", NO_INCLUDE_ERROR);
419 json_object_set_new(set->not_updated, id, jerr);
420 json_decref(vacation);
421 return;
422 }
423
424 /* Update VacationResponse object */
425
426 json_t *new_vacation = jmap_patchobject_apply(vacation, patch, NULL);
427 json_decref(vacation);
428 vacation = new_vacation;
429
430 /* Dump VacationResponse JMAP object in a comment */
431 size_t size = json_dumpb(vacation, NULL, 0, JSON_COMPACT);
432 struct buf data = BUF_INITIALIZER;
433
434 buf_setcstr(&data, SCRIPT_HEADER);
435 buf_ensure(&data, size);
436 json_dumpb(vacation,
437 (char *) buf_base(&data) + buf_len(&data), size, JSON_COMPACT);
438 buf_truncate(&data, buf_len(&data) + size);
439 buf_appendcstr(&data, "\r\n\r\n*/\r\n\r\n");
440
441 /* Create actual sieve rule */
442 int isEnabled = json_boolean_value(json_object_get(vacation, "isEnabled"));
443 const char *fromDate =
444 json_string_value(json_object_get(vacation, "fromDate"));
445 const char *toDate =
446 json_string_value(json_object_get(vacation, "toDate"));
447 const char *subject =
448 json_string_value(json_object_get(vacation, "subject"));
449 const char *textBody =
450 json_string_value(json_object_get(vacation, "textBody"));
451 const char *htmlBody =
452 json_string_value(json_object_get(vacation, "htmlBody"));
453
454 /* Add required extensions */
455 buf_printf(&data, "require [ \"vacation\"%s ];\r\n\r\n",
456 (fromDate || toDate) ? ", \"date\", \"relational\"" : "");
457
458 /* Add isEnabled and date tests */
459 buf_printf(&data, "if allof (%s", isEnabled ? "true" : "false");
460 if (fromDate) {
461 buf_printf(&data, ",\r\n%10scurrentdate :zone \"+0000\""
462 " :value \"ge\" \"iso8601\" \"%s\"", "", fromDate);
463 }
464 if (toDate) {
465 buf_printf(&data, ",\r\n%10scurrentdate :zone \"+0000\""
466 " :value \"lt\" \"iso8601\" \"%s\"", "", toDate);
467 }
468 buf_appendcstr(&data, ")\r\n{\r\n");
469
470 /* Add vacation action */
471 buf_appendcstr(&data, " vacation");
472 if (subject) buf_printf(&data, " :subject \"%s\"", subject);
473 /* XXX Need to add :addresses */
474 /* XXX Should we add :fcc ? */
475
476 if (htmlBody) {
477 const char *boundary = makeuuid();
478 char *text = NULL;
479
480 if (!textBody) textBody = text = charset_extract_plain(htmlBody);
481
482 buf_appendcstr(&data, " :mime text:\r\n");
483 buf_printf(&data,
484 "Content-Type: multipart/alternative; boundary=%s\r\n"
485 "\r\n--%s\r\n", boundary, boundary);
486 buf_appendcstr(&data,
487 "Content-Type: text/plain; charset=utf-8\r\n\r\n");
488 buf_printf(&data, "%s\r\n\r\n--%s\r\n", textBody, boundary);
489 buf_appendcstr(&data,
490 "Content-Type: text/html; charset=utf-8\r\n\r\n");
491 buf_printf(&data, "%s\r\n\r\n--%s--\r\n", htmlBody, boundary);
492 free(text);
493 }
494 else {
495 buf_printf(&data, " text:\r\n%s",
496 textBody ? textBody : DEFAULT_MESSAGE);
497 }
498 buf_appendcstr(&data, "\r\n.\r\n;\r\n}\r\n");
499
500 /* Store script */
501 r = sync_sieve_upload(userid, SCRIPT_NAME SCRIPT_SUFFIX,
502 time(NULL), buf_base(&data), buf_len(&data));
503 buf_free(&data);
504 json_decref(vacation);
505
506 const char *err = NULL;
507 if (r) err = "Failed to update vacation response";
508 else if (status == STATUS_ENABLE) {
509 /* Activate vacation script */
510 r = sync_sieve_activate(userid, SCRIPT_NAME BYTECODE_SUFFIX);
511 if (r) err = "Failed to enable vacation response";
512 }
513
514 if (r) {
515 /* Failure to upload or activate */
516 jerr = json_pack("{s:s s:s}", "type", "serverError", "description", err);
517 json_object_set_new(set->not_updated, id, jerr);
518 r = 0;
519 }
520 else {
521 /* Report vacation as updated. */
522 json_object_set_new(set->updated, id, json_null());
523 }
524 }
525
jmap_vacation_set(struct jmap_req * req)526 static int jmap_vacation_set(struct jmap_req *req)
527 {
528 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
529 struct jmap_set set;
530 json_t *jerr = NULL;
531 int r = 0;
532
533 /* Parse arguments */
534 jmap_set_parse(req, &parser, vacation_props, NULL, NULL, &set, &jerr);
535 if (jerr) {
536 jmap_error(req, jerr);
537 goto done;
538 }
539
540 set.old_state = vacation_state(req->accountid);
541
542 if (set.if_in_state && strcmp(set.if_in_state, set.old_state)) {
543 jmap_error(req, json_pack("{s:s}", "type", "stateMismatch"));
544 goto done;
545 }
546
547
548 /* create */
549 const char *key;
550 json_t *arg;
551 json_object_foreach(set.create, key, arg) {
552 jerr= json_pack("{s:s}", "type", "singleton");
553 json_object_set_new(set.not_created, key, jerr);
554 }
555
556
557 /* update */
558 const char *uid;
559 json_object_foreach(set.update, uid, arg) {
560
561 /* Validate uid */
562 if (!uid) {
563 continue;
564 }
565 if (strcmp(uid, "singleton")) {
566 jerr = json_pack("{s:s}", "type", "notFound");
567 json_object_set_new(set.not_updated, uid, jerr);
568 continue;
569 }
570
571 vacation_update(req->accountid, uid, arg, &set);
572 }
573
574
575 /* destroy */
576 size_t index;
577 json_t *juid;
578
579 json_array_foreach(set.destroy, index, juid) {
580 json_t *err= json_pack("{s:s}", "type", "singleton");
581 json_object_set_new(set.not_destroyed, json_string_value(juid), err);
582 }
583
584 set.new_state = vacation_state(req->accountid);
585 jmap_ok(req, jmap_set_reply(&set));
586
587 done:
588 jmap_parser_fini(&parser);
589 jmap_set_fini(&set);
590 return r;
591 }
592