1 /* jmap_mdn.c -- Routines for handling JMAP MDNs
2 *
3 * Copyright (c) 1994-2020 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 "http_jmap.h"
56 #include "jmap_api.h"
57 #include "jmap_mail.h"
58 #include "json_support.h"
59 #include "parseaddr.h"
60 #include "times.h"
61 #include "util.h"
62
63 /* generated headers are not necessarily in current directory */
64 #include "imap/http_err.h"
65 #include "imap/imap_err.h"
66
67 static int jmap_mdn_send(jmap_req_t *req);
68 static int jmap_mdn_parse(jmap_req_t *req);
69
70 jmap_method_t jmap_mdn_methods_standard[] = {
71 {
72 "MDN/send",
73 JMAP_URN_MDN,
74 &jmap_mdn_send,
75 JMAP_NEED_CSTATE | JMAP_READ_WRITE
76 },
77 {
78 "MDN/parse",
79 JMAP_URN_MDN,
80 &jmap_mdn_parse,
81 JMAP_NEED_CSTATE
82 },
83 { NULL, NULL, NULL, 0}
84 };
85
86 jmap_method_t jmap_mdn_methods_nonstandard[] = {
87 { NULL, NULL, NULL, 0}
88 };
89
jmap_mdn_init(jmap_settings_t * settings)90 HIDDEN void jmap_mdn_init(jmap_settings_t *settings)
91 {
92 jmap_method_t *mp;
93 for (mp = jmap_mdn_methods_standard; mp->name; mp++) {
94 hash_insert(mp->name, mp, &settings->methods);
95 }
96
97 json_object_set_new(settings->server_capabilities,
98 JMAP_URN_MDN, json_object());
99
100 if (config_getswitch(IMAPOPT_JMAP_NONSTANDARD_EXTENSIONS)) {
101 for (mp = jmap_mdn_methods_nonstandard; mp->name; mp++) {
102 hash_insert(mp->name, mp, &settings->methods);
103 }
104 }
105
106 }
107
jmap_mdn_capabilities(json_t * account_capabilities)108 HIDDEN void jmap_mdn_capabilities(json_t *account_capabilities)
109 {
110 json_object_set_new(account_capabilities, JMAP_URN_MDN, json_object());
111 }
112
113 struct mdn_t {
114 const char *emailid;
115 const char *subj;
116 const char *body;
117 const char *mua;
118 int inc_msg;
119 struct {
120 const char *action;
121 const char *sending;
122 const char *type;
123 } dispo;
124
125 /* server-set */
126 strarray_t notify_to;
127 char *gateway;
128 char *orig_msgid;
129 char *orig_rcpt;
130 char *final_rcpt;
131 char *error;
132 };
133
free_mdn(struct mdn_t * mdn)134 static void free_mdn(struct mdn_t *mdn)
135 {
136 strarray_fini(&mdn->notify_to);
137 free(mdn->gateway);
138 free(mdn->orig_msgid);
139 free(mdn->orig_rcpt);
140 free(mdn->final_rcpt);
141 free(mdn->error);
142 }
143
parse_mdn_props(json_t * jmdn,struct mdn_t * mdn)144 static json_t *parse_mdn_props(json_t *jmdn, struct mdn_t *mdn)
145 {
146 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
147 json_t *arg, *err = NULL;
148
149 memset(mdn, 0, sizeof(struct mdn_t));
150
151 mdn->emailid = json_string_value(json_object_get(jmdn, "forEmailId"));
152 if (!mdn->emailid) {
153 jmap_parser_invalid(&parser, "forEmailId");
154 }
155
156 arg = json_object_get(jmdn, "subject");
157 if (json_is_string(arg)) {
158 mdn->subj = json_string_value(arg);
159 }
160 else if (JNOTNULL(arg)) {
161 jmap_parser_invalid(&parser, "subject");
162 }
163
164 arg = json_object_get(jmdn, "textBody");
165 if (json_is_string(arg)) {
166 mdn->body = json_string_value(arg);
167 }
168 else if (JNOTNULL(arg)) {
169 jmap_parser_invalid(&parser, "textBody");
170 }
171
172 arg = json_object_get(jmdn, "includeOriginalMessage");
173 if (json_is_boolean(arg)) {
174 mdn->inc_msg = json_boolean_value(arg);
175 }
176 else if (JNOTNULL(arg)) {
177 jmap_parser_invalid(&parser, "includeOriginalMessage");
178 }
179
180 arg = json_object_get(jmdn, "reportingUA");
181 if (json_is_string(arg)) {
182 mdn->mua = json_string_value(arg);
183 }
184 else if (JNOTNULL(arg)) {
185 jmap_parser_invalid(&parser, "reportingUA");
186 }
187
188 arg = json_object_get(jmdn, "disposition");
189 if (json_is_object(arg)) {
190 const char *key;
191 json_t *val;
192
193 jmap_parser_push(&parser, "disposition");
194 json_object_foreach(arg, key, val) {
195 if (!strcmp(key, "actionMode"))
196 mdn->dispo.action = json_string_value(val);
197 else if (!strcmp(key, "sendingMode"))
198 mdn->dispo.sending = json_string_value(val);
199 else if (!strcmp(key, "type"))
200 mdn->dispo.type = json_string_value(val);
201 else
202 jmap_parser_invalid(&parser, key);
203 }
204
205 const char *s = mdn->dispo.action;
206 if (!s || (strcmp(s, "manual-action") &&
207 strcmp(s, "automatic-action"))) {
208 jmap_parser_invalid(&parser, "actionMode");
209 }
210
211 s = mdn->dispo.sending;
212 if (!s || (strcmp(s, "MDN-sent-manually") &&
213 strcmp(s, "MDN-sent-automatically"))) {
214 jmap_parser_invalid(&parser, "sendingMode");
215 }
216
217 s = mdn->dispo.type;
218 if (!s || (strcmp(s, "deleted") &&
219 strcmp(s, "dispatched") &&
220 strcmp(s, "displayed") &&
221 strcmp(s, "processed"))) {
222 jmap_parser_invalid(&parser, "type");
223 }
224
225 jmap_parser_pop(&parser);
226 }
227 else {
228 jmap_parser_invalid(&parser, "disposition");
229 }
230
231 if (json_array_size(parser.invalid)) {
232 err = json_pack("{s:s}", "type", "invalidProperties");
233 json_object_set(err, "properties", parser.invalid);
234 }
235
236 jmap_parser_fini(&parser);
237
238 return err;
239 }
240
generate_mdn(struct jmap_req * req,struct mdn_t * mdn,struct buf * msgbuf)241 static json_t *generate_mdn(struct jmap_req *req,
242 struct mdn_t *mdn, struct buf *msgbuf)
243 {
244 char datestr[RFC5322_DATETIME_MAX+1];
245 const char *uuid = makeuuid(), *from;
246 char *mboxname = NULL;
247 struct mailbox *mbox = NULL;
248 struct buf buf = BUF_INITIALIZER;
249 msgrecord_t *mr = NULL;
250 message_t *msg;
251 uint32_t uid;
252 json_t *err = NULL;
253 int r = 0;
254
255 buf_reset(msgbuf);
256
257 /* Lookup the message */
258 r = jmap_email_find(req, mdn->emailid, &mboxname, &uid);
259 if (r) {
260 if (r == IMAP_NOTFOUND) {
261 err = json_pack("{s:s}", "type", "emailNotFound");
262 }
263 goto done;
264 }
265
266 /* Check ACL */
267 int rights = jmap_myrights(req, mboxname);
268 if ((rights & JACL_READITEMS) != JACL_READITEMS) {
269 err = json_pack("{s:s}", "type", "emailNotFound");
270 goto done;
271 }
272 if ((rights & JACL_SETKEYWORDS) != JACL_SETKEYWORDS) {
273 err = json_pack("{s:s}", "type", "forbidden");
274 goto done;
275 }
276
277 /* Open the mailbox */
278 r = jmap_openmbox(req, mboxname, &mbox, 1);
279 if (r) goto done;
280
281 /* Load the message */
282 mr = msgrecord_from_uid(mbox, uid);
283 if (!mr) {
284 /* That's a never-should-happen error */
285 syslog(LOG_ERR, "Unexpected null msgrecord at %s:%d",
286 __FILE__, __LINE__);
287 r = IMAP_INTERNAL;
288 goto done;
289 }
290
291 /* Have we already sent an MDN? */
292 int mdnsent;
293 r = msgrecord_hasflag(mr, "$MDNSent", &mdnsent);
294 if (r) {
295 r = IMAP_INTERNAL;
296 goto done;
297 }
298 if (mdnsent) {
299 err = json_pack("{s:s}", "type", "mdnAlreadySent");
300 goto done;
301 }
302
303 /* Get recipients of the MDN */
304 r = msgrecord_get_message(mr, &msg);
305 if (r) {
306 r = IMAP_INTERNAL;
307 goto done;
308 }
309
310 r = message_get_field(msg, "disposition-notification-to", MESSAGE_RAW, &buf);
311 if (r) {
312 err = json_pack("{s:s}", "type", "noRecipients");
313 goto done;
314 }
315
316 struct address *a, *al = NULL;
317 parseaddr_list(buf_cstring(&buf), &al);
318 for (a = al; a; a = a->next) {
319 if (!a->invalid) {
320 strarray_appendm(&mdn->notify_to, address_get_all(a, 1/*canon*/));
321 }
322 }
323 parseaddr_free(al);
324
325 if (!strarray_size(&mdn->notify_to)) {
326 err = json_pack("{s:s}", "type", "noRecipients");
327 goto done;
328 }
329
330
331 /* Build message */
332 time_to_rfc5322(time(NULL), datestr, sizeof(datestr));
333
334 /* XXX Is this the best/only way to determine the Final-Recipient? */
335 buf_setcstr(&buf, "rfc822; ");
336 buf_appendcstr(&buf, req->userid);
337 if (!strchr(req->userid, '@')) buf_printf(&buf, "@%s", config_servername);
338 mdn->final_rcpt = buf_release(&buf);
339 from = mdn->final_rcpt + 8; /* skip "rfc822; " */
340
341 buf_printf(msgbuf, "Date: %s\r\n", datestr);
342 buf_printf(msgbuf, "From: <%s>\r\n", from);
343
344 int i;
345 for (i = 0; i < strarray_size(&mdn->notify_to); i++) {
346 buf_printf(msgbuf, "To: <%s>\r\n", strarray_nth(&mdn->notify_to, i));
347 }
348
349 buf_printf(msgbuf, "Message-ID: <%s@%s>\r\n", uuid, config_servername);
350
351 if (mdn->subj) {
352 char *subj = charset_encode_mimeheader(mdn->subj, 0, 0);
353 buf_printf(msgbuf, "Subject: %s\r\n", subj);
354 free(subj);
355 }
356 else {
357 buf_printf(msgbuf, "Subject: Return Receipt (%s)", mdn->dispo.type);
358 r = message_get_subject(msg, &buf);
359 if (!r && buf_len(&buf)) {
360 buf_printf(msgbuf, " for\r\n\t%s", buf_cstring(&buf));
361 }
362 buf_appendcstr(msgbuf, "\r\n");
363 }
364
365 buf_printf(msgbuf, "Content-Type: "
366 "multipart/report; report-type=disposition-notification;"
367 "\r\n\tboundary=\"%s\"\r\n", uuid);
368 buf_appendcstr(msgbuf, "MIME-Version: 1.0\r\n"
369 "\r\nThis is a MIME-encapsulated message\r\n\r\n");
370
371 /* This is the human readable status report */
372 buf_printf(msgbuf, "--%s\r\n", uuid);
373 buf_appendcstr(msgbuf, "Content-Type: text/plain; charset=utf-8\r\n");
374 buf_appendcstr(msgbuf, "Content-Disposition: inline\r\n");
375 buf_appendcstr(msgbuf, "Content-Transfer-Encoding: 8bit\r\n\r\n");
376
377 if (mdn->body) buf_appendcstr(msgbuf, mdn->body);
378 else {
379 buf_printf(msgbuf,
380 "This is a Return Receipt for the mail that you sent to %s.",
381 from);
382 }
383 buf_appendcstr(msgbuf, "\r\n\r\n");
384
385 /* This is the MDN status report */
386 buf_printf(msgbuf, "--%s\r\n", uuid);
387 buf_appendcstr(msgbuf,
388 "Content-Type: message/disposition-notification\r\n\r\n");
389 if (mdn->mua) buf_printf(msgbuf, "Reporting-UA: %s\r\n", mdn->mua);
390
391 r = message_get_field(msg, "original-recipient", MESSAGE_RAW, &buf);
392 if (!r && buf_len(&buf)) {
393 mdn->orig_rcpt = xstrdup(buf_cstring(&buf));
394 buf_printf(msgbuf, "Original-Recipient: rfc822; %s\r\n", mdn->orig_rcpt);
395 }
396 buf_printf(msgbuf, "Final-Recipient: %s\r\n", mdn->final_rcpt);
397
398 r = message_get_messageid(msg, &buf);
399 if (!r && buf_len(&buf)) {
400 mdn->orig_msgid = xstrdup(buf_cstring(&buf));
401 buf_printf(msgbuf, "Original-Message-ID: %s\r\n", mdn->orig_msgid);
402 }
403 buf_printf(msgbuf, "Disposition: %s/%s; %s\r\n",
404 mdn->dispo.action, mdn->dispo.sending, mdn->dispo.type);
405 buf_appendcstr(msgbuf, "\r\n");
406
407 if (mdn->inc_msg) {
408 r = message_get_headers(msg, &buf);
409 if (!r) {
410 /* This is the original message */
411 buf_printf(msgbuf, "--%s\r\n", uuid);
412 buf_appendcstr(msgbuf, "Content-Type: text/rfc822-headers\r\n");
413 buf_appendcstr(msgbuf, "Content-Disposition: inline\r\n\r\n");
414 buf_appendcstr(msgbuf, buf_cstring(&buf));
415 }
416 }
417
418 buf_printf(msgbuf, "--%s--\r\n", uuid);
419
420 done:
421 if (r && err == NULL) err = jmap_server_error(r);
422 if (mr) msgrecord_unref(&mr);
423 if (mbox) jmap_closembox(req, &mbox);
424 free(mboxname);
425 buf_free(&buf);
426
427 return err;
428 }
429
send_mdn(struct jmap_req * req,struct mdn_t * mdn,struct buf * msgbuf,smtpclient_t ** sm)430 static json_t *send_mdn(struct jmap_req *req, struct mdn_t *mdn,
431 struct buf *msgbuf, smtpclient_t **sm)
432 {
433 json_t *err = NULL;
434 int r;
435
436 if (!*sm) {
437 /* Open the SMTP connection */
438 r = smtpclient_open(sm);
439 if (r) goto done;
440 }
441
442 smtpclient_set_auth(*sm, req->userid);
443
444 /* Prepare envelope */
445 smtp_envelope_t smtpenv = SMTP_ENVELOPE_INITIALIZER;
446 smtp_envelope_set_from(&smtpenv, "<>");
447
448 int i;
449 for (i = 0; i < strarray_size(&mdn->notify_to); i++) {
450 smtp_envelope_add_rcpt(&smtpenv, strarray_nth(&mdn->notify_to, i));
451 }
452
453 r = smtpclient_send(*sm, &smtpenv, msgbuf);
454 if (r) {
455 const char *desc = smtpclient_get_resp_text(*sm);
456
457 syslog(LOG_ERR, "MDN/send failed: %s", desc ? desc : error_message(r));
458
459 if (desc) {
460 err = json_pack("{s:s, s:s}", "type", "serverFail",
461 "description", desc);
462 }
463 }
464
465 smtp_envelope_fini(&smtpenv);
466
467 done:
468 if (r && err == NULL) err = jmap_server_error(r);
469
470 return err;
471 }
472
jmap_mdn_send(struct jmap_req * req)473 static int jmap_mdn_send(struct jmap_req *req)
474 {
475 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
476 const char *key, *id;
477 json_t *arg, *val, *send = NULL, *err = NULL;
478 int r = 0;
479
480 /* Parse request */
481 json_object_foreach(req->args, key, arg) {
482 if (!strcmp(key, "accountId")) {
483 /* already handled in jmap_api() */
484 }
485
486 else if (!strcmp(key, "send")) {
487 if (json_is_object(arg)) {
488 send = arg;
489
490 jmap_parser_push(&parser, "send");
491 json_object_foreach(send, id, val) {
492 if (!json_is_object(val)) {
493 jmap_parser_invalid(&parser, id);
494 }
495 }
496 jmap_parser_pop(&parser);
497 }
498 else {
499 jmap_parser_invalid(&parser, "send");
500 }
501 }
502
503 else {
504 jmap_parser_invalid(&parser, key);
505 }
506 }
507
508 /* send is a required argument */
509 if (!send || !json_object_size(send)) jmap_parser_invalid(&parser, "send");
510
511 if (json_array_size(parser.invalid)) {
512 err = json_pack("{s:s s:O}", "type", "invalidArguments",
513 "arguments", parser.invalid);
514 jmap_error(req, err);
515 goto done;
516 }
517
518
519 /* Process request */
520 json_t *sent = NULL, *not_sent = NULL, *update = NULL;
521 smtpclient_t *sm = NULL;
522 struct buf msgbuf = BUF_INITIALIZER;
523
524 json_object_foreach(send, id, val) {
525 /* Parse MDN props */
526 struct mdn_t mdn;
527
528 err = parse_mdn_props(val, &mdn);
529 if (!err) {
530 /* Generate MDN */
531 err = generate_mdn(req, &mdn, &msgbuf);
532 }
533
534 if (!err) {
535 /* Send MDN */
536 err = send_mdn(req, &mdn, &msgbuf, &sm);
537
538 if (!err) {
539 /* XXX With Jansson 2.11 we can use json_pack() and s*
540 which will skip NULL values */
541 json_t *jmdn = json_object();
542
543 if (mdn.gateway) {
544 json_object_set_new(jmdn, "mdnGateway",
545 json_string(mdn.gateway));
546 }
547 if (mdn.orig_rcpt) {
548 json_object_set_new(jmdn, "originalRecipient",
549 json_string(mdn.orig_rcpt));
550 }
551 if (mdn.final_rcpt) {
552 json_object_set_new(jmdn, "finalRecipient",
553 json_string(mdn.final_rcpt));
554 }
555 if (mdn.orig_msgid) {
556 json_object_set_new(jmdn, "originalMessageId",
557 json_string(mdn.orig_msgid));
558 }
559 if (mdn.error) {
560 json_object_set_new(jmdn, "error", json_string(mdn.error));
561 }
562
563 /* Add this id to the sent list */
564 if (!sent) sent = json_object();
565 json_object_set_new(sent, id, jmdn);
566
567 /* Add this emailid to the list to be updated */
568 if (!update) update = json_object();
569 json_object_set_new(update, mdn.emailid,
570 json_pack("{s:b}", "keywords/$MDNSent", 1));
571 }
572 }
573
574 if (err) {
575 /* Add this id to the not_sent list */
576 if (!not_sent) not_sent = json_object();
577 json_object_set_new(not_sent, id, err);
578 }
579
580 free_mdn(&mdn);
581 }
582
583 if (sm) smtpclient_close(&sm);
584 buf_free(&msgbuf);
585
586
587 /* Reply */
588 jmap_ok(req, json_pack("{s:s s:o s:o}",
589 "accountId", req->accountid,
590 "sent", sent ? sent : json_null(),
591 "notSent", not_sent ? not_sent : json_null()));
592
593 /* Implicitly set the $MDNSent keyword for successful MDNs */
594 if (update) {
595 jmap_add_subreq(req, "Email/set",
596 json_pack("{s:o}", "update", update), NULL);
597 }
598
599 done:
600 jmap_parser_fini(&parser);
601 return r;
602 }
603
jmap_mdn_parse(jmap_req_t * req)604 static int jmap_mdn_parse(jmap_req_t *req)
605 {
606 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
607 struct jmap_parse parse;
608 json_t *err = NULL;
609
610 /* Parse request */
611 jmap_parse_parse(req, &parser, NULL, NULL, &parse, &err);
612 if (err) {
613 jmap_error(req, err);
614 goto done;
615 }
616
617 /* Process request */
618 json_t *jval;
619 size_t i;
620 json_array_foreach(parse.blob_ids, i, jval) {
621 const char *blobid = json_string_value(jval);
622 struct mailbox *mbox = NULL;
623 msgrecord_t *mr = NULL;
624 struct body *body = NULL;
625 const struct body *part = NULL;
626 struct buf buf = BUF_INITIALIZER;
627
628 int r = jmap_findblob(req, NULL/*accountid*/, blobid,
629 &mbox, &mr, &body, &part, &buf);
630 if (r) {
631 json_array_append_new(parse.not_found, json_string(blobid));
632 continue;
633 }
634
635 /* parse blob */
636 json_t *mdn = NULL;
637
638 // XXX -> convert `buf` into an mdn
639
640 if (mdn) {
641 json_object_set_new(parse.parsed, blobid, mdn);
642 }
643 else {
644 json_array_append_new(parse.not_parsable, json_string(blobid));
645 }
646 msgrecord_unref(&mr);
647 jmap_closembox(req, &mbox);
648 message_free_body(body);
649 free(body);
650 buf_free(&buf);
651 }
652
653 /* Build response */
654 jmap_ok(req, jmap_parse_reply(&parse));
655
656 done:
657 jmap_parser_fini(&parser);
658 jmap_parse_fini(&parse);
659 return 0;
660 }
661