1 /* jmap_core.c -- Routines for handling JMAP Core requests
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 #include <errno.h>
47 
48 #include <string.h>
49 #include <syslog.h>
50 
51 #include "acl.h"
52 #include "append.h"
53 #include "http_jmap.h"
54 
55 /* generated headers are not necessarily in current directory */
56 #include "imap/http_err.h"
57 #include "imap/imap_err.h"
58 #include "imap/jmap_err.h"
59 
60 
61 /* JMAP Core API Methods */
62 static int jmap_blob_copy(jmap_req_t *req);
63 static int jmap_core_echo(jmap_req_t *req);
64 
65 /* JMAP extension methods */
66 static int jmap_blob_get(jmap_req_t *req);
67 static int jmap_quota_get(jmap_req_t *req);
68 
69 jmap_method_t jmap_core_methods_standard[] = {
70     {
71         "Blob/copy",
72         JMAP_URN_CORE,
73         &jmap_blob_copy,
74         0/*flags*/
75     },
76     {
77         "Core/echo",
78         JMAP_URN_CORE,
79         &jmap_core_echo,
80         JMAP_SHARED_CSTATE
81     },
82     { NULL, NULL, NULL, 0}
83 };
84 
85 jmap_method_t jmap_core_methods_nonstandard[] = {
86     {
87         "Blob/get",
88         JMAP_BLOB_EXTENSION,
89         &jmap_blob_get,
90         JMAP_SHARED_CSTATE
91     },
92     {
93         "Quota/get",
94         JMAP_QUOTA_EXTENSION,
95         &jmap_quota_get,
96         JMAP_SHARED_CSTATE
97     },
98     { NULL, NULL, NULL, 0}
99 };
100 
jmap_core_init(jmap_settings_t * settings)101 HIDDEN void jmap_core_init(jmap_settings_t *settings)
102 {
103 #define _read_opt(val, optkey) \
104     val = config_getint(optkey); \
105     if (val <= 0) { \
106         syslog(LOG_ERR, "jmap: invalid property value: %s", \
107                 imapopts[optkey].optname); \
108         val = 0; \
109     }
110     _read_opt(settings->limits[MAX_SIZE_UPLOAD],
111               IMAPOPT_JMAP_MAX_SIZE_UPLOAD);
112     settings->limits[MAX_SIZE_UPLOAD] *= 1024;
113     _read_opt(settings->limits[MAX_CONCURRENT_UPLOAD],
114               IMAPOPT_JMAP_MAX_CONCURRENT_UPLOAD);
115     _read_opt(settings->limits[MAX_SIZE_REQUEST],
116               IMAPOPT_JMAP_MAX_SIZE_REQUEST);
117     settings->limits[MAX_SIZE_REQUEST] *= 1024;
118     _read_opt(settings->limits[MAX_CONCURRENT_REQUESTS],
119               IMAPOPT_JMAP_MAX_CONCURRENT_REQUESTS);
120     _read_opt(settings->limits[MAX_CALLS_IN_REQUEST],
121               IMAPOPT_JMAP_MAX_CALLS_IN_REQUEST);
122     _read_opt(settings->limits[MAX_OBJECTS_IN_GET],
123               IMAPOPT_JMAP_MAX_OBJECTS_IN_GET);
124     _read_opt(settings->limits[MAX_OBJECTS_IN_SET],
125               IMAPOPT_JMAP_MAX_OBJECTS_IN_SET);
126 #undef _read_opt
127 
128     json_object_set_new(settings->server_capabilities,
129             JMAP_URN_CORE,
130             json_pack("{s:i s:i s:i s:i s:i s:i s:i s:o}",
131                 "maxSizeUpload",
132                 settings->limits[MAX_SIZE_UPLOAD],
133                 "maxConcurrentUpload",
134                 settings->limits[MAX_CONCURRENT_UPLOAD],
135                 "maxSizeRequest",
136                 settings->limits[MAX_SIZE_REQUEST],
137                 "maxConcurrentRequests",
138                 settings->limits[MAX_CONCURRENT_REQUESTS],
139                 "maxCallsInRequest",
140                 settings->limits[MAX_CALLS_IN_REQUEST],
141                 "maxObjectsInGet",
142                 settings->limits[MAX_OBJECTS_IN_GET],
143                 "maxObjectsInSet",
144                 settings->limits[MAX_OBJECTS_IN_SET],
145                 "collationAlgorithms", json_array()));
146 
147     jmap_method_t *mp;
148     for (mp = jmap_core_methods_standard; mp->name; mp++) {
149         hash_insert(mp->name, mp, &settings->methods);
150     }
151 
152     if (config_getswitch(IMAPOPT_JMAP_NONSTANDARD_EXTENSIONS)) {
153         json_object_set_new(settings->server_capabilities,
154                 JMAP_QUOTA_EXTENSION, json_object());
155         json_object_set_new(settings->server_capabilities,
156                 JMAP_PERFORMANCE_EXTENSION, json_object());
157         json_object_set_new(settings->server_capabilities,
158                 JMAP_DEBUG_EXTENSION, json_object());
159         json_object_set_new(settings->server_capabilities,
160                 JMAP_BLOB_EXTENSION, json_object());
161 
162         for (mp = jmap_core_methods_nonstandard; mp->name; mp++) {
163             hash_insert(mp->name, mp, &settings->methods);
164         }
165     }
166 
167 }
168 
jmap_core_capabilities(json_t * account_capabilities)169 HIDDEN void jmap_core_capabilities(json_t *account_capabilities)
170 {
171     json_object_set_new(account_capabilities,
172             JMAP_URN_CORE, json_object());
173 
174     if (config_getswitch(IMAPOPT_JMAP_NONSTANDARD_EXTENSIONS)) {
175         json_object_set_new(account_capabilities,
176                 JMAP_QUOTA_EXTENSION, json_object());
177 
178         json_object_set_new(account_capabilities,
179                 JMAP_PERFORMANCE_EXTENSION, json_object());
180 
181         json_object_set_new(account_capabilities,
182                 JMAP_DEBUG_EXTENSION, json_object());
183 
184         json_object_set_new(account_capabilities,
185                 JMAP_BLOB_EXTENSION, json_object());
186     }
187 }
188 
189 /*
190  * JMAP Core API Methods
191  */
192 
193 /* Core/echo method */
jmap_core_echo(jmap_req_t * req)194 static int jmap_core_echo(jmap_req_t *req)
195 {
196     json_array_append_new(req->response,
197                           json_pack("[s,O,s]", "Core/echo", req->args, req->tag));
198     return 0;
199 }
200 
jmap_copyblob(jmap_req_t * req,const char * blobid,const char * from_accountid,struct mailbox * to_mbox)201 static int jmap_copyblob(jmap_req_t *req,
202                          const char *blobid,
203                          const char *from_accountid,
204                          struct mailbox *to_mbox)
205 {
206     struct mailbox *mbox = NULL;
207     msgrecord_t *mr = NULL;
208     struct body *body = NULL;
209     const struct body *part = NULL;
210     struct buf msg_buf = BUF_INITIALIZER;
211     FILE *to_fp = NULL;
212     struct stagemsg *stage = NULL;
213 
214     int r = jmap_findblob(req, from_accountid, blobid,
215                           &mbox, &mr, &body, &part, &msg_buf);
216     if (r) return r;
217 
218     /* Create staging file */
219     time_t internaldate = time(NULL);
220     if (!(to_fp = append_newstage(to_mbox->name, internaldate, 0, &stage))) {
221         syslog(LOG_ERR, "jmap_copyblob(%s): append_newstage(%s) failed",
222                 blobid, mbox->name);
223         r = IMAP_INTERNAL;
224         goto done;
225     }
226 
227     /* Copy blob. Keep the original MIME headers, we wouldn't really
228      * know which ones are safe to rewrite for arbitrary blobs. */
229     if (part) {
230         fwrite(buf_base(&msg_buf) + part->header_offset,
231                part->header_size + part->content_size, 1, to_fp);
232     }
233     else {
234         fwrite(buf_base(&msg_buf), buf_len(&msg_buf), 1, to_fp);
235     }
236     if (ferror(to_fp)) {
237         syslog(LOG_ERR, "jmap_copyblob(%s): tofp=%s: %s",
238                blobid, append_stagefname(stage), strerror(errno));
239         r = IMAP_IOERROR;
240         goto done;
241     }
242     fclose(to_fp);
243     to_fp = NULL;
244 
245     /* Append blob to mailbox */
246     struct body *to_body = NULL;
247     struct appendstate as;
248     r = append_setup_mbox(&as, to_mbox, httpd_userid, httpd_authstate,
249             0, /*quota*/NULL, 0, 0, /*event*/0);
250     if (r) {
251         syslog(LOG_ERR, "jmap_copyblob(%s): append_setup_mbox: %s",
252                 blobid, error_message(r));
253         goto done;
254     }
255     strarray_t flags = STRARRAY_INITIALIZER;
256     strarray_append(&flags, "\\Deleted");
257     strarray_append(&flags, "\\Expunged");  // custom flag to insta-expunge!
258 	r = append_fromstage(&as, &to_body, stage, 0, internaldate, &flags, 0, NULL);
259     strarray_fini(&flags);
260 	if (r) {
261         syslog(LOG_ERR, "jmap_copyblob(%s): append_fromstage: %s",
262                 blobid, error_message(r));
263 		append_abort(&as);
264 		goto done;
265 	}
266 	message_free_body(to_body);
267 	free(to_body);
268 	r = append_commit(&as);
269 	if (r) {
270         syslog(LOG_ERR, "jmap_copyblob(%s): append_commit: %s",
271                 blobid, error_message(r));
272         goto done;
273     }
274 
275 done:
276     if (stage) append_removestage(stage);
277     if (to_fp) fclose(to_fp);
278     buf_free(&msg_buf);
279     message_free_body(body);
280     free(body);
281     msgrecord_unref(&mr);
282     jmap_closembox(req, &mbox);
283     return r;
284 }
285 
286 /* Blob/copy method */
jmap_blob_copy(jmap_req_t * req)287 static int jmap_blob_copy(jmap_req_t *req)
288 {
289     struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
290     struct jmap_copy copy;
291     json_t *val, *err = NULL;
292     size_t i = 0;
293     int r = 0;
294     struct mailbox *to_mbox = NULL;
295 
296     /* Parse request */
297     jmap_copy_parse(req, &parser, NULL, NULL, &copy, &err);
298     if (err) {
299         jmap_error(req, err);
300         goto cleanup;
301     }
302 
303     /* Check if we can upload to toAccountId */
304     r = jmap_open_upload_collection(req->accountid, &to_mbox);
305     if (r == IMAP_PERMISSION_DENIED) {
306         json_array_foreach(copy.create, i, val) {
307             json_object_set(copy.not_created, json_string_value(val),
308                     json_pack("{s:s}", "type", "toAccountNotFound"));
309         }
310         goto done;
311     } else if (r) {
312         syslog(LOG_ERR, "jmap_blob_copy: jmap_create_upload_collection(%s): %s",
313                req->accountid, error_message(r));
314         goto cleanup;
315     }
316 
317     /* Copy blobs one by one. XXX should we batch copy here? */
318     json_array_foreach(copy.create, i, val) {
319         const char *blobid = json_string_value(val);
320         r = jmap_copyblob(req, blobid, copy.from_account_id, to_mbox);
321         if (r == IMAP_NOTFOUND || r == IMAP_PERMISSION_DENIED) {
322             json_object_set_new(copy.not_created, blobid,
323                     json_pack("{s:s}", "type", "blobNotFound"));
324         }
325         else if (r) goto cleanup;
326         else json_object_set_new(copy.created, blobid, json_string(blobid));
327     }
328 
329 done:
330     /* Build response */
331     jmap_ok(req, jmap_copy_reply(&copy));
332     r = 0;
333 
334 cleanup:
335     jmap_parser_fini(&parser);
336     jmap_copy_fini(&copy);
337     mailbox_close(&to_mbox);
338     return r;
339 }
340 
341 /* Blob/get method */
342 
343 struct getblob_rec {
344     const char *blob_id;
345     uint32_t uid;
346     char *part;
347 };
348 
349 struct getblob_cb_rock {
350     jmap_req_t *req;
351     const char *blob_id;
352     hash_table *getblobs_by_mboxname;
353 };
354 
getblob_cb(const conv_guidrec_t * rec,void * vrock)355 static int getblob_cb(const conv_guidrec_t* rec, void* vrock)
356 {
357     struct getblob_cb_rock *rock = vrock;
358 
359     struct getblob_rec *getblob = xzmalloc(sizeof(struct getblob_rec));
360     getblob->blob_id = rock->blob_id;
361     getblob->uid = rec->uid;
362     getblob->part = xstrdupnull(rec->part);
363 
364     ptrarray_t *getblobs = hash_lookup(rec->mboxname, rock->getblobs_by_mboxname);
365     if (!getblobs) {
366         getblobs = ptrarray_new();
367         hash_insert(rec->mboxname, getblobs, rock->getblobs_by_mboxname);
368     }
369     ptrarray_append(getblobs, getblob);
370 
371     return 0;
372 }
373 
374 static const jmap_property_t blob_props[] = {
375     {
376         "mailboxIds",
377         NULL,
378         JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE
379     },
380     {
381         "threadIds",
382         NULL,
383         JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE
384     },
385     {
386         "emailIds",
387         NULL,
388         JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE
389     },
390     { NULL, NULL, 0 }
391 };
392 
jmap_blob_get(jmap_req_t * req)393 static int jmap_blob_get(jmap_req_t *req)
394 {
395     struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
396     struct jmap_get get;
397     json_t *err = NULL;
398     json_t *jval;
399     size_t i;
400 
401     /* Parse request */
402     jmap_get_parse(req, &parser, blob_props, /*allow_null_ids*/0,
403                    NULL, NULL, &get, &err);
404     if (err) {
405         jmap_error(req, err);
406         goto done;
407     }
408 
409     /* Sort blob lookups by mailbox */
410     hash_table getblobs_by_mboxname = HASH_TABLE_INITIALIZER;
411     construct_hash_table(&getblobs_by_mboxname, 128, 0);
412     json_array_foreach(get.ids, i, jval) {
413         const char *blob_id = json_string_value(jval);
414         if (*blob_id == 'G') {
415             struct getblob_cb_rock rock = { req, blob_id, &getblobs_by_mboxname };
416             int r = conversations_guid_foreach(req->cstate, blob_id + 1, getblob_cb, &rock);
417             if (r) {
418                 syslog(LOG_ERR, "jmap_blob_get: can't lookup guid %s: %s",
419                         blob_id, error_message(r));
420             }
421         }
422     }
423 
424     /* Lookup blobs by mailbox */
425     json_t *found = json_object();
426     hash_iter *iter = hash_table_iter(&getblobs_by_mboxname);
427     while (hash_iter_next(iter)) {
428         const char *mboxname = hash_iter_key(iter);
429         ptrarray_t *getblobs = hash_iter_val(iter);
430         struct mailbox *mbox = NULL;
431 
432         /* Open mailbox */
433         if (!jmap_hasrights_byname(req, mboxname, ACL_READ|ACL_LOOKUP)) {
434             continue;
435         }
436         int r = jmap_openmbox(req, mboxname, &mbox, 0);
437         if (r) {
438             syslog(LOG_ERR, "jmap_blob_get: can't open mailbox %s: %s",
439                     mboxname, error_message(r));
440             continue;
441         }
442 
443         int j;
444         for (j = 0; j < ptrarray_size(getblobs); j++) {
445             struct getblob_rec *getblob = ptrarray_nth(getblobs, j);
446 
447             /* Read message record */
448             struct message_guid guid;
449             bit64 cid;
450             msgrecord_t *mr = NULL;
451             r = msgrecord_find(mbox, getblob->uid, &mr);
452             if (!r) r = msgrecord_get_guid(mr, &guid);
453             if (!r) r = msgrecord_get_cid(mr, &cid);
454             msgrecord_unref(&mr);
455             if (r) {
456                 syslog(LOG_ERR, "jmap_blob_get: can't read msgrecord %s:%d: %s",
457                         mboxname, getblob->uid, error_message(r));
458                 continue;
459             }
460 
461             /* Report Blob entry */
462             json_t *jblob = json_object_get(found, getblob->blob_id);
463             if (!jblob) {
464                 jblob = json_object();
465                 json_object_set_new(found, getblob->blob_id, jblob);
466             }
467             if (jmap_wantprop(get.props, "mailboxIds")) {
468                 json_t *jmailboxIds = json_object_get(jblob, "mailboxIds");
469                 if (!jmailboxIds) {
470                     jmailboxIds = json_object();
471                     json_object_set_new(jblob, "mailboxIds", jmailboxIds);
472                 }
473                 json_object_set_new(jmailboxIds, mbox->uniqueid, json_true());
474             }
475             if (jmap_wantprop(get.props, "emailIds")) {
476                 json_t *jemailIds = json_object_get(jblob, "emailIds");
477                 if (!jemailIds) {
478                     jemailIds = json_object();
479                     json_object_set_new(jblob, "emailIds", jemailIds);
480                 }
481                 char emailid[JMAP_EMAILID_SIZE];
482                 jmap_set_emailid(&guid, emailid);
483                 json_object_set_new(jemailIds, emailid, json_true());
484             }
485             if (jmap_wantprop(get.props, "threadIds")) {
486                 json_t *jthreadIds = json_object_get(jblob, "threadIds");
487                 if (!jthreadIds) {
488                     jthreadIds = json_object();
489                     json_object_set_new(jblob, "threadIds", jthreadIds);
490                 }
491                 char threadid[JMAP_THREADID_SIZE];
492                 jmap_set_threadid(cid, threadid);
493                 json_object_set_new(jthreadIds, threadid, json_true());
494             }
495         }
496 
497        jmap_closembox(req, &mbox);
498     }
499 
500     /* Clean up memory */
501     hash_iter_reset(iter);
502     while (hash_iter_next(iter)) {
503         ptrarray_t *getblobs = hash_iter_val(iter);
504         struct getblob_rec *getblob;
505         while ((getblob = ptrarray_pop(getblobs))) {
506             free(getblob->part);
507             free(getblob);
508         }
509         ptrarray_free(getblobs);
510     }
511     hash_iter_free(&iter);
512     free_hash_table(&getblobs_by_mboxname, NULL);
513 
514     /* Report found blobs */
515     if (json_object_size(found)) {
516         const char *blob_id;
517         json_t *jblob;
518         json_object_foreach(found, blob_id, jblob) {
519             json_array_append(get.list, jblob);
520         }
521     }
522 
523     /* Report unknown or erroneous blobs */
524     json_array_foreach(get.ids, i, jval) {
525         const char *blob_id = json_string_value(jval);
526         if (!json_object_get(found, blob_id)) {
527             json_array_append_new(get.not_found, json_string(blob_id));
528         }
529     }
530 
531     json_decref(found);
532 
533     /* Reply */
534     jmap_ok(req, jmap_get_reply(&get));
535 
536 done:
537     jmap_parser_fini(&parser);
538     jmap_get_fini(&get);
539     return 0;
540 }
541 
542 /* Quota/get method */
543 static const jmap_property_t quota_props[] = {
544     {
545         "id",
546         NULL,
547         JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE
548     },
549     {
550         "used",
551         NULL,
552         JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE
553     },
554     {
555         "total",
556         NULL,
557         JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE
558     },
559     { NULL, NULL, 0 }
560 };
561 
jmap_quota_get(jmap_req_t * req)562 static int jmap_quota_get(jmap_req_t *req)
563 {
564     struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
565     struct jmap_get get;
566     json_t *err = NULL;
567     char *inboxname = mboxname_user_mbox(req->accountid, NULL);
568 
569     /* Parse request */
570     jmap_get_parse(req, &parser, quota_props, /*allow_null_ids*/1,
571                    NULL, NULL, &get, &err);
572     if (err) {
573         jmap_error(req, err);
574         goto done;
575     }
576 
577     int want_mail_quota = !get.ids || json_is_null(get.ids);
578     size_t i;
579     json_t *jval;
580     json_array_foreach(get.ids, i, jval) {
581         if (strcmp("mail", json_string_value(jval))) {
582             json_array_append(get.not_found, jval);
583         }
584         else want_mail_quota = 1;
585     }
586 
587     if (want_mail_quota) {
588         struct quota quota;
589         quota_init(&quota, inboxname);
590         int r = quota_read(&quota, NULL, 0);
591         if (!r) {
592             quota_t total = quota.limits[QUOTA_STORAGE] * quota_units[QUOTA_STORAGE];
593             quota_t used = quota.useds[QUOTA_STORAGE];
594             json_t *jquota = json_object();
595             json_object_set_new(jquota, "id", json_string("mail"));
596             json_object_set_new(jquota, "used", json_integer(used));
597             json_object_set_new(jquota, "total", json_integer(total));
598             json_array_append_new(get.list, jquota);
599         }
600         else {
601             syslog(LOG_ERR, "jmap_quota_get: can't read quota for %s: %s",
602                     inboxname, error_message(r));
603             json_array_append_new(get.not_found, json_string("mail"));
604         }
605         quota_free(&quota);
606     }
607 
608 
609     modseq_t quotamodseq = mboxname_readquotamodseq(inboxname);
610     struct buf buf = BUF_INITIALIZER;
611     buf_printf(&buf, MODSEQ_FMT, quotamodseq);
612     get.state = buf_release(&buf);
613 
614     jmap_ok(req, jmap_get_reply(&get));
615 
616 done:
617     jmap_parser_fini(&parser);
618     jmap_get_fini(&get);
619     free(inboxname);
620     return 0;
621 }
622