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