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, ©, &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(©));
332 r = 0;
333
334 cleanup:
335 jmap_parser_fini(&parser);
336 jmap_copy_fini(©);
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("a, inboxname);
590 int r = quota_read("a, 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("a);
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