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