1 /* http_caldav_sched.c -- Routines for dealing with CALDAV scheduling in httpd
2  *
3  * Copyright (c) 1994-2015 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 
45 #include <config.h>
46 
47 #include <syslog.h>
48 
49 #include <jansson.h>
50 #include <libical/ical.h>
51 #include <sys/types.h>
52 #include <sys/wait.h>
53 
54 #include <libxml/HTMLparser.h>
55 #include <libxml/tree.h>
56 
57 #include "httpd.h"
58 #include "http_caldav_sched.h"
59 #include "http_dav.h"
60 #include "http_proxy.h"
61 #include "jmap_ical.h"
62 #include "jmap_util.h"
63 #include "notify.h"
64 #include "crc32.h"
65 #include "smtpclient.h"
66 #include "strhash.h"
67 #include "times.h"
68 #include "xmalloc.h"
69 #include "xstrlcat.h"
70 #include "xstrlcpy.h"
71 
72 /* generated headers are not necessarily in current directory */
73 #include "imap/http_err.h"
74 #include "imap/imap_err.h"
75 
76 static const char *get_organizer(icalcomponent *comp);
77 static int partstat_changed(icalcomponent *oldcomp,
78                             icalcomponent *newcomp, const char *attendee);
79 
caladdress_lookup(const char * addr,struct caldav_sched_param * param,const strarray_t * schedule_addresses)80 int caladdress_lookup(const char *addr, struct caldav_sched_param *param,
81                       const strarray_t *schedule_addresses)
82 {
83     const char *userid = addr;
84     int i;
85 
86     if (!addr) return HTTP_NOT_FOUND;
87 
88     if (!strncasecmp(userid, "mailto:", 7)) userid += 7;
89 
90     char *addresses = schedule_addresses ? strarray_join(schedule_addresses, ",") : xstrdup("NULL");
91     syslog(LOG_DEBUG,
92            "caladdress_lookup(userid: '%s', schedule_addresses: '%s')",
93            userid, addresses);
94     free(addresses);
95 
96     memset(param, 0, sizeof(struct caldav_sched_param));
97 
98     param->userid = xstrdup(userid);
99 
100     if (schedule_addresses) {
101         for (i = 0; i < strarray_size(schedule_addresses); i++) {
102             const char *item = strarray_nth(schedule_addresses, i);
103             if (!strncasecmp(item, "mailto:", 7)) item += 7;
104             if (strcasecmp(item, userid)) continue;
105             // found one!
106             param->isyou = 1;
107             return 0; // myself is always local
108         }
109     }
110 
111     // does this user have an inbox on this machine?
112 
113     /* XXX  Do LDAP/DB/socket lookup to see if user is local */
114     /* XXX  Hack until real lookup stuff is written */
115     int islocal = 0;
116 
117     const char *at = strchr(userid, '@');
118     if (at) {
119         struct strlist *domains = cua_domains;
120         for (; domains && strcmp(at+1, domains->s); domains = domains->next);
121         if (domains) islocal = 1;
122     }
123 
124     if (islocal) {
125         mbentry_t *mbentry = NULL;
126         /* Lookup user's cal-home-set to see if its on this server */
127         mbname_t *mbname = mbname_from_userid(userid);
128         mbname_push_boxes(mbname, config_getstring(IMAPOPT_CALENDARPREFIX));
129         int r = proxy_mlookup(mbname_intname(mbname), &mbentry, NULL, NULL);
130         mbname_free(&mbname);
131 
132         if (!r) {
133             param->server = xstrdupnull(mbentry->server); /* freed by sched_param_fini */
134             mboxlist_entry_free(&mbentry);
135             if (param->server) param->flags |= SCHEDTYPE_ISCHEDULE;
136             return 0;
137         }
138     }
139 
140     /* User is outside of our domain(s) -
141        Do remote scheduling (default = iMIP) */
142     param->flags |= SCHEDTYPE_REMOTE;
143 
144     /* Do iSchedule DNS SRV lookup */
145 
146     /* XXX  If success, set server, port,
147        and flags |= SCHEDTYPE_ISCHEDULE [ | SCHEDTYPE_SSL ] */
148 
149 #ifdef IOPTEST  /* CalConnect ioptest */
150     if (!strcmp(p, "example.com")) {
151         param->server = xstrdup("ischedule.example.com");
152         param->port = 8008;
153         param->flags |= SCHEDTYPE_ISCHEDULE;
154     }
155     else if (!strcmp(p, "mysite.edu")) {
156         param->server = xstrdup("ischedule.mysite.edu");
157         param->port = 8080;
158         param->flags |= SCHEDTYPE_ISCHEDULE;
159     }
160     else if (!strcmp(p, "bedework.org")) {
161         param->server = xstrdup("www.bedework.org");
162         param->port = 80;
163         param->flags |= SCHEDTYPE_ISCHEDULE;
164     }
165 #endif /* IOPTEST */
166 
167     return 0;
168 }
169 
170 struct address_t {
171     const char *addr;
172     const char *name;
173     char *qpname;
174     const char *role;
175     const char *partstat;
176     struct address_t *next;
177 };
178 
add_address(struct address_t ** recipients,icalproperty * prop,const char * (* icalproperty_get_address)(icalproperty *))179 static void add_address(struct address_t **recipients, icalproperty *prop,
180                         const char* (*icalproperty_get_address)(icalproperty *))
181 {
182     struct address_t *new = xzmalloc(sizeof(struct address_t));
183     icalparameter *param;
184 
185     const char *address = icalproperty_get_address(prop);
186     if (!address) return;
187     if (!strncasecmp(address, "mailto:", 7))
188         address += 7;
189 
190     new->addr = address;
191     param = icalproperty_get_first_parameter(prop, ICAL_CN_PARAMETER);
192     if (param) {
193         new->name = icalparameter_get_cn(param);
194         new->qpname = charset_encode_mimeheader(new->name, 0, 0);
195     }
196     param = icalproperty_get_first_parameter(prop, ICAL_ROLE_PARAMETER);
197     if (param)
198         new->role = icalparameter_enum_to_string(icalparameter_get_role(param));
199     param = icalproperty_get_first_parameter(prop, ICAL_PARTSTAT_PARAMETER);
200     if (param)
201         new->partstat =
202             icalparameter_enum_to_string(icalparameter_get_partstat(param));
203 
204     new->next = *recipients;
205     *recipients = new;
206 }
207 
HTMLencode(struct buf * output,const char * input)208 static void HTMLencode(struct buf *output, const char *input)
209 {
210     int inlen = strlen(input);
211     int outlen = 8*inlen;  /* room for every char to become a named entity */
212 
213     buf_ensure(output, outlen+1);
214     htmlEncodeEntities((unsigned char *) buf_base(output), &outlen,
215                        (unsigned char *) input, &inlen, 0);
216     buf_truncate(output, outlen);
217     buf_replace_all(output, "\n", "\n  <br>");
218 }
219 
220 #define TEXT_INDENT     "             "
221 #define HTML_ROW        "<tr><td><b>%s</b></td><td>%s</td></tr>\r\n"
222 
223 /* Send an iMIP request for attendees in 'ical' */
imip_send_sendmail(icalcomponent * ical,const char * sender,const char * recipient,int is_update)224 static int imip_send_sendmail(icalcomponent *ical, const char *sender,
225                               const char *recipient, int is_update)
226 {
227     int r;
228     icalcomponent *comp;
229     icalproperty *prop;
230     icalproperty_method meth;
231     icalcomponent_kind kind;
232     const char *uid, *summary, *location, *descrip, *status;
233     const char *msg_type, *filename;
234     struct address_t *recipients = NULL, *originator = NULL, *recip;
235     struct icaltimetype start, end;
236     char *cp, when[2*RFC5322_DATETIME_MAX+4], datestr[RFC5322_DATETIME_MAX+1];
237     char boundary[100], *mimebody, *ical_str;
238     size_t outlen;
239     struct buf plainbuf = BUF_INITIALIZER, tmpbuf = BUF_INITIALIZER, msgbuf = BUF_INITIALIZER;
240     pid_t p = getpid();
241     time_t t = time(NULL);
242     static unsigned send_count = 0;
243 
244     meth = icalcomponent_get_method(ical);
245     comp = icalcomponent_get_first_real_component(ical);
246     kind = icalcomponent_isa(comp);
247     uid = icalcomponent_get_uid(comp);
248 
249     /* Determine Originator and Recipient(s) based on method and component */
250     if (meth == ICAL_METHOD_REPLY) {
251         msg_type = "a RSVP";
252         filename = "RSVP";
253 
254         prop = icalcomponent_get_first_invitee(comp);
255         add_address(&originator, prop, &icalproperty_get_invitee);
256 
257         prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
258         add_address(&recipients, prop,
259                     (const char*(*)(icalproperty *))&icalproperty_get_organizer);
260     }
261     else {
262         if (meth == ICAL_METHOD_CANCEL) {
263             msg_type = "a cancellation";
264             filename = "Canceled";
265         }
266         else if (is_update) {
267             msg_type = "an updated invitation";
268             filename = "Update";
269         }
270         else {
271             msg_type = "an invitation";
272             filename = "Invitation";
273         }
274 
275         prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
276         add_address(&originator, prop,
277                     (const char*(*)(icalproperty *))&icalproperty_get_organizer);
278 
279         for (prop = icalcomponent_get_first_invitee(comp);
280              prop;
281              prop = icalcomponent_get_next_invitee(comp)) {
282 
283             add_address(&recipients, prop, &icalproperty_get_invitee);
284         }
285     }
286 
287     /* Get other useful properties/values */
288     summary = icalcomponent_get_summary(comp);
289     location = icalcomponent_get_location(comp);
290     descrip = icalcomponent_get_description(comp);
291     if ((prop = icalcomponent_get_first_property(comp, ICAL_STATUS_PROPERTY))) {
292         status = icalproperty_get_value_as_string(prop);
293     }
294     else status = NULL;
295 
296     start = icaltime_convert_to_zone(icalcomponent_get_dtstart(comp), utc_zone);
297     end = icaltime_convert_to_zone(icalcomponent_get_dtend(comp), utc_zone);
298 
299     cp = when;
300     cp += sprintf(cp, "%s, %02u %s %04u",
301                   wday[icaltime_day_of_week(start)-1],
302                   start.day, monthname[start.month-1], start.year);
303     if (!icaltime_is_date(start)) {
304         cp += sprintf(cp, " %02u:%02u", start.hour, start.minute);
305         if (start.second) cp += sprintf(cp, ":%02u", start.second);
306         strcpy(cp, " UTC");
307     }
308     else icaltime_adjust(&end, -1, 0, 0, 0);
309 
310     if (icaltime_compare(end, start)) {
311         strcpy(cp, " -");
312         cp += 2;
313         if (icaltime_compare_date_only(end, start)) {
314             cp += sprintf(cp, " %s, %02u %s %04u",
315                           wday[icaltime_day_of_week(end)-1],
316                           end.day, monthname[end.month-1], end.year);
317         }
318         if (!icaltime_is_date(end)) {
319             cp += sprintf(cp, " %02u:%02u", end.hour, end.minute);
320             if (end.second) cp += sprintf(cp, ":%02u", end.second);
321             strcpy(cp, " UTC");
322         }
323     }
324 
325     /* Create multipart/mixed + multipart/alternative iMIP message */
326     buf_printf(&msgbuf, "From: %s <%s>\r\n",
327             originator->qpname ? originator->qpname : "", sender);
328 
329     for (recip = recipients; recip; recip = recip->next) {
330         if (strcmp(recip->addr, sender) &&
331             (!recipient || !strcasecmp(recip->addr, recipient))) {
332             buf_printf(&msgbuf, "To: %s <%s>\r\n",
333                     recip->qpname ? recip->qpname : "", recip->addr);
334         }
335     }
336 
337     buf_printf(&msgbuf, "Subject: %s: ", filename);
338     if (summary) {
339         char *mimehdr = charset_encode_mimeheader(summary, 0, 0);
340         buf_appendcstr(&msgbuf, mimehdr);
341         free(mimehdr);
342     }
343     else {
344         buf_appendcstr(&msgbuf, icalcomponent_kind_to_string(kind));
345     }
346     buf_appendcstr(&msgbuf, "\r\n");
347 
348     time_to_rfc5322(t, datestr, sizeof(datestr));
349     buf_printf(&msgbuf, "Date: %s\r\n", datestr);
350 
351     buf_printf(&msgbuf, "Message-ID: <cyrus-caldav-%u-" TIME_T_FMT "-%u@%s>\r\n",
352             p, t, send_count++, config_servername);
353 
354     /* Create multipart boundary */
355     snprintf(boundary, sizeof(boundary), "%s=_%ld=_%ld=_%ld",
356              config_servername, (long) p, (long) t, (long) rand());
357 
358     buf_printf(&msgbuf, "Content-Type: multipart/mixed;"
359             "\r\n\tboundary=\"%s_M\"\r\n", boundary);
360 
361     buf_printf(&msgbuf, "iMIP-Content-ID: <%s@%s>\r\n", uid, config_servername);
362 
363     buf_appendcstr(&msgbuf, "Auto-Submitted: auto-generated\r\n");
364     buf_appendcstr(&msgbuf, "MIME-Version: 1.0\r\n");
365     buf_appendcstr(&msgbuf, "\r\n");
366 
367     /* preamble */
368     buf_appendcstr(&msgbuf, "This is a message with multiple parts in MIME format.\r\n");
369 
370     /* multipart/alternative */
371     buf_printf(&msgbuf, "\r\n--%s_M\r\n", boundary);
372 
373     buf_printf(&msgbuf, "Content-Type: multipart/alternative;"
374             "\r\n\tboundary=\"%s_A\"\r\n", boundary);
375 
376     /* plain text part */
377     buf_printf(&msgbuf, "\r\n--%s_A\r\n", boundary);
378 
379     buf_appendcstr(&msgbuf, "Content-Type: text/plain; charset=utf-8\r\n");
380     buf_appendcstr(&msgbuf, "Content-Disposition: inline\r\n");
381 
382     buf_printf(&plainbuf, "You have received %s from %s <%s>\r\n\r\n", msg_type,
383                originator->name ? originator->name : "", originator->addr);
384     if (summary) {
385         buf_setcstr(&tmpbuf, summary);
386         buf_replace_all(&tmpbuf, "\n", "\r\n" TEXT_INDENT);
387         buf_printf(&plainbuf, "Summary    : %s\r\n", buf_cstring(&tmpbuf));
388     }
389     if (location) {
390         buf_setcstr(&tmpbuf, location);
391         buf_replace_all(&tmpbuf, "\n", "\r\n" TEXT_INDENT);
392         buf_printf(&plainbuf, "Location   : %s\r\n", buf_cstring(&tmpbuf));
393     }
394     buf_printf(&plainbuf, "When       : %s\r\n", when);
395     if (meth == ICAL_METHOD_REPLY) {
396         if (originator->partstat)
397             buf_printf(&plainbuf, "RSVP       : %s\r\n", originator->partstat);
398     }
399     else {
400         if (status) buf_printf(&plainbuf, "Status     : %s\r\n", status);
401 
402         for (cp = "Attendees  : ", recip=recipients; recip; recip=recip->next) {
403             buf_printf(&plainbuf, "%s* %s <%s>",
404                        cp, recip->name ? recip->name : "", recip->addr);
405             if (recip->role) buf_printf(&plainbuf, "\t(%s)", recip->role);
406             buf_appendcstr(&plainbuf, "\r\n");
407 
408             cp = TEXT_INDENT;
409         }
410 
411         if (descrip) {
412             buf_setcstr(&tmpbuf, descrip);
413             buf_replace_all(&tmpbuf, "\n", "\r\n" TEXT_INDENT);
414             buf_printf(&plainbuf, "Description: %s\r\n", buf_cstring(&tmpbuf));
415         }
416     }
417 
418     mimebody = charset_qpencode_mimebody(buf_base(&plainbuf),
419                                          buf_len(&plainbuf), 0, &outlen);
420 
421     if (outlen > buf_len(&plainbuf)) {
422         buf_appendcstr(&msgbuf, "Content-Transfer-Encoding: quoted-printable\r\n");
423     }
424     buf_appendcstr(&msgbuf, "\r\n");
425 
426     buf_appendmap(&msgbuf, mimebody, outlen);
427     free(mimebody);
428     buf_free(&plainbuf);
429 
430     /* HTML part */
431     buf_printf(&msgbuf, "\r\n--%s_A\r\n", boundary);
432 
433     buf_printf(&msgbuf, "Content-Type: text/html; charset=utf-8\r\n");
434     buf_appendcstr(&msgbuf, "Content-Disposition: inline\r\n");
435     buf_appendcstr(&msgbuf, "\r\n");
436 
437     buf_appendcstr(&msgbuf, HTML_DOCTYPE "\r\n<html><head><title></title></head><body>\r\n");
438 
439     if (originator->name) {
440         HTMLencode(&tmpbuf, originator->name);
441         originator->name = buf_cstring(&tmpbuf);
442     }
443     else originator->name = originator->addr;
444 
445     buf_printf(&msgbuf, "<b>You have received %s from"
446             " <a href=\"mailto:%s\">%s</a></b><p>\r\n",
447             msg_type, originator->addr, originator->name);
448 
449     buf_appendcstr(&msgbuf, "<table border cellpadding=5>\r\n");
450     if (summary) {
451         HTMLencode(&tmpbuf, summary);
452         buf_printf(&msgbuf, HTML_ROW, "Summary", buf_cstring(&tmpbuf));
453     }
454     if (location) {
455         HTMLencode(&tmpbuf, location);
456         buf_printf(&msgbuf, HTML_ROW, "Location", buf_cstring(&tmpbuf));
457     }
458     buf_printf(&msgbuf, HTML_ROW, "When", when);
459     if (meth == ICAL_METHOD_REPLY) {
460         if (originator->partstat)
461             buf_printf(&msgbuf, HTML_ROW, "RSVP", originator->partstat);
462     }
463     else {
464         if (status) buf_printf(&msgbuf, HTML_ROW, "Status", status);
465 
466         buf_appendcstr(&msgbuf, "<tr><td><b>Attendees</b></td>");
467         for (cp = "<td>", recip = recipients; recip; recip = recip->next) {
468             if (recip->name) {
469                 HTMLencode(&tmpbuf, recip->name);
470                 recip->name = buf_cstring(&tmpbuf);
471             }
472             else recip->name = recip->addr;
473 
474             buf_printf(&msgbuf, "%s&#8226; <a href=\"mailto:%s\">%s</a>",
475                     cp, recip->addr, recip->name);
476             if (recip->role) buf_printf(&msgbuf, " <i>(%s)</i>", recip->role);
477 
478             cp = "\n  <br>";
479         }
480         buf_appendcstr(&msgbuf, "</td></tr>\r\n");
481 
482         if (descrip) {
483             HTMLencode(&tmpbuf, descrip);
484             buf_printf(&msgbuf, HTML_ROW, "Description", buf_cstring(&tmpbuf));
485         }
486     }
487     buf_printf(&msgbuf, "</table></body></html>\r\n");
488 
489     /* iCalendar part */
490     buf_printf(&msgbuf, "\r\n--%s_A\r\n", boundary);
491 
492     buf_printf(&msgbuf, "Content-Type: text/calendar; charset=utf-8");
493     buf_printf(&msgbuf, "; method=%s; component=%s \r\n",
494             icalproperty_method_to_string(meth),
495             icalcomponent_kind_to_string(kind));
496 
497     buf_printf(&msgbuf, "Content-ID: <%s@%s>\r\n", uid, config_servername);
498 
499     ical_str = icalcomponent_as_ical_string(ical);
500     mimebody = charset_qpencode_mimebody(ical_str, strlen(ical_str), 0, &outlen);
501 
502     if (outlen > strlen(ical_str)) {
503         buf_appendcstr(&msgbuf, "Content-Transfer-Encoding: quoted-printable\r\n");
504     }
505     buf_appendcstr(&msgbuf, "\r\n");
506 
507     buf_appendmap(&msgbuf, mimebody, outlen);
508     free(mimebody);
509 
510     /* end boundary (alternative) */
511     buf_printf(&msgbuf, "\r\n--%s_A--\r\n", boundary);
512 
513     /* application/ics part */
514     buf_printf(&msgbuf, "\r\n--%s_M\r\n", boundary);
515 
516     buf_printf(&msgbuf,
517             "Content-Type: application/ics; charset=utf-8; name=\"%s.ics\"\r\n",
518             filename);
519     buf_printf(&msgbuf, "Content-Disposition: attachment; filename=\"%s.ics\"\r\n",
520             filename);
521     buf_appendcstr(&msgbuf, "Content-Transfer-Encoding: base64\r\n");
522     buf_appendcstr(&msgbuf, "\r\n");
523 
524     charset_encode_mimebody(NULL, strlen(ical_str), NULL, &outlen,
525                             NULL, 1 /* wrap */);
526     buf_ensure(&tmpbuf, outlen);
527     charset_encode_mimebody(ical_str, strlen(ical_str),
528                             (char *) buf_base(&tmpbuf), &outlen,
529                             NULL, 1 /* wrap */);
530     buf_appendmap(&msgbuf, buf_base(&tmpbuf), outlen);
531 
532     /* end boundary (mixed) and epilogue */
533     buf_printf(&msgbuf, "\r\n--%s_M--\r\n\r\nEnd of MIME multipart body.\r\n", boundary);
534 
535     /* Open SMTP connection */
536     smtpclient_t *sm = NULL;
537     r = smtpclient_open(&sm);
538     if (r) {
539         syslog(LOG_ERR,
540                "imip_send_sendmail(%s): failed to open SMTP client", recipient);
541         r = HTTP_UNAVAILABLE;
542         goto done;
543     }
544     smtpclient_set_auth(sm, httpd_userid);
545     smtpclient_set_notify(sm, "FAILURE,DELAY");
546 
547     /* Set SMTP envelope */
548     smtp_envelope_t sm_env = SMTP_ENVELOPE_INITIALIZER;
549     smtp_envelope_set_from(&sm_env, originator->addr);
550     for (recip = recipients; recip; recip = recip->next) {
551         if (strcmp(recip->addr, originator->addr) &&
552             (!recipient || !strcasecmp(recip->addr, recipient))) {
553             smtp_envelope_add_rcpt(&sm_env, recip->addr);
554         }
555     }
556 
557     /* Send message */
558     r = smtpclient_send(sm, &sm_env, &msgbuf);
559     syslog(LOG_INFO,
560            "imip_send_sendmail(%s): %s", recipient, error_message(r));
561     smtp_envelope_fini(&sm_env);
562 
563     int r2 = smtpclient_close(&sm);
564     if (!r) r = r2;
565 
566   done:
567     buf_free(&msgbuf);
568     buf_free(&tmpbuf);
569     free(originator->qpname);
570     free(originator);
571     do {
572         struct address_t *freeme = recipients;
573         recipients = recipients->next;
574         free(freeme->qpname);
575         free(freeme);
576     } while (recipients);
577 
578     return r;
579 }
580 
581 
582 /* Send an iMIP request for attendees in 'ical' */
imip_send(struct sched_data * sched_data,const char * sender,const char * recipient)583 static int imip_send(struct sched_data *sched_data,
584                      const char *sender, const char *recipient)
585 {
586     const char *notifier = config_getstring(IMAPOPT_IMIPNOTIFIER);
587 
588     syslog(LOG_DEBUG, "imip_send(%s)", recipient);
589 
590     /* if no notifier, fall back to sendmail */
591     if (!notifier) {
592         return imip_send_sendmail(sched_data->itip,
593                                   sender, recipient, sched_data->is_update);
594     }
595 
596     json_t *jsevent, *patch;
597 
598 #ifdef WITH_JMAP
599     if (sched_data->oldical) {
600         jsevent = jmapical_tojmap(sched_data->oldical, NULL);
601 
602         if (sched_data->newical) {
603             /* Updated event */
604             json_t *new_jsevent = jmapical_tojmap(sched_data->newical, NULL);
605 
606             patch = jmap_patchobject_create(jsevent, new_jsevent);
607             json_decref(new_jsevent);
608         }
609         else {
610             /* Canceled event */
611             patch = json_null();
612         }
613     }
614     else {
615         /* New event */
616         jsevent = json_null();
617         patch = jmapical_tojmap(sched_data->newical, NULL);
618     }
619 #else
620     jsevent = json_null();
621     patch = json_null();
622 #endif
623 
624     /* Don't send a bogus message - check late to not allocate our own copy */
625     const char *ical_str = icalcomponent_as_ical_string(sched_data->itip);
626     if (!ical_str) return 0;
627 
628     json_t *val = json_pack("{s:s s:s s:s s:o s:o s:b}",
629                             "recipient", recipient,
630                             "sender", sender,
631                             "ical", ical_str,
632                             "jsevent", jsevent,
633                             "patch", patch,
634                             "is_update", sched_data->is_update);
635     char *serial = json_dumps(val, JSON_COMPACT);
636     // XXX: should we be replacing httpd_userid with sender?
637     notify(notifier, "IMIP", NULL, httpd_userid, NULL, 0, NULL, serial, NULL);
638     free(serial);
639     json_decref(val);
640 
641     return 0;
642 }
643 
644 
645 /* Add a <response> XML element for 'recipient' to 'root' */
xml_add_schedresponse(xmlNodePtr root,xmlNsPtr dav_ns,xmlChar * recipient,xmlChar * status)646 xmlNodePtr xml_add_schedresponse(xmlNodePtr root, xmlNsPtr dav_ns,
647                                  xmlChar *recipient, xmlChar *status)
648 {
649     xmlNodePtr resp, recip;
650 
651     resp = xmlNewChild(root, NULL, BAD_CAST "response", NULL);
652     recip = xmlNewChild(resp, NULL, BAD_CAST "recipient", NULL);
653 
654     if (dav_ns) xml_add_href(recip, dav_ns, (const char *) recipient);
655     else xmlNodeAddContent(recip, recipient);
656 
657     if (status)
658         xmlNewChild(resp, NULL, BAD_CAST "request-status", status);
659 
660     return resp;
661 }
662 
663 
664 struct remote_rock {
665     struct transaction_t *txn;
666     icalcomponent *ical;
667     xmlNodePtr root;
668     xmlNsPtr *ns;
669 };
670 
671 /* Send an iTIP busytime request to remote attendees via iMIP or iSchedule */
busytime_query_remote(const char * server,void * data,void * rock)672 static void busytime_query_remote(const char *server __attribute__((unused)),
673                                   void *data, void *rock)
674 {
675     struct caldav_sched_param *remote = (struct caldav_sched_param *) data;
676     struct remote_rock *rrock = (struct remote_rock *) rock;
677     icalcomponent *comp;
678     struct proplist *list;
679     xmlNodePtr resp;
680     const char *status = NULL;
681     int r;
682 
683     syslog(LOG_DEBUG, "busytime_query_remote(server: '%s', flags: 0x%x)",
684            server, remote->flags);
685 
686     comp = icalcomponent_get_first_real_component(rrock->ical);
687 
688     /* Add the attendees to the iTIP request */
689     for (list = remote->props; list; list = list->next) {
690         icalcomponent_add_property(comp, list->prop);
691     }
692 
693     if (remote->flags == SCHEDTYPE_REMOTE) {
694         /* Use iMIP -
695            don't bother sending, its not very useful and not well supported */
696         status = REQSTAT_TEMPFAIL;
697     }
698     else {
699         /* Use iSchedule */
700         xmlNodePtr xml;
701 
702         r = isched_send(remote, NULL, rrock->ical, &xml);
703         if (r) status = REQSTAT_TEMPFAIL;
704         else if (xmlStrcmp(xml->name, BAD_CAST "schedule-response")) {
705             if (r) status = REQSTAT_TEMPFAIL;
706         }
707         else {
708             xmlNodePtr cur;
709 
710             /* Process each response element */
711             for (cur = xml->children; cur; cur = cur->next) {
712                 xmlNodePtr node;
713                 xmlChar *recip = NULL, *status = NULL, *content = NULL;
714 
715                 if (cur->type != XML_ELEMENT_NODE) continue;
716 
717                 for (node = cur->children; node; node = node->next) {
718                     if (node->type != XML_ELEMENT_NODE) continue;
719 
720                     if (!xmlStrcmp(node->name, BAD_CAST "recipient"))
721                         recip = xmlNodeGetContent(node);
722                     else if (!xmlStrcmp(node->name, BAD_CAST "request-status"))
723                         status = xmlNodeGetContent(node);
724                     else if (!xmlStrcmp(node->name, BAD_CAST "calendar-data"))
725                         content = xmlNodeGetContent(node);
726                 }
727 
728                 resp =
729                     xml_add_schedresponse(rrock->root,
730                                           !(rrock->txn->req_tgt.allow & ALLOW_ISCHEDULE) ?
731                                           rrock->ns[NS_DAV] : NULL,
732                                           recip, status);
733 
734                 xmlFree(status);
735                 xmlFree(recip);
736 
737                 if (content) {
738                     xmlNodePtr cdata =
739                         xmlNewTextChild(resp, NULL,
740                                         BAD_CAST "calendar-data", NULL);
741                     xmlAddChild(cdata,
742                                 xmlNewCDataBlock(rrock->root->doc,
743                                                  content,
744                                                  xmlStrlen(content)));
745                     xmlFree(content);
746 
747                     /* iCal data in resp SHOULD NOT be transformed */
748                     rrock->txn->flags.cc |= CC_NOTRANSFORM;
749                 }
750             }
751 
752             xmlFreeDoc(xml->doc);
753         }
754     }
755 
756     /* Report request-status (if necessary)
757      * Remove the attendees from the iTIP request and hash bucket
758      */
759     for (list = remote->props; list; list = list->next) {
760         if (status) {
761             const char *attendee = icalproperty_get_attendee(list->prop);
762             xml_add_schedresponse(rrock->root,
763                                   !(rrock->txn->req_tgt.allow & ALLOW_ISCHEDULE) ?
764                                   rrock->ns[NS_DAV] : NULL,
765                                   BAD_CAST attendee,
766                                   BAD_CAST status);
767         }
768 
769         icalcomponent_remove_property(comp, list->prop);
770         icalproperty_free(list->prop);
771     }
772 
773     if (remote->server) free(remote->server);
774 }
775 
776 
sched_param_cleanup(void * data)777 static void sched_param_cleanup(void *data)
778 {
779     struct caldav_sched_param *sparam = (struct caldav_sched_param *) data;
780 
781     if (sparam) {
782         sched_param_fini(sparam);
783         free(sparam);
784     }
785 }
786 
787 
788 /* Perform a Busy Time query based on given VFREEBUSY component */
789 /* NOTE: This function is destructive of 'ical' */
sched_busytime_query(struct transaction_t * txn,struct mime_type_t * mime,icalcomponent * ical)790 int sched_busytime_query(struct transaction_t *txn,
791                          struct mime_type_t *mime, icalcomponent *ical)
792 {
793     int ret = 0;
794     static const char *calendarprefix = NULL;
795     icalcomponent *comp;
796     icalproperty *prop = NULL, *next;
797     const char *uid = NULL, *organizer = NULL;
798     struct caldav_sched_param sparam;
799     struct auth_state *org_authstate = NULL;
800     xmlNodePtr root = NULL;
801     xmlNsPtr ns[NUM_NAMESPACE];
802     struct propfind_ctx fctx;
803     struct freebusy_filter calfilter;
804     struct hash_table remote_table;
805     struct caldav_sched_param *remote = NULL;
806 
807     if (!calendarprefix) {
808         calendarprefix = config_getstring(IMAPOPT_CALENDARPREFIX);
809     }
810 
811     comp = icalcomponent_get_first_real_component(ical);
812     uid = icalcomponent_get_uid(comp);
813 
814     prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
815     organizer = icalproperty_get_organizer(prop);
816 
817     /* XXX  Do we need to do more checks here? */
818     if (caladdress_lookup(organizer, &sparam, NULL) ||
819         (sparam.flags & SCHEDTYPE_REMOTE))
820         org_authstate = auth_newstate("anonymous");
821     else
822         org_authstate = auth_newstate(sparam.userid);
823 
824     /* Start construction of our schedule-response */
825     if (!(root =
826           init_xml_response("schedule-response",
827                             (txn->req_tgt.allow & ALLOW_ISCHEDULE) ? NS_ISCHED :
828                             NS_CALDAV, NULL, ns))) {
829         ret = HTTP_SERVER_ERROR;
830         txn->error.desc = "Unable to create XML response\r\n";
831         goto done;
832     }
833 
834     /* Need DAV for hrefs */
835     ensure_ns(ns, NS_DAV, root, XML_NS_DAV, "D");
836 
837     /* Populate our filter and propfind context for local attendees */
838     memset(&calfilter, 0, sizeof(struct freebusy_filter));
839     calfilter.start = icalcomponent_get_dtstart(comp);
840     calfilter.end = icalcomponent_get_dtend(comp);
841     calfilter.flags = CHECK_CAL_TRANSP | CHECK_USER_AVAIL;
842 
843     memset(&fctx, 0, sizeof(struct propfind_ctx));
844     fctx.txn = txn;
845     fctx.req_tgt = &txn->req_tgt;
846     fctx.depth = 2;
847     fctx.userid = httpd_userid;
848     fctx.userisadmin = httpd_userisadmin;
849     fctx.authstate = org_authstate;
850     fctx.reqd_privs = 0;  /* handled by CALDAV:schedule-deliver on Inbox */
851     fctx.filter_crit = &calfilter;
852     fctx.ret = &ret;
853 
854     /* Create hash table for any remote attendee servers */
855     construct_hash_table(&remote_table, 10, 1);
856 
857     /* Process each attendee */
858     for (prop = icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY);
859          prop;
860          prop = next) {
861         const char *attendee;
862         int r;
863 
864         next = icalcomponent_get_next_property(comp, ICAL_ATTENDEE_PROPERTY);
865 
866         /* Remove each attendee so we can add in only those
867            that reside on a given remote server later */
868         icalcomponent_remove_property(comp, prop);
869 
870         /* Is attendee remote or local? */
871         attendee = icalproperty_get_attendee(prop);
872         r = caladdress_lookup(attendee, &sparam, NULL);
873 
874         /* Don't allow scheduling of remote users via an iSchedule request */
875         if ((sparam.flags & SCHEDTYPE_REMOTE) &&
876             (txn->req_tgt.allow & ALLOW_ISCHEDULE)) {
877             r = HTTP_FORBIDDEN;
878         }
879 
880         if (r) {
881             xml_add_schedresponse(root,
882                                   !(txn->req_tgt.allow & ALLOW_ISCHEDULE) ?
883                                   ns[NS_DAV] : NULL,
884                                   BAD_CAST attendee, BAD_CAST REQSTAT_NOUSER);
885 
886             icalproperty_free(prop);
887         }
888         else if (sparam.flags) {
889             /* Remote attendee */
890             struct proplist *newprop;
891             const char *key;
892 
893             if (sparam.flags == SCHEDTYPE_REMOTE) {
894                 /* iMIP - collect attendees under empty key (no server) */
895                 key = "";
896             }
897             else {
898                 /* iSchedule - collect attendees by server */
899                 key = sparam.server;
900             }
901 
902             remote = hash_lookup(key, &remote_table);
903             if (!remote) {
904                 /* New remote - add it to the hash table */
905                 remote = xzmalloc(sizeof(struct caldav_sched_param));
906                 if (sparam.server) remote->server = xstrdup(sparam.server);
907                 remote->port = sparam.port;
908                 remote->flags = sparam.flags;
909                 hash_insert(key, remote, &remote_table);
910             }
911             newprop = xmalloc(sizeof(struct proplist));
912             newprop->prop = prop;
913             newprop->next = remote->props;
914             remote->props = newprop;
915         }
916         else {
917             /* Local attendee on this server */
918             xmlNodePtr resp;
919             const char *userid = sparam.userid;
920             icalcomponent *busy = NULL;
921             mbentry_t *mbentry = NULL;
922             const char *status = REQSTAT_NOUSER;
923 
924             resp =
925                 xml_add_schedresponse(root,
926                                       !(txn->req_tgt.allow & ALLOW_ISCHEDULE) ?
927                                       ns[NS_DAV] : NULL,
928                                       BAD_CAST attendee, NULL);
929 
930             /* Check ACL of ORGANIZER on attendee's Scheduling Inbox */
931             char *inboxname = caldav_mboxname(userid, SCHED_INBOX);
932 
933             r = mboxlist_lookup(inboxname, &mbentry, NULL);
934             if (r) {
935                 syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s",
936                        inboxname, error_message(r));
937                 status = REQSTAT_REJECTED;
938             }
939             else if (!(httpd_myrights(org_authstate, mbentry) & DACL_SCHEDFB)) {
940                 status = REQSTAT_NOPRIVS;
941             }
942             else {
943                 /* Start query at attendee's calendar-home-set */
944                 char *mboxname = caldav_mboxname(userid, NULL);
945 
946                 fctx.davdb = NULL;
947                 fctx.req_tgt->collection = NULL;
948                 calfilter.freebusy.len = 0;
949                 busy = busytime_query_local(txn, &fctx, mboxname,
950                                             ICAL_METHOD_REPLY, uid,
951                                             organizer, attendee);
952                 free(mboxname);
953             }
954             mboxlist_entry_free(&mbentry);
955             free(inboxname);
956 
957             if (busy) {
958                 xmlNodePtr cdata;
959                 struct buf *fb_str = mime->from_object(busy);
960                 icalcomponent_free(busy);
961 
962                 xmlNewChild(resp, NULL, BAD_CAST "request-status",
963                             BAD_CAST REQSTAT_SUCCESS);
964 
965                 cdata = xmlNewTextChild(resp, NULL,
966                                         BAD_CAST "calendar-data", NULL);
967 
968                 /* Trim any charset from content-type */
969                 buf_reset(&txn->buf);
970                 buf_printf(&txn->buf, "%.*s",
971                            (int) strcspn(mime->content_type, ";"),
972                            mime->content_type);
973 
974                 xmlNewProp(cdata, BAD_CAST "content-type",
975                            BAD_CAST buf_cstring(&txn->buf));
976 
977                 if (mime->version)
978                     xmlNewProp(cdata, BAD_CAST "version",
979                                BAD_CAST mime->version);
980 
981                 xmlAddChild(cdata,
982                             xmlNewCDataBlock(root->doc,
983                                              BAD_CAST buf_base(fb_str),
984                                              buf_len(fb_str)));
985                 buf_destroy(fb_str);
986 
987                 /* iCalendar data in response should not be transformed */
988                 txn->flags.cc |= CC_NOTRANSFORM;
989             }
990             else {
991                 xmlNewChild(resp, NULL, BAD_CAST "request-status",
992                             BAD_CAST status);
993             }
994 
995             icalproperty_free(prop);
996         }
997     }
998 
999     buf_reset(&txn->buf);
1000 
1001     if (remote) {
1002         struct remote_rock rrock = { txn, ical, root, ns };
1003         hash_enumerate(&remote_table, busytime_query_remote, &rrock);
1004     }
1005     free_hash_table(&remote_table, sched_param_cleanup);
1006 
1007     /* Output the XML response */
1008     if (!ret) xml_response(HTTP_OK, txn, root->doc);
1009 
1010   done:
1011     if (org_authstate) auth_freestate(org_authstate);
1012     if (calfilter.freebusy.fb) free(calfilter.freebusy.fb);
1013     if (root) xmlFreeDoc(root->doc);
1014 
1015     return ret;
1016 }
1017 
1018 
1019 
1020 #define SCHEDSTAT_PENDING       "1.0"
1021 #define SCHEDSTAT_SENT          "1.1"
1022 #define SCHEDSTAT_DELIVERED     "1.2"
1023 #define SCHEDSTAT_SUCCESS       "2.0"
1024 #define SCHEDSTAT_PARAM         "2.3"
1025 #define SCHEDSTAT_NOUSER        "3.7"
1026 #define SCHEDSTAT_NOPRIVS       "3.8"
1027 #define SCHEDSTAT_TEMPFAIL      "5.1"
1028 #define SCHEDSTAT_PERMFAIL      "5.2"
1029 #define SCHEDSTAT_REJECTED      "5.3"
1030 
1031 /* Deliver scheduling object to a remote recipient */
sched_deliver_remote(const char * sender,const char * recipient,struct caldav_sched_param * sparam,struct sched_data * sched_data)1032 static void sched_deliver_remote(const char *sender, const char *recipient,
1033                                  struct caldav_sched_param *sparam,
1034                                  struct sched_data *sched_data)
1035 {
1036     int r;
1037 
1038     syslog(LOG_DEBUG, "sched_deliver_remote(%s, %X)", recipient, sparam->flags);
1039 
1040     icalcomponent_add_required_timezones(sched_data->itip);
1041 
1042     if (sparam->flags & SCHEDTYPE_ISCHEDULE) {
1043         /* Use iSchedule */
1044         xmlNodePtr xml;
1045 
1046         r = isched_send(sparam, recipient, sched_data->itip, &xml);
1047         if (r) {
1048             sched_data->status = sched_data->ischedule ?
1049                 REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
1050         }
1051         else if (xmlStrcmp(xml->name, BAD_CAST "schedule-response")) {
1052             sched_data->status = sched_data->ischedule ?
1053                 REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
1054         }
1055         else {
1056             xmlNodePtr cur;
1057 
1058             /* Process each response element */
1059             for (cur = xml->children; cur; cur = cur->next) {
1060                 xmlNodePtr node;
1061                 xmlChar *recip = NULL, *status = NULL;
1062                 static char statbuf[1024];
1063 
1064                 if (cur->type != XML_ELEMENT_NODE) continue;
1065 
1066                 for (node = cur->children; node; node = node->next) {
1067                     if (node->type != XML_ELEMENT_NODE) continue;
1068 
1069                     if (!xmlStrcmp(node->name, BAD_CAST "recipient"))
1070                         recip = xmlNodeGetContent(node);
1071                     else if (!xmlStrcmp(node->name,
1072                                         BAD_CAST "request-status"))
1073                         status = xmlNodeGetContent(node);
1074                 }
1075 
1076                 if (!strncmp((const char *) status, "2.0", 3)) {
1077                     sched_data->status = sched_data->ischedule ?
1078                         REQSTAT_DELIVERED : SCHEDSTAT_DELIVERED;
1079                 }
1080                 else {
1081                     if (sched_data->ischedule)
1082                         strlcpy(statbuf, (const char *) status, sizeof(statbuf));
1083                     else
1084                         strlcpy(statbuf, (const char *) status, 4);
1085 
1086                     sched_data->status = statbuf;
1087                 }
1088 
1089                 xmlFree(status);
1090                 xmlFree(recip);
1091             }
1092         }
1093     }
1094     else {
1095         r = imip_send(sched_data, sender, recipient);
1096         if (!r) {
1097             sched_data->status =
1098                 sched_data->ischedule ? REQSTAT_SENT : SCHEDSTAT_SENT;
1099         }
1100         else {
1101             sched_data->status = sched_data->ischedule ?
1102                 REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
1103         }
1104     }
1105 }
1106 
1107 
1108 /*
1109  * deliver_merge_reply() helper function
1110  *
1111  * Merge VOTER responses into VPOLL subcomponents
1112  */
deliver_merge_vpoll_reply(icalcomponent * ical,icalcomponent * reply)1113 static void deliver_merge_vpoll_reply(icalcomponent *ical, icalcomponent *reply)
1114 {
1115     icalcomponent *new_ballot, *vvoter;
1116     icalproperty *voterp;
1117     const char *voter;
1118 
1119     /* Get VOTER from reply */
1120     new_ballot =
1121         icalcomponent_get_first_component(reply, ICAL_VVOTER_COMPONENT);
1122     voterp = icalcomponent_get_first_property(new_ballot, ICAL_VOTER_PROPERTY);
1123     voter = icalproperty_get_voter(voterp);
1124 
1125     /* Locate VOTER in existing VPOLL */
1126     for (vvoter =
1127            icalcomponent_get_first_component(ical, ICAL_VVOTER_COMPONENT);
1128          vvoter;
1129          vvoter =
1130              icalcomponent_get_next_component(ical, ICAL_VVOTER_COMPONENT)) {
1131 
1132         voterp =
1133             icalcomponent_get_first_property(vvoter, ICAL_VOTER_PROPERTY);
1134 
1135         if (!strcmp(voter, icalproperty_get_voter(voterp))) {
1136             icalcomponent_remove_component(ical, vvoter);
1137             icalcomponent_free(vvoter);
1138             break;
1139         }
1140     }
1141 
1142     /* XXX  Actually need to compare POLL-ITEM-IDs */
1143     icalcomponent_add_component(ical, icalcomponent_clone(new_ballot));
1144 }
1145 
1146 
1147 /* sched_reply() helper function
1148  *
1149  * Add voter responses to VPOLL reply and remove candidate components
1150  *
1151  */
sched_vpoll_reply(icalcomponent * poll)1152 static void sched_vpoll_reply(icalcomponent *poll)
1153 {
1154     icalcomponent *item, *next;
1155 
1156     for (item = icalcomponent_get_first_component(poll, ICAL_ANY_COMPONENT);
1157          item;
1158          item = next) {
1159 
1160         next = icalcomponent_get_next_component(poll, ICAL_ANY_COMPONENT);
1161 
1162         switch (icalcomponent_isa(item)) {
1163         case ICAL_VVOTER_COMPONENT:
1164             /* Our ballot, leave it */
1165             /* XXX  Need to compare against previous votes */
1166             break;
1167 
1168         default:
1169             /* Candidate component, remove it */
1170             icalcomponent_remove_component(poll, item);
1171             icalcomponent_free(item);
1172             break;
1173         }
1174     }
1175 }
1176 
1177 
deliver_merge_pollstatus(icalcomponent * ical,icalcomponent * request)1178 static int deliver_merge_pollstatus(icalcomponent *ical, icalcomponent *request)
1179 {
1180     int deliver_inbox = 0;
1181     icalcomponent *oldpoll, *newpoll, *vvoter, *next;
1182 
1183     /* Remove each VVOTER from old object */
1184     oldpoll =
1185         icalcomponent_get_first_component(ical, ICAL_VPOLL_COMPONENT);
1186     for (vvoter =
1187              icalcomponent_get_first_component(oldpoll, ICAL_VVOTER_COMPONENT);
1188          vvoter;
1189          vvoter = next) {
1190 
1191         next = icalcomponent_get_next_component(oldpoll, ICAL_VVOTER_COMPONENT);
1192 
1193         icalcomponent_remove_component(oldpoll, vvoter);
1194         icalcomponent_free(vvoter);
1195     }
1196 
1197     /* Add each VVOTER in the iTIP request to old object */
1198     newpoll = icalcomponent_get_first_component(request, ICAL_VPOLL_COMPONENT);
1199     for (vvoter =
1200              icalcomponent_get_first_component(newpoll, ICAL_VVOTER_COMPONENT);
1201          vvoter;
1202          vvoter =
1203              icalcomponent_get_next_component(newpoll, ICAL_VVOTER_COMPONENT)) {
1204 
1205         icalcomponent_add_component(oldpoll, icalcomponent_clone(vvoter));
1206     }
1207 
1208     return deliver_inbox;
1209 }
1210 
1211 
sched_pollstatus(const char * organizer,struct caldav_sched_param * sparam,icalcomponent * ical,const char * voter)1212 static void sched_pollstatus(const char *organizer,
1213                              struct caldav_sched_param *sparam, icalcomponent *ical,
1214                              const char *voter)
1215 {
1216     struct auth_state *authstate;
1217     struct sched_data sched_data;
1218     icalcomponent *itip, *comp;
1219     icalproperty *prop;
1220 
1221     /* XXX  Do we need to do more checks here? */
1222     if (sparam->flags & SCHEDTYPE_REMOTE)
1223         authstate = auth_newstate("anonymous");
1224     else
1225         authstate = auth_newstate(sparam->userid);
1226 
1227     memset(&sched_data, 0, sizeof(struct sched_data));
1228     sched_data.force_send = ICAL_SCHEDULEFORCESEND_NONE;
1229 
1230     /* Create a shell for our iTIP request objects */
1231     itip = icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT,
1232                                icalproperty_new_version("2.0"),
1233                                icalproperty_new_prodid(ical_prodid),
1234                                icalproperty_new_method(ICAL_METHOD_POLLSTATUS),
1235                                0);
1236 
1237     /* Copy over any CALSCALE property */
1238     prop = icalcomponent_get_first_property(ical, ICAL_CALSCALE_PROPERTY);
1239     if (prop) icalcomponent_add_property(itip, icalproperty_clone(prop));
1240 
1241     /* Process each VPOLL in resource */
1242     for (comp = icalcomponent_get_first_component(ical, ICAL_VPOLL_COMPONENT);
1243          comp;
1244          comp =icalcomponent_get_next_component(ical, ICAL_VPOLL_COMPONENT)) {
1245 
1246         icalcomponent *stat, *poll, *sub, *next;
1247         struct strlist *voters = NULL;
1248 
1249         /* Make a working copy of the iTIP */
1250         stat = icalcomponent_clone(itip);
1251 
1252         /* Make a working copy of the VPOLL and add to pollstatus */
1253         poll = icalcomponent_clone(comp);
1254         icalcomponent_add_component(stat, poll);
1255 
1256         /* Process each sub-component of VPOLL */
1257         for (sub = icalcomponent_get_first_component(poll, ICAL_ANY_COMPONENT);
1258              sub; sub = next) {
1259 
1260             next = icalcomponent_get_next_component(poll, ICAL_ANY_COMPONENT);
1261 
1262             switch (icalcomponent_isa(sub)) {
1263             case ICAL_VVOTER_COMPONENT: {
1264                 /* Make list of VOTERs (stripping SCHEDULE-STATUS) */
1265                 const char *this_voter;
1266 
1267                 prop =
1268                     icalcomponent_get_first_property(sub, ICAL_VOTER_PROPERTY);
1269                 this_voter = icalproperty_get_voter(prop);
1270 
1271                 /* Don't update organizer or voter that triggered POLLSTATUS */
1272                 if (strcmp(this_voter, organizer) && strcmp(this_voter, voter))
1273                     appendstrlist(&voters, (char *) this_voter);
1274 
1275                 icalproperty_remove_parameter_by_name(prop, "SCHEDULE-STATUS");
1276                 break;
1277             }
1278 
1279             default:
1280                 /* Remove candidate components */
1281                 icalcomponent_remove_component(poll, sub);
1282                 icalcomponent_free(sub);
1283                 break;
1284             }
1285         }
1286 
1287         /* Attempt to deliver to each voter in the list - removing as we go */
1288         while (voters) {
1289             struct strlist *next = voters->next;
1290 
1291             sched_data.itip = stat;
1292             sched_deliver(organizer, voters->s, &sched_data, authstate);
1293 
1294             free(voters->s);
1295             free(voters);
1296             voters = next;
1297         }
1298 
1299         icalcomponent_free(stat);
1300     }
1301 
1302     icalcomponent_free(itip);
1303     auth_freestate(authstate);
1304 }
1305 
1306 /* annoying copypaste from libical because it's not exposed */
_get_datetime(icalcomponent * comp,icalproperty * prop)1307 static struct icaltimetype _get_datetime(icalcomponent *comp, icalproperty *prop)
1308 {
1309     icalcomponent *c;
1310     icalparameter *param;
1311     struct icaltimetype ret;
1312 
1313     ret = icalvalue_get_datetime(icalproperty_get_value(prop));
1314 
1315     if ((param = icalproperty_get_first_parameter(prop, ICAL_TZID_PARAMETER)) != NULL) {
1316         const char *tzid = icalparameter_get_tzid(param);
1317         icaltimezone *tz = NULL;
1318 
1319         for (c = comp; c != NULL; c = icalcomponent_get_parent(c)) {
1320             tz = icalcomponent_get_timezone(c, tzid);
1321             if (tz != NULL)
1322                 break;
1323         }
1324 
1325         if (tz == NULL)
1326             tz = icaltimezone_get_builtin_timezone_from_tzid(tzid);
1327 
1328         if (tz != NULL)
1329             ret = icaltime_set_timezone(&ret, tz);
1330     }
1331 
1332     return ret;
1333 }
1334 
1335 
master_to_recurrence(icalcomponent * master,icalproperty * recurid)1336 static icalcomponent *master_to_recurrence(icalcomponent *master, icalproperty *recurid)
1337 {
1338     icalproperty *prop, *next;
1339 
1340     icalproperty *endprop = NULL;
1341     icalproperty *startprop = NULL;
1342 
1343     icalcomponent *comp = icalcomponent_clone(master);
1344 
1345     for (prop = icalcomponent_get_first_property(comp, ICAL_ANY_PROPERTY);
1346          prop; prop = next) {
1347         next = icalcomponent_get_next_property(comp, ICAL_ANY_PROPERTY);
1348 
1349         switch (icalproperty_isa(prop)) {
1350             /* extract start and end for later processing */
1351         case ICAL_DTEND_PROPERTY:
1352             endprop = prop;
1353             break;
1354 
1355         case ICAL_DTSTART_PROPERTY:
1356             startprop = prop;
1357             break;
1358 
1359             /* Remove all recurrence properties */
1360         case ICAL_RRULE_PROPERTY:
1361         case ICAL_RDATE_PROPERTY:
1362         case ICAL_EXDATE_PROPERTY:
1363             icalcomponent_remove_property(comp, prop);
1364             icalproperty_free(prop);
1365             break;
1366 
1367         default:
1368             break;
1369         }
1370     }
1371 
1372     /* Add RECURRENCE-ID */
1373     icalcomponent_add_property(comp, icalproperty_clone(recurid));
1374 
1375     /* calculate a new dtend based on recurid */
1376     struct icaltimetype start = _get_datetime(master, startprop);
1377     struct icaltimetype newstart = _get_datetime(master, recurid);
1378 
1379     icaltimezone *startzone = (icaltimezone *)icaltime_get_timezone(start);
1380     icalcomponent_set_dtstart(comp, icaltime_convert_to_zone(newstart, startzone));
1381 
1382     if (endprop) {
1383         struct icaltimetype end = _get_datetime(master, endprop);
1384 
1385         // calculate and re-apply the diff
1386         struct icaldurationtype diff = icaltime_subtract(end, start);
1387         struct icaltimetype newend = icaltime_add(newstart, diff);
1388 
1389         icaltimezone *endzone = (icaltimezone *)icaltime_get_timezone(end);
1390         icalcomponent_set_dtend(comp, icaltime_convert_to_zone(newend, endzone));
1391     }
1392     /* otherwise it will be a duration, which is still valid! */
1393 
1394     return comp;
1395 }
1396 
1397 
deliver_merge_reply(icalcomponent * ical,icalcomponent * reply)1398 static const char *deliver_merge_reply(icalcomponent *ical,
1399                                        icalcomponent *reply)
1400 {
1401     struct hash_table comp_table;
1402     icalcomponent *comp, *itip, *master = NULL;
1403     icalcomponent_kind kind;
1404     icalproperty *prop, *att;
1405     icalparameter *param;
1406     icalparameter_partstat partstat = ICAL_PARTSTAT_NONE;
1407     icalparameter_rsvp rsvp = ICAL_RSVP_NONE;
1408     const char *recurid, *attendee = NULL, *req_stat = SCHEDSTAT_SUCCESS;
1409 
1410     /* Add each component of old object to hash table for comparison */
1411     construct_hash_table(&comp_table, 10, 1);
1412     comp = icalcomponent_get_first_real_component(ical);
1413     kind = icalcomponent_isa(comp);
1414     do {
1415         prop =
1416             icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
1417         if (prop) recurid = icalproperty_get_value_as_string(prop);
1418         else {
1419             master = comp;
1420             recurid = "";
1421         }
1422 
1423         hash_insert(recurid, comp, &comp_table);
1424 
1425     } while ((comp = icalcomponent_get_next_component(ical, kind)));
1426 
1427 
1428     /* Process each component in the iTIP reply */
1429     for (itip = icalcomponent_get_first_component(reply, kind);
1430          itip;
1431          itip = icalcomponent_get_next_component(reply, kind)) {
1432 
1433         /* Lookup this comp in the hash table */
1434         prop =
1435             icalcomponent_get_first_property(itip, ICAL_RECURRENCEID_PROPERTY);
1436         if (prop) recurid = icalproperty_get_value_as_string(prop);
1437         else recurid = "";
1438 
1439         comp = hash_lookup(recurid, &comp_table);
1440         if (!comp) {
1441             /* New recurrence overridden by attendee. */
1442             if (icalcomponent_get_status(master) == ICAL_STATUS_CANCELLED) {
1443                 /* The master event has been cancelled - ignore this override. */
1444                 continue;
1445             }
1446 
1447             /* create a new recurrence from master component. */
1448             comp = master_to_recurrence(master, prop);
1449 
1450             /* Replace DTSTART, DTEND, SEQUENCE */
1451             prop =
1452                 icalcomponent_get_first_property(comp, ICAL_DTSTART_PROPERTY);
1453             if (prop) {
1454                 icalcomponent_remove_property(comp, prop);
1455                 icalproperty_free(prop);
1456             }
1457             prop =
1458                 icalcomponent_get_first_property(itip, ICAL_DTSTART_PROPERTY);
1459             if (prop)
1460                 icalcomponent_add_property(comp, icalproperty_clone(prop));
1461 
1462             prop =
1463                 icalcomponent_get_first_property(comp, ICAL_DTEND_PROPERTY);
1464             if (prop) {
1465                 icalcomponent_remove_property(comp, prop);
1466                 icalproperty_free(prop);
1467             }
1468             prop =
1469                 icalcomponent_get_first_property(itip, ICAL_DTEND_PROPERTY);
1470             if (prop)
1471                 icalcomponent_add_property(comp, icalproperty_clone(prop));
1472 
1473             prop =
1474                 icalcomponent_get_first_property(comp, ICAL_SEQUENCE_PROPERTY);
1475             if (prop) {
1476                 icalcomponent_remove_property(comp, prop);
1477                 icalproperty_free(prop);
1478             }
1479             prop =
1480                 icalcomponent_get_first_property(itip, ICAL_SEQUENCE_PROPERTY);
1481             if (prop)
1482                 icalcomponent_add_property(comp, icalproperty_clone(prop));
1483 
1484             icalcomponent_add_component(ical, comp);
1485         }
1486         else if (icalcomponent_get_status(comp) == ICAL_STATUS_CANCELLED) {
1487             /* This component has been cancelled - ignore the reply */
1488             continue;
1489         }
1490 
1491         /* Get the sending attendee */
1492         att = icalcomponent_get_first_invitee(itip);
1493         attendee = icalproperty_get_invitee(att);
1494         param = icalproperty_get_first_parameter(att, ICAL_PARTSTAT_PARAMETER);
1495         if (param) partstat = icalparameter_get_partstat(param);
1496         param = icalproperty_get_first_parameter(att, ICAL_RSVP_PARAMETER);
1497         if (param) rsvp = icalparameter_get_rsvp(param);
1498 
1499         prop =
1500             icalcomponent_get_first_property(itip, ICAL_REQUESTSTATUS_PROPERTY);
1501         if (prop) {
1502             struct icalreqstattype rq = icalproperty_get_requeststatus(prop);
1503             req_stat = icalenum_reqstat_code(rq.code);
1504         }
1505 
1506         /* Find matching attendee in existing object */
1507         for (prop = icalcomponent_get_first_invitee(comp);
1508              prop && strcmp(attendee, icalproperty_get_invitee(prop));
1509              prop = icalcomponent_get_next_invitee(comp));
1510         if (!prop) {
1511             /* Attendee added themselves to this recurrence */
1512             assert(icalproperty_isa(prop) != ICAL_VOTER_PROPERTY);
1513             prop = icalproperty_clone(att);
1514             icalcomponent_add_property(comp, prop);
1515         }
1516 
1517         /* Set PARTSTAT */
1518         if (partstat != ICAL_PARTSTAT_NONE) {
1519             param = icalparameter_new_partstat(partstat);
1520             icalproperty_set_parameter(prop, param);
1521         }
1522 
1523         /* Set RSVP */
1524         icalproperty_remove_parameter_by_kind(prop, ICAL_RSVP_PARAMETER);
1525         if (rsvp != ICAL_RSVP_NONE) {
1526             param = icalparameter_new_rsvp(rsvp);
1527             icalproperty_add_parameter(prop, param);
1528         }
1529 
1530         /* Set SCHEDULE-STATUS */
1531         param = icalparameter_new_schedulestatus(req_stat);
1532         icalproperty_set_parameter(prop, param);
1533 
1534         /* Handle VPOLL reply */
1535         if (kind == ICAL_VPOLL_COMPONENT) deliver_merge_vpoll_reply(comp, itip);
1536     }
1537 
1538     free_hash_table(&comp_table, NULL);
1539 
1540     return attendee;
1541 }
1542 
1543 
deliver_merge_request(const char * attendee,icalcomponent * ical,icalcomponent * request)1544 static int deliver_merge_request(const char *attendee,
1545                                  icalcomponent *ical, icalcomponent *request)
1546 {
1547     int deliver_inbox = 0;
1548     struct hash_table comp_table;
1549     icalcomponent *comp, *itip;
1550     icalcomponent_kind kind = ICAL_NO_COMPONENT;
1551     icalproperty *prop;
1552     icalparameter *param;
1553     const char *tzid, *recurid, *organizer = NULL;
1554 
1555     /* Add each VTIMEZONE of old object to hash table for comparison */
1556     construct_hash_table(&comp_table, 10, 1);
1557     for (comp =
1558              icalcomponent_get_first_component(ical, ICAL_VTIMEZONE_COMPONENT);
1559          comp;
1560          comp =
1561              icalcomponent_get_next_component(ical, ICAL_VTIMEZONE_COMPONENT)) {
1562         prop = icalcomponent_get_first_property(comp, ICAL_TZID_PROPERTY);
1563         tzid = icalproperty_get_tzid(prop);
1564         if (!tzid) continue;
1565 
1566         hash_insert(tzid, comp, &comp_table);
1567     }
1568 
1569     /* Process each VTIMEZONE in the iTIP request */
1570     for (itip = icalcomponent_get_first_component(request,
1571                                                   ICAL_VTIMEZONE_COMPONENT);
1572          itip;
1573          itip = icalcomponent_get_next_component(request,
1574                                                  ICAL_VTIMEZONE_COMPONENT)) {
1575         /* Lookup this TZID in the hash table */
1576         prop = icalcomponent_get_first_property(itip, ICAL_TZID_PROPERTY);
1577         tzid = icalproperty_get_tzid(prop);
1578         if (!tzid) continue;
1579 
1580         comp = hash_lookup(tzid, &comp_table);
1581         if (comp) {
1582             /* Remove component from old object */
1583             icalcomponent_remove_component(ical, comp);
1584             icalcomponent_free(comp);
1585         }
1586 
1587         /* Add new/modified component from iTIP request */
1588         icalcomponent_add_component(ical, icalcomponent_clone(itip));
1589     }
1590 
1591     free_hash_table(&comp_table, NULL);
1592 
1593     /* Add each component of old object to hash table for comparison */
1594     construct_hash_table(&comp_table, 10, 1);
1595     comp = icalcomponent_get_first_real_component(ical);
1596     if (comp) {
1597         kind = icalcomponent_isa(comp);
1598         organizer = get_organizer(comp);
1599     }
1600     for (; comp; comp = icalcomponent_get_next_component(ical, kind)) {
1601         prop =
1602             icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
1603         if (prop) recurid = icalproperty_get_value_as_string(prop);
1604         else recurid = "";
1605 
1606         hash_insert(recurid, comp, &comp_table);
1607     }
1608 
1609     /* Process each component in the iTIP request */
1610     itip = icalcomponent_get_first_real_component(request);
1611     if (kind == ICAL_NO_COMPONENT) kind = icalcomponent_isa(itip);
1612     for (; itip; itip = icalcomponent_get_next_component(request, kind)) {
1613         icalcomponent *new_comp = icalcomponent_clone(itip);
1614 
1615         /* Lookup this comp in the hash table */
1616         prop =
1617             icalcomponent_get_first_property(itip, ICAL_RECURRENCEID_PROPERTY);
1618         if (prop) recurid = icalproperty_get_value_as_string(prop);
1619         else recurid = "";
1620 
1621         comp = hash_lookup(recurid, &comp_table);
1622         if (comp) {
1623             int old_seq, new_seq;
1624 
1625             /* Check if this is something more than an update */
1626             /* XXX  Probably need to check PARTSTAT=NEEDS-ACTION
1627                and RSVP=TRUE as well */
1628             old_seq = icalcomponent_get_sequence(comp);
1629             new_seq = icalcomponent_get_sequence(itip);
1630             if (new_seq > old_seq) deliver_inbox = 1;
1631             else if (partstat_changed(comp, itip, organizer)) deliver_inbox = 1;
1632 
1633             /* Copy over any COMPLETED, PERCENT-COMPLETE,
1634                or TRANSP properties */
1635             prop =
1636                 icalcomponent_get_first_property(comp, ICAL_COMPLETED_PROPERTY);
1637             if (prop) {
1638                 icalcomponent_add_property(new_comp,
1639                                            icalproperty_clone(prop));
1640             }
1641             prop =
1642                 icalcomponent_get_first_property(comp,
1643                                                  ICAL_PERCENTCOMPLETE_PROPERTY);
1644             if (prop) {
1645                 icalcomponent_add_property(new_comp,
1646                                            icalproperty_clone(prop));
1647             }
1648             prop =
1649                 icalcomponent_get_first_property(comp, ICAL_TRANSP_PROPERTY);
1650             if (prop) {
1651                 icalcomponent_add_property(new_comp,
1652                                            icalproperty_clone(prop));
1653             }
1654 
1655             /* Copy over any ORGANIZER;SCHEDULE-STATUS */
1656             /* XXX  Do we only do this iff PARTSTAT!=NEEDS-ACTION */
1657             prop =
1658                 icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
1659             param = icalproperty_get_schedulestatus_parameter(prop);
1660             if (param) {
1661                 param = icalparameter_clone(param);
1662                 prop =
1663                     icalcomponent_get_first_property(new_comp,
1664                                                      ICAL_ORGANIZER_PROPERTY);
1665                 icalproperty_add_parameter(prop, param);
1666             }
1667 
1668             /* Remove component from old object */
1669             icalcomponent_remove_component(ical, comp);
1670             icalcomponent_free(comp);
1671         }
1672         else {
1673             /* New component */
1674             deliver_inbox = 1;
1675         }
1676 
1677         if (config_allowsched == IMAP_ENUM_CALDAV_ALLOWSCHEDULING_APPLE &&
1678             kind == ICAL_VEVENT_COMPONENT) {
1679             /* Make VEVENT component transparent if recipient ATTENDEE
1680                PARTSTAT=NEEDS-ACTION (for compatibility with CalendarServer) */
1681             for (prop =
1682                      icalcomponent_get_first_property(new_comp,
1683                                                       ICAL_ATTENDEE_PROPERTY);
1684                  prop && strcmp(icalproperty_get_attendee(prop), attendee);
1685                  prop =
1686                      icalcomponent_get_next_property(new_comp,
1687                                                      ICAL_ATTENDEE_PROPERTY));
1688             param =
1689                 icalproperty_get_first_parameter(prop, ICAL_PARTSTAT_PARAMETER);
1690             if (param &&
1691                 icalparameter_get_partstat(param) ==
1692                 ICAL_PARTSTAT_NEEDSACTION) {
1693                 prop =
1694                     icalcomponent_get_first_property(new_comp,
1695                                                      ICAL_TRANSP_PROPERTY);
1696                 if (prop)
1697                     icalproperty_set_transp(prop, ICAL_TRANSP_TRANSPARENT);
1698                 else {
1699                     prop = icalproperty_new_transp(ICAL_TRANSP_TRANSPARENT);
1700                     icalcomponent_add_property(new_comp, prop);
1701                 }
1702             }
1703         }
1704 
1705         /* Add new/modified component from iTIP request */
1706         icalcomponent_add_component(ical, new_comp);
1707     }
1708 
1709     free_hash_table(&comp_table, NULL);
1710 
1711     return deliver_inbox;
1712 }
1713 
1714 
1715 /* Deliver scheduling object to local recipient */
sched_deliver_local(const char * sender,const char * recipient,struct caldav_sched_param * sparam,struct sched_data * sched_data,struct auth_state * authstate)1716 static void sched_deliver_local(const char *sender, const char *recipient,
1717                                 struct caldav_sched_param *sparam,
1718                                 struct sched_data *sched_data,
1719                                 struct auth_state *authstate)
1720 {
1721     int r = 0, rights, reqd_privs, deliver_inbox = 1;
1722     const char *userid = sparam->userid, *attendee = NULL;
1723     static struct buf resource = BUF_INITIALIZER;
1724     char *mailboxname = NULL;
1725     mbentry_t *mbentry = NULL;
1726     struct mailbox *mailbox = NULL, *inbox = NULL;
1727     struct caldav_db *caldavdb = NULL;
1728     struct caldav_data *cdata;
1729     icalcomponent *ical = NULL;
1730     icalproperty_method method;
1731     icalcomponent_kind kind;
1732     icalcomponent *comp;
1733     icalproperty *prop;
1734     struct transaction_t txn;
1735 
1736     syslog(LOG_DEBUG, "sched_deliver_local(%s, %s, %X)", sender, recipient, sparam->flags);
1737 
1738     /* Start with an empty (clean) transaction */
1739     memset(&txn, 0, sizeof(struct transaction_t));
1740 
1741     /* Check ACL of sender on recipient's Scheduling Inbox */
1742     mailboxname = caldav_mboxname(userid, SCHED_INBOX);
1743     r = mboxlist_lookup(mailboxname, &mbentry, NULL);
1744     if (r) {
1745         syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s",
1746                mailboxname, error_message(r));
1747         sched_data->status =
1748             sched_data->ischedule ? REQSTAT_REJECTED : SCHEDSTAT_REJECTED;
1749         goto done;
1750     }
1751 
1752     rights = httpd_myrights(authstate, mbentry);
1753     mboxlist_entry_free(&mbentry);
1754 
1755     reqd_privs = sched_data->is_reply ? DACL_REPLY : DACL_INVITE;
1756     if (!(rights & reqd_privs)) {
1757         sched_data->status =
1758             sched_data->ischedule ? REQSTAT_NOPRIVS : SCHEDSTAT_NOPRIVS;
1759         syslog(LOG_DEBUG, "No scheduling receive ACL for user %s on Inbox %s",
1760                httpd_userid, userid);
1761         goto done;
1762     }
1763 
1764     /* Open recipient's Inbox for writing */
1765     if ((r = mailbox_open_iwl(mailboxname, &inbox))) {
1766         syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
1767                mailboxname, error_message(r));
1768         sched_data->status =
1769             sched_data->ischedule ? REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
1770         goto done;
1771     }
1772     free(mailboxname);
1773     mailboxname = NULL;
1774 
1775     /* Get METHOD of the iTIP message */
1776     method = icalcomponent_get_method(sched_data->itip);
1777 
1778     /* Search for iCal UID in recipient's calendars */
1779     caldavdb = caldav_open_userid(userid);
1780     if (!caldavdb) {
1781         sched_data->status =
1782             sched_data->ischedule ? REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
1783         goto done;
1784     }
1785 
1786     caldav_lookup_uid(caldavdb,
1787                       icalcomponent_get_uid(sched_data->itip), &cdata);
1788 
1789     if (cdata->dav.mailbox) {
1790         mailboxname = xstrdup(cdata->dav.mailbox);
1791         buf_setcstr(&resource, cdata->dav.resource);
1792     }
1793     else if (sched_data->is_reply) {
1794         /* Can't find object belonging to organizer - ignore reply */
1795         sched_data->status =
1796             sched_data->ischedule ? REQSTAT_PERMFAIL : SCHEDSTAT_PERMFAIL;
1797         goto done;
1798     }
1799     else if (method == ICAL_METHOD_CANCEL || method == ICAL_METHOD_POLLSTATUS) {
1800         /* Can't find object belonging to attendee - we're done */
1801         sched_data->status =
1802             sched_data->ischedule ? REQSTAT_SUCCESS : SCHEDSTAT_DELIVERED;
1803         goto done;
1804     }
1805     else {
1806         /* Can't find object belonging to attendee - use default calendar */
1807         char *scheddefault = caldav_scheddefault(userid);
1808         mailboxname = caldav_mboxname(userid, scheddefault);
1809         free(scheddefault);
1810         buf_reset(&resource);
1811         /* XXX - sanitize the uid? */
1812         buf_printf(&resource, "%s.ics",
1813                    icalcomponent_get_uid(sched_data->itip));
1814 
1815         /* Create new attendee object */
1816         ical = icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT, 0);
1817 
1818         /* Copy over VERSION property */
1819         prop = icalcomponent_get_first_property(sched_data->itip,
1820                                                 ICAL_VERSION_PROPERTY);
1821         icalcomponent_add_property(ical, icalproperty_clone(prop));
1822 
1823         /* Copy over PRODID property */
1824         prop = icalcomponent_get_first_property(sched_data->itip,
1825                                                 ICAL_PRODID_PROPERTY);
1826         icalcomponent_add_property(ical, icalproperty_clone(prop));
1827 
1828         /* Copy over any CALSCALE property */
1829         prop = icalcomponent_get_first_property(sched_data->itip,
1830                                                 ICAL_CALSCALE_PROPERTY);
1831         if (prop) {
1832             icalcomponent_add_property(ical,
1833                                        icalproperty_clone(prop));
1834         }
1835     }
1836 
1837     /* Open recipient's calendar for writing */
1838     r = mailbox_open_iwl(mailboxname, &mailbox);
1839     if (r) {
1840         syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
1841                mailboxname, error_message(r));
1842         sched_data->status =
1843             sched_data->ischedule ? REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
1844         goto done;
1845     }
1846 
1847     if (cdata->dav.imap_uid) {
1848         /* Load message containing the resource and parse iCal data */
1849         ical = caldav_record_to_ical(mailbox, cdata, NULL, NULL);
1850 
1851         for (comp = icalcomponent_get_first_component(sched_data->itip,
1852                                                       ICAL_ANY_COMPONENT);
1853              comp;
1854              comp = icalcomponent_get_next_component(sched_data->itip,
1855                                                      ICAL_ANY_COMPONENT)) {
1856             /* Don't allow component type to be changed */
1857             int reject = 0;
1858             kind = icalcomponent_isa(comp);
1859             switch (kind) {
1860             case ICAL_VEVENT_COMPONENT:
1861                 if (cdata->comp_type != CAL_COMP_VEVENT) reject = 1;
1862                 break;
1863             case ICAL_VTODO_COMPONENT:
1864                 if (cdata->comp_type != CAL_COMP_VTODO) reject = 1;
1865                 break;
1866             case ICAL_VJOURNAL_COMPONENT:
1867                 if (cdata->comp_type != CAL_COMP_VJOURNAL) reject = 1;
1868                 break;
1869             case ICAL_VFREEBUSY_COMPONENT:
1870                 if (cdata->comp_type != CAL_COMP_VFREEBUSY) reject = 1;
1871                 break;
1872             case ICAL_VAVAILABILITY_COMPONENT:
1873                 if (cdata->comp_type != CAL_COMP_VAVAILABILITY) reject = 1;
1874                 break;
1875             case ICAL_VPOLL_COMPONENT:
1876                 if (cdata->comp_type != CAL_COMP_VPOLL) reject = 1;
1877                 break;
1878             default:
1879                 break;
1880             }
1881 
1882             /* Don't allow ORGANIZER to be changed */
1883             if (!reject && cdata->organizer) {
1884                 prop =
1885                     icalcomponent_get_first_property(comp,
1886                                                      ICAL_ORGANIZER_PROPERTY);
1887                 if (prop) {
1888                     const char *organizer =
1889                         organizer = icalproperty_get_organizer(prop);
1890                     if (organizer) {
1891                         if (!strncasecmp(organizer, "mailto:", 7)) organizer += 7;
1892                         if (strcasecmp(cdata->organizer, organizer)) reject = 1;
1893                     }
1894                 }
1895             }
1896 
1897             if (reject) {
1898                 sched_data->status = sched_data->ischedule ?
1899                     REQSTAT_REJECTED : SCHEDSTAT_REJECTED;
1900                 goto done;
1901             }
1902         }
1903     }
1904 
1905     switch (method) {
1906     case ICAL_METHOD_CANCEL:
1907         /* Get component type */
1908         comp = icalcomponent_get_first_real_component(ical);
1909         kind = icalcomponent_isa(comp);
1910 
1911         /* Set STATUS:CANCELLED on all components */
1912         do {
1913             icalcomponent_set_status(comp, ICAL_STATUS_CANCELLED);
1914             icalcomponent_set_sequence(comp,
1915                                        icalcomponent_get_sequence(comp)+1);
1916         } while ((comp = icalcomponent_get_next_component(ical, kind)));
1917 
1918         break;
1919 
1920     case ICAL_METHOD_REPLY:
1921         attendee = deliver_merge_reply(ical, sched_data->itip);
1922 
1923         break;
1924 
1925     case ICAL_METHOD_REQUEST:
1926         deliver_inbox = deliver_merge_request(recipient,
1927                                               ical, sched_data->itip);
1928         break;
1929 
1930     case ICAL_METHOD_POLLSTATUS:
1931         deliver_inbox = deliver_merge_pollstatus(ical, sched_data->itip);
1932         break;
1933 
1934     default:
1935         /* Unknown METHOD -- ignore it */
1936         syslog(LOG_ERR, "Unknown iTIP method: %s",
1937                icalenum_method_to_string(method));
1938 
1939         sched_data->is_reply = 0;
1940         goto inbox;
1941     }
1942 
1943     /* Create header cache */
1944     txn.req_hdrs = spool_new_hdrcache();
1945     if (!txn.req_hdrs) r = HTTP_SERVER_ERROR;
1946 
1947     /* Store the (updated) object in the recipients's calendar */
1948     strarray_t recipient_addresses = STRARRAY_INITIALIZER;
1949     strarray_append(&recipient_addresses, recipient);
1950     if (!r) r = caldav_store_resource(&txn, ical, mailbox,
1951                                       buf_cstring(&resource), cdata->dav.createdmodseq,
1952                                       caldavdb, NEW_STAG, recipient, &recipient_addresses);
1953     strarray_fini(&recipient_addresses);
1954 
1955     if (r == HTTP_CREATED || r == HTTP_NO_CONTENT) {
1956         sched_data->status =
1957             sched_data->ischedule ? REQSTAT_SUCCESS : SCHEDSTAT_DELIVERED;
1958     }
1959     else {
1960         syslog(LOG_ERR, "caldav_store_resource(%s) failed: %s (%s)",
1961                mailbox->name, error_message(r), txn.error.resource);
1962         sched_data->status =
1963             sched_data->ischedule ? REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
1964         goto done;
1965     }
1966 
1967   inbox:
1968     if (deliver_inbox) {
1969         /* Create a name for the new iTIP message resource */
1970         buf_reset(&resource);
1971         buf_printf(&resource, "%s.ics", makeuuid());
1972 
1973         /* Store the message in the recipient's Inbox */
1974         r = caldav_store_resource(&txn, sched_data->itip, inbox,
1975                                   buf_cstring(&resource), 0, caldavdb, 0, NULL, NULL);
1976         /* XXX  What do we do if storing to Inbox fails? */
1977     }
1978 
1979     /* XXX  Should this be a config option? - it might have perf implications */
1980     if (sched_data->is_reply) {
1981         /* Send updates to attendees - skipping sender of reply */
1982         comp = icalcomponent_get_first_real_component(ical);
1983         if (icalcomponent_isa(comp) == ICAL_VPOLL_COMPONENT)
1984             sched_pollstatus(recipient, sparam, ical, attendee);
1985         else
1986             sched_request(userid, NULL, recipient, NULL, ical); // oldical?
1987     }
1988 
1989   done:
1990     if (ical) icalcomponent_free(ical);
1991     mailbox_close(&inbox);
1992     mailbox_close(&mailbox);
1993     if (caldavdb) caldav_close(caldavdb);
1994     spool_free_hdrcache(txn.req_hdrs);
1995     buf_free(&txn.buf);
1996     free(mailboxname);
1997 }
1998 
1999 
2000 /* Deliver scheduling object to recipient's Inbox */
sched_deliver(const char * sender,const char * recipient,void * data,void * rock)2001 void sched_deliver(const char *sender, const char *recipient, void *data, void *rock)
2002 {
2003     struct sched_data *sched_data = (struct sched_data *) data;
2004     struct auth_state *authstate = (struct auth_state *) rock;
2005     struct caldav_sched_param sparam;
2006     int islegal;
2007 
2008     syslog(LOG_DEBUG, "sched_deliver(%s)", recipient);
2009 
2010     memset(&sparam, 0, sizeof(struct caldav_sched_param));
2011 
2012     /* Check SCHEDULE-FORCE-SEND value */
2013     switch (sched_data->force_send) {
2014     case ICAL_SCHEDULEFORCESEND_NONE:
2015         islegal = 1;
2016         break;
2017 
2018     case ICAL_SCHEDULEFORCESEND_REPLY:
2019         islegal = sched_data->is_reply;
2020         break;
2021 
2022     case ICAL_SCHEDULEFORCESEND_REQUEST:
2023         islegal = !sched_data->is_reply;
2024         break;
2025 
2026     default:
2027         islegal = 0;
2028         break;
2029     }
2030 
2031     if (!islegal) {
2032         sched_data->status = SCHEDSTAT_PARAM;
2033         return;
2034     }
2035 
2036     if (caladdress_lookup(recipient, &sparam, sched_data->schedule_addresses)) {
2037         sched_data->status =
2038             sched_data->ischedule ? REQSTAT_NOUSER : SCHEDSTAT_NOUSER;
2039         /* Unknown user */
2040         goto done;
2041     }
2042 
2043     /* don't schedule to yourself */
2044     if (sparam.isyou) goto done;
2045 
2046     if (sparam.flags) {
2047         /* Remote recipient */
2048         syslog(LOG_NOTICE, "%s scheduling delivery to %s",
2049                (sparam.flags & SCHEDTYPE_ISCHEDULE) ? "iSchedule" : "iMIP",
2050                recipient);
2051 
2052         sched_deliver_remote(sender, recipient, &sparam, sched_data);
2053     }
2054     else {
2055         /* Local recipient */
2056         syslog(LOG_NOTICE, "CalDAV scheduling delivery to %s", recipient);
2057 
2058         sched_deliver_local(sender, recipient, &sparam, sched_data, authstate);
2059     }
2060 
2061 done:
2062     sched_param_fini(&sparam);
2063 }
2064 
2065 /*
2066  * sched_request/reply() helper function
2067  *
2068  * Update DTSTAMP, remove VALARMs, remove SCHEDULE-* parameters
2069  */
clean_component(icalcomponent * comp)2070 static void clean_component(icalcomponent *comp)
2071 {
2072     icalcomponent *alarm, *next;
2073     icalproperty *prop;
2074 
2075     /* Replace DTSTAMP on component */
2076     prop = icalcomponent_get_first_property(comp, ICAL_DTSTAMP_PROPERTY);
2077     if (!prop) {
2078         prop = icalproperty_new(ICAL_DTSTAMP_PROPERTY);
2079         icalcomponent_add_property(comp, prop);
2080     }
2081     icalproperty_set_dtstamp(prop, icaltime_current_time_with_zone(utc_zone));
2082 
2083     /* Remove any VALARM components */
2084     for (alarm = icalcomponent_get_first_component(comp, ICAL_VALARM_COMPONENT);
2085          alarm; alarm = next) {
2086         next = icalcomponent_get_next_component(comp, ICAL_VALARM_COMPONENT);
2087         icalcomponent_remove_component(comp, alarm);
2088         icalcomponent_free(alarm);
2089     }
2090 
2091     /* Grab the organizer */
2092     prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
2093 
2094     /* Remove CalDAV Scheduling parameters from organizer */
2095     icalproperty_remove_parameter_by_name(prop, "SCHEDULE-AGENT");
2096     icalproperty_remove_parameter_by_name(prop, "SCHEDULE-FORCE-SEND");
2097 
2098     /* Remove CalDAV Scheduling parameters from attendees */
2099     for (prop = icalcomponent_get_first_invitee(comp);
2100          prop;
2101          prop = icalcomponent_get_next_invitee(comp)) {
2102         icalproperty_remove_parameter_by_name(prop, "SCHEDULE-AGENT");
2103         icalproperty_remove_parameter_by_name(prop, "SCHEDULE-STATUS");
2104         icalproperty_remove_parameter_by_name(prop, "SCHEDULE-FORCE-SEND");
2105     }
2106 }
2107 
2108 /*****************************************************************************/
2109 
2110 /*
2111  * Compare the properties of the given kind in two components.
2112  * Returns 0 if equal, 1 otherwise.
2113  *
2114  * If the property exists in neither comp, then they are equal.
2115  * If the property exists in only 1 comp, then they are not equal.
2116  * if the property is RDATE or EXDATE, create an XORed CRC32 of all
2117  *   property strings for each component (order irrelevant) and compare the CRCs.
2118  * Otherwise compare the two property strings.
2119  */
propcmp(icalcomponent * oldical,icalcomponent * newical,icalproperty_kind kind)2120 static unsigned propcmp(icalcomponent *oldical, icalcomponent *newical,
2121                         icalproperty_kind kind)
2122 {
2123     icalproperty *oldprop = icalcomponent_get_first_property(oldical, kind);
2124     icalproperty *newprop = icalcomponent_get_first_property(newical, kind);
2125 
2126     if (!oldprop) return (newprop != NULL);
2127     else if (!newprop) return 1;
2128     else if (kind == ICAL_DURATION_PROPERTY) {
2129         struct icaldurationtype olddur = icalproperty_get_duration(oldprop);
2130         struct icaldurationtype newdur = icalproperty_get_duration(newprop);
2131 
2132         return (icaldurationtype_as_int(olddur) != icaldurationtype_as_int(newdur));
2133     }
2134     else if ((kind == ICAL_RDATE_PROPERTY) || (kind == ICAL_EXDATE_PROPERTY)) {
2135         const char *str;
2136         uint32_t old_crc = 0, new_crc = 0;
2137 
2138         do {
2139             str = icalproperty_get_value_as_string(oldprop);
2140             if (str) old_crc ^= crc32_cstring(str);
2141         } while ((oldprop = icalcomponent_get_next_property(oldical, kind)));
2142 
2143         do {
2144             str = icalproperty_get_value_as_string(newprop);
2145             if (str) new_crc ^= crc32_cstring(str);
2146         } while ((newprop = icalcomponent_get_next_property(newical, kind)));
2147 
2148         return (old_crc != new_crc);
2149     }
2150     else {
2151         return (strcmpsafe(icalproperty_get_value_as_string(oldprop),
2152                            icalproperty_get_value_as_string(newprop)) != 0);
2153     }
2154 }
2155 
2156 /*
2157  * sched_request() helper function
2158  *
2159  * Process all attendees in the given component and add them
2160  * to the request data
2161  */
add_attendees(icalcomponent * ical,const char * organizer,strarray_t * attendees)2162 static void add_attendees(icalcomponent *ical,
2163                           const char *organizer, strarray_t *attendees)
2164 {
2165     if (!ical) return;
2166 
2167     icalcomponent *comp = icalcomponent_get_first_real_component(ical);
2168 
2169     /* if no organizer, this isn't a scheduling resource, so nothing else to do */
2170     if (!icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY))
2171         return;
2172 
2173     icalcomponent_kind kind = icalcomponent_isa(comp);
2174 
2175     for (; comp; comp = icalcomponent_get_next_component(ical, kind)) {
2176         icalproperty *prop;
2177         icalparameter *param;
2178         for (prop = icalcomponent_get_first_invitee(comp);
2179             prop;
2180             prop = icalcomponent_get_next_invitee(comp)) {
2181 
2182             const char *attendee = icalproperty_get_invitee(prop);
2183             if (!attendee) continue;
2184 
2185             if (!strncasecmp(attendee, "mailto:", 7)) attendee += 7;
2186 
2187             /* Skip where attendee == organizer */
2188             if (!strcasecmp(attendee, organizer)) continue;
2189 
2190             /* Skip where not the server's responsibility */
2191             param = icalproperty_get_scheduleagent_parameter(prop);
2192             if (param) {
2193                 icalparameter_scheduleagent agent =
2194                     icalparameter_get_scheduleagent(param);
2195                 if (agent != ICAL_SCHEDULEAGENT_SERVER) continue;
2196             }
2197 
2198             strarray_add_case(attendees, attendee);
2199         }
2200     }
2201 }
2202 
find_attendee(icalcomponent * comp,const char * match)2203 static icalproperty *find_attendee(icalcomponent *comp, const char *match)
2204 {
2205     if (!comp) return NULL;
2206 
2207     icalproperty *prop = icalcomponent_get_first_invitee(comp);
2208 
2209     for (; prop; prop = icalcomponent_get_next_invitee(comp)) {
2210         const char *attendee = icalproperty_get_invitee(prop);
2211         if (!attendee) continue;
2212         if (!strncasecmp(attendee, "mailto:", 7)) attendee += 7;
2213 
2214         /* Skip where not the server's responsibility */
2215         icalparameter *param = icalproperty_get_scheduleagent_parameter(prop);
2216         if (param) {
2217             icalparameter_scheduleagent agent =
2218                 icalparameter_get_scheduleagent(param);
2219             if (agent != ICAL_SCHEDULEAGENT_SERVER) continue;
2220         }
2221 
2222         if (!strcasecmp(attendee, match)) return prop;
2223     }
2224 
2225     return NULL;
2226 }
2227 
find_component(icalcomponent * ical,const char * match)2228 static icalcomponent *find_component(icalcomponent *ical, const char *match)
2229 {
2230     if (!ical) return NULL;
2231 
2232     icalcomponent *comp = icalcomponent_get_first_real_component(ical);
2233 
2234     icalcomponent_kind kind = icalcomponent_isa(comp);
2235 
2236     for (; comp; comp = icalcomponent_get_next_component(ical, kind)) {
2237         icalproperty *prop =
2238             icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
2239         const char *recurid = "";
2240         if (prop) recurid = icalproperty_get_value_as_string(prop);
2241         if (!strcmpsafe(recurid, match)) return comp;
2242     }
2243 
2244     return NULL;
2245 }
2246 
find_attended_component(icalcomponent * ical,const char * recurid,const char * attendee)2247 static icalcomponent *find_attended_component(icalcomponent *ical,
2248                                               const char *recurid,
2249                                               const char *attendee)
2250 {
2251     icalcomponent *comp = find_component(ical, recurid);
2252     if (icalcomponent_get_status(comp) == ICAL_STATUS_CANCELLED) {
2253         /* Can't attend a cancelled event */
2254         return NULL;
2255     }
2256     if (find_attendee(comp, attendee))
2257         return comp;
2258     return NULL;
2259 }
2260 
has_exdate(icalcomponent * ical,struct icaltimetype test)2261 static int has_exdate(icalcomponent *ical, struct icaltimetype test)
2262 {
2263     if (!ical) return 0;
2264 
2265     icalproperty *prop =
2266         icalcomponent_get_first_property(ical, ICAL_EXDATE_PROPERTY);
2267     for (; prop;
2268          prop = icalcomponent_get_next_property(ical, ICAL_EXDATE_PROPERTY)) {
2269         struct icaltimetype exdate = icalproperty_get_exdate(prop);
2270         if (!icaltime_compare(exdate, test)) return 1;
2271     }
2272 
2273     return 0;
2274 }
2275 
check_changes_any(icalcomponent * old,icalcomponent * comp,int * needs_actionp)2276 static int check_changes_any(icalcomponent *old,
2277                              icalcomponent *comp, int *needs_actionp)
2278 {
2279     if (!old) {
2280         if (needs_actionp) *needs_actionp = 1;
2281         return 1;
2282     }
2283 
2284     int is_changed = 0;
2285     int needs_action = 0;
2286 
2287     /* Per RFC 6638, Section 3.2.8: We need to compare
2288        DTSTART, DTEND, DURATION, DUE, RRULE, RDATE, EXDATE */
2289     if (propcmp(old, comp, ICAL_DTSTART_PROPERTY))
2290         needs_action = 1;
2291     else if (propcmp(old, comp, ICAL_DTEND_PROPERTY))
2292         needs_action = 1;
2293     else if (propcmp(old, comp, ICAL_DURATION_PROPERTY))
2294         needs_action = 1;
2295     else if (propcmp(old, comp, ICAL_DUE_PROPERTY))
2296         needs_action = 1;
2297     else if (propcmp(old, comp, ICAL_RRULE_PROPERTY))
2298         needs_action = 1;
2299     else if (propcmp(old, comp, ICAL_RDATE_PROPERTY))
2300         needs_action = 1;
2301     else if (propcmp(old, comp, ICAL_EXDATE_PROPERTY))
2302         needs_action = 1;
2303 
2304     if (needs_action)
2305         is_changed = 1;
2306     else if (propcmp(old, comp, ICAL_SUMMARY_PROPERTY))
2307         is_changed = 1;
2308     else if (propcmp(old, comp, ICAL_LOCATION_PROPERTY))
2309         is_changed = 1;
2310     else if (propcmp(old, comp, ICAL_DESCRIPTION_PROPERTY))
2311         is_changed = 1;
2312     else if (propcmp(old, comp, ICAL_POLLWINNER_PROPERTY))
2313         is_changed = 1;
2314     else if (partstat_changed(old, comp, get_organizer(comp)))
2315         is_changed = 1;
2316 
2317     if (needs_actionp) *needs_actionp = needs_action;
2318 
2319     return is_changed;
2320 }
2321 
check_changes(icalcomponent * old,icalcomponent * comp,const char * attendee)2322 static int check_changes(icalcomponent *old, icalcomponent *comp, const char *attendee)
2323 {
2324     int needs_action = 0;
2325     int res = check_changes_any(old, comp, &needs_action);
2326     if (needs_action) {
2327         /* Make sure SEQUENCE is set properly */
2328         int oldseq = icalcomponent_get_sequence(old);
2329         int newseq = icalcomponent_get_sequence(comp);
2330         if (oldseq >= newseq) icalcomponent_set_sequence(comp, oldseq + 1);
2331         icalproperty *prop = find_attendee(comp, attendee);
2332         if (prop) {
2333             icalparameter *param =
2334                 icalparameter_new_partstat(ICAL_PARTSTAT_NEEDSACTION);
2335             icalproperty_set_parameter(prop, param);
2336         }
2337     }
2338     return res;
2339 }
2340 
make_itip(icalproperty_method method,icalcomponent * ical)2341 icalcomponent *make_itip(icalproperty_method method, icalcomponent *ical)
2342 {
2343     /* Create a shell for our iTIP request objects */
2344     icalcomponent *req = icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT,
2345                                              icalproperty_new_version("2.0"),
2346                                              icalproperty_new_prodid(ical_prodid),
2347                                              icalproperty_new_method(method),
2348                                              0);
2349 
2350     /* XXX  Make sure SEQUENCE is incremented */
2351 
2352     /* Copy over any CALSCALE property */
2353     icalproperty *prop =
2354         icalcomponent_get_first_property(ical, ICAL_CALSCALE_PROPERTY);
2355     if (prop) {
2356         icalcomponent_add_property(req, icalproperty_clone(prop));
2357     }
2358 
2359     /* Copy over any VTIMEZONE components */
2360     icalcomponent *comp;
2361     for (comp = icalcomponent_get_first_component(ical, ICAL_VTIMEZONE_COMPONENT);
2362          comp;
2363          comp = icalcomponent_get_next_component(ical, ICAL_VTIMEZONE_COMPONENT)) {
2364          icalcomponent_add_component(req, icalcomponent_clone(comp));
2365     }
2366 
2367     return req;
2368 }
2369 
schedule_set_exdate(icalcomponent * master,icalcomponent * this)2370 static void schedule_set_exdate(icalcomponent *master, icalcomponent *this)
2371 {
2372     icalproperty *recurid, *exdate;
2373     struct icaltimetype exdt;
2374     icalparameter *param;
2375 
2376     /* Fetch the RECURRENCE-ID and use it to create a new EXDATE */
2377     recurid = icalcomponent_get_first_property(this, ICAL_RECURRENCEID_PROPERTY);
2378     exdt = icalproperty_get_recurrenceid(recurid);
2379     exdate = icalproperty_new_exdate(exdt);
2380 
2381     /* Copy any parameters from RECURRENCE-ID to EXDATE */
2382     param = icalproperty_get_first_parameter(recurid, ICAL_TZID_PARAMETER);
2383     if (param) {
2384         icalproperty_add_parameter(exdate, icalparameter_clone(param));
2385     }
2386     param = icalproperty_get_first_parameter(recurid, ICAL_VALUE_PARAMETER);
2387     if (param) {
2388         icalproperty_add_parameter(exdate, icalparameter_clone(param));
2389     }
2390 
2391     /* XXX  Need to handle RANGE parameter */
2392 
2393     /* Add the EXDATE */
2394     icalcomponent_add_property(master, exdate);
2395 }
2396 
2397 /* we've already tested that master contains this attendee */
update_attendee_status(icalcomponent * ical,strarray_t * onrecurids,const char * onattendee,const char * status)2398 static void update_attendee_status(icalcomponent *ical, strarray_t *onrecurids,
2399                                    const char *onattendee, const char *status)
2400 {
2401     icalcomponent *comp = icalcomponent_get_first_real_component(ical);
2402     icalcomponent_kind kind = icalcomponent_isa(comp);
2403 
2404     for (; comp; comp = icalcomponent_get_next_component(ical, kind)) {
2405         if (onrecurids) {
2406             /* this recurrenceid is attended by this attendee in the new data?
2407             * there's nothing to cancel */
2408             icalproperty *prop =
2409                 icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
2410             const char *recurid = "";
2411             if (prop) recurid = icalproperty_get_value_as_string(prop);
2412             if (strarray_find(onrecurids, recurid, 0) < 0) continue;
2413         }
2414 
2415         icalproperty *prop = icalcomponent_get_first_invitee(comp);
2416         for (; prop; prop = icalcomponent_get_next_invitee(comp)) {
2417             const char *attendee = icalproperty_get_invitee(prop);
2418             if (!attendee) continue;
2419             if (!strncasecmp(attendee, "mailto:", 7)) attendee += 7;
2420 
2421             /* skip attendees other than the one we're updating */
2422             if (onattendee && strcasecmp(attendee, onattendee)) continue;
2423 
2424             /* mark the status */
2425             icalparameter *param = icalparameter_new_schedulestatus(status);
2426             icalproperty_set_parameter(prop, param);
2427         }
2428     }
2429 }
2430 
get_historical_cutoff()2431 static icaltimetype get_historical_cutoff()
2432 {
2433     int age = config_getduration(IMAPOPT_CALDAV_HISTORICAL_AGE, 'd');
2434     icaltimetype cutoff;
2435 
2436     if (age < 0) return icaltime_null_time();
2437 
2438     /* Set cutoff to current time -age days */
2439     cutoff = icaltime_current_time_with_zone(icaltimezone_get_utc_timezone());
2440     icaltime_adjust(&cutoff, 0, 0, 0, -age);
2441 
2442     return cutoff;
2443 }
2444 
icalcomponent_is_historical(icalcomponent * comp,icaltimetype cutoff)2445 static int icalcomponent_is_historical(icalcomponent *comp, icaltimetype cutoff)
2446 {
2447     if (icaltime_is_null_time(cutoff)) return 0;
2448 
2449     icalcomponent_kind kind = icalcomponent_isa(comp);
2450     struct icalperiodtype span;
2451 
2452     if (icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY)) {
2453          /* span is just the span of the override */
2454         span = icalcomponent_get_utc_timespan(comp, kind, NULL);
2455     }
2456     else {
2457         /* span is entire span of the master */
2458         icalcomponent *ical = icalcomponent_new_vcalendar();
2459 
2460         icalcomponent_add_component(ical, icalcomponent_clone(comp));
2461         span = icalrecurrenceset_get_utc_timespan(ical, kind, NULL, NULL, NULL, NULL);
2462         icalcomponent_free(ical);
2463     }
2464 
2465     return (icaltime_compare(span.end, cutoff) < 0);
2466 }
2467 
schedule_full_cancel(const strarray_t * schedule_addresses,const char * organizer,const char * attendee,icalcomponent * mastercomp,icaltimetype h_cutoff,icalcomponent * oldical,icalcomponent * newical)2468 static void schedule_full_cancel(const strarray_t *schedule_addresses,
2469                                  const char *organizer, const char *attendee,
2470                                  icalcomponent *mastercomp, icaltimetype h_cutoff,
2471                                  icalcomponent *oldical, icalcomponent *newical)
2472 {
2473     /* we need to send a cancel for all recurrences with this attendee,
2474        and add exdates to the master for all without this attendee */
2475     icalcomponent *itip = make_itip(ICAL_METHOD_CANCEL, oldical);
2476 
2477     icalcomponent *mastercopy = icalcomponent_clone(mastercomp);
2478     clean_component(mastercopy);
2479     icalcomponent_set_status(mastercopy, ICAL_STATUS_CANCELLED);
2480     icalcomponent_add_component(itip, mastercopy);
2481 
2482     int do_send = !icalcomponent_is_historical(mastercopy, h_cutoff);
2483 
2484     icalcomponent *comp = icalcomponent_get_first_real_component(oldical);
2485     icalcomponent_kind kind = icalcomponent_isa(comp);
2486 
2487     for (; comp; comp = icalcomponent_get_next_component(oldical, kind)) {
2488         icalproperty *prop =
2489             icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
2490         if (!prop) continue; /* skip master */
2491         const char *recurid = icalproperty_get_value_as_string(prop);
2492 
2493         /* non matching are exdates on the master */
2494         if (!find_attendee(comp, attendee)) {
2495             schedule_set_exdate(mastercopy, comp);
2496             continue;
2497         }
2498 
2499         icalcomponent *newcomp =
2500             find_attended_component(newical, recurid, attendee);
2501         if (newcomp) continue; /* will be scheduled separately */
2502 
2503         icalcomponent *copy = icalcomponent_clone(comp);
2504         clean_component(copy);
2505         icalcomponent_set_status(copy, ICAL_STATUS_CANCELLED);
2506         icalcomponent_add_component(itip, copy);
2507 
2508         if (!do_send && !icalcomponent_is_historical(copy, h_cutoff))
2509             do_send = 1;
2510     }
2511 
2512     if (do_send) {
2513         struct sched_data sched =
2514             { 0, 0, 0, itip, oldical, newical, ICAL_SCHEDULEFORCESEND_NONE, schedule_addresses, NULL };
2515         sched_deliver(organizer, attendee, &sched, httpd_authstate);
2516     }
2517 
2518     icalcomponent_free(itip);
2519 }
2520 
2521 /* we've already tested that master does NOT contain this attendee */
schedule_sub_cancels(const strarray_t * schedule_addresses,const char * organizer,const char * attendee,icaltimetype h_cutoff,icalcomponent * oldical,icalcomponent * newical)2522 static void schedule_sub_cancels(const strarray_t *schedule_addresses,
2523                                  const char *organizer, const char *attendee,
2524                                  icaltimetype h_cutoff,
2525                                  icalcomponent *oldical, icalcomponent *newical)
2526 {
2527     if (!oldical) return;
2528 
2529     /* we have to create this upfront because it walks the components too */
2530     icalcomponent *itip = make_itip(ICAL_METHOD_CANCEL, oldical);
2531 
2532     icalcomponent *comp = icalcomponent_get_first_real_component(oldical);
2533     icalcomponent_kind kind = icalcomponent_isa(comp);
2534 
2535     int do_send = 0;
2536 
2537     for (; comp; comp = icalcomponent_get_next_component(oldical, kind)) {
2538         icalproperty *prop =
2539             icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
2540         if (!prop) continue;
2541         const char *recurid = icalproperty_get_value_as_string(prop);
2542 
2543         /* we're not attending, there's nothing to cancel */
2544         if (!find_attendee(comp, attendee))
2545             continue;
2546 
2547         /* this recurrenceid is attended by this attendee in the new data?
2548          * there's nothing to cancel */
2549         if (find_attended_component(newical, recurid, attendee))
2550             continue;
2551 
2552         icalcomponent *copy = icalcomponent_clone(comp);
2553         clean_component(copy);
2554         icalcomponent_set_status(copy, ICAL_STATUS_CANCELLED);
2555         icalcomponent_add_component(itip, copy);
2556 
2557         if (!do_send && !icalcomponent_is_historical(copy, h_cutoff))
2558             do_send = 1;
2559     }
2560 
2561     if (do_send) {
2562         struct sched_data sched =
2563             { 0, 0, 0, itip, oldical, newical, ICAL_SCHEDULEFORCESEND_NONE, schedule_addresses, NULL };
2564         sched_deliver(organizer, attendee, &sched, httpd_authstate);
2565 
2566     }
2567 
2568     icalcomponent_free(itip);
2569 }
2570 
get_forcesend(icalproperty * prop)2571 icalparameter_scheduleforcesend get_forcesend(icalproperty *prop)
2572 {
2573     icalparameter *param = icalproperty_get_scheduleforcesend_parameter(prop);
2574     if (!param) return ICAL_SCHEDULEFORCESEND_NONE;
2575     return icalparameter_get_scheduleforcesend(param);
2576 }
2577 
2578 
2579 
2580 /* we've already tested that master does NOT contain this attendee or that
2581  * master doesn't need to be scheduled */
schedule_sub_updates(const strarray_t * schedule_addresses,const char * organizer,const char * attendee,icaltimetype h_cutoff,icalcomponent * oldical,icalcomponent * newical)2582 static void schedule_sub_updates(const strarray_t *schedule_addresses,
2583                                  const char *organizer, const char *attendee,
2584                                  icaltimetype h_cutoff,
2585                                  icalcomponent *oldical, icalcomponent *newical)
2586 {
2587     if (!newical) return;
2588 
2589     icalcomponent *itip = make_itip(ICAL_METHOD_REQUEST, newical);
2590     strarray_t recurids = STRARRAY_INITIALIZER;
2591     icalparameter_scheduleforcesend force_send = ICAL_SCHEDULEFORCESEND_NONE;
2592     int is_update = 0;
2593 
2594     icalcomponent *oldmaster = find_attended_component(oldical, "", attendee);
2595 
2596     icalcomponent *comp = icalcomponent_get_first_real_component(newical);
2597     icalcomponent_kind kind = icalcomponent_isa(comp);
2598 
2599     int do_send = 0;
2600 
2601     for (; comp; comp = icalcomponent_get_next_component(newical, kind)) {
2602         icalproperty *prop =
2603             icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
2604         if (!prop) continue;
2605         const char *recurid = icalproperty_get_value_as_string(prop);
2606 
2607         /* we're not attending, nothing to do */
2608         icalproperty *att = find_attendee(comp, attendee);
2609         if (!att) continue;
2610         force_send = get_forcesend(att);
2611 
2612         icalcomponent *freeme = NULL;
2613 
2614         /* this recurrenceid was in the old data? if not we need to
2615          * generate a synthetic one */
2616         icalcomponent *oldcomp = find_component(oldical, recurid);
2617         if (!oldcomp && oldmaster) {
2618             oldcomp = freeme = master_to_recurrence(oldmaster, prop);
2619         }
2620 
2621         /* unchanged event - we don't need to send anything */
2622         if (!check_changes(oldcomp, comp, attendee)) {
2623             if (force_send == ICAL_SCHEDULEFORCESEND_NONE) {
2624                 if (freeme) icalcomponent_free(freeme);
2625                 continue;
2626             }
2627         }
2628 
2629         icalcomponent *copy = icalcomponent_clone(comp);
2630         clean_component(copy);
2631 
2632         if (find_attendee(oldcomp, attendee))
2633             is_update = 1;
2634 
2635         icalcomponent_add_component(itip, copy);
2636 
2637         strarray_add(&recurids, recurid);
2638 
2639         if (!do_send && !icalcomponent_is_historical(copy, h_cutoff))
2640             do_send = 1;
2641 
2642         if (freeme) icalcomponent_free(freeme);
2643     }
2644 
2645     if (do_send) {
2646         struct sched_data sched =
2647             { 0, 0, is_update, itip, oldical, newical, force_send, schedule_addresses, NULL };
2648         sched_deliver(organizer, attendee, &sched, httpd_authstate);
2649         update_attendee_status(newical, &recurids, attendee, sched.status);
2650     }
2651 
2652     icalcomponent_free(itip);
2653     strarray_fini(&recurids);
2654 }
2655 
2656 /* we've already tested that master does contain this attendee */
schedule_full_update(const strarray_t * schedule_addresses,const char * organizer,const char * attendee,icalcomponent * mastercomp,icaltimetype h_cutoff,icalcomponent * oldical,icalcomponent * newical)2657 static void schedule_full_update(const strarray_t *schedule_addresses,
2658                                  const char *organizer, const char *attendee,
2659                                  icalcomponent *mastercomp, icaltimetype h_cutoff,
2660                                  icalcomponent *oldical, icalcomponent *newical)
2661 {
2662     /* create an itip for the complete event */
2663     icalcomponent *itip = make_itip(ICAL_METHOD_REQUEST, newical);
2664 
2665     icalcomponent *mastercopy = icalcomponent_clone(mastercomp);
2666     clean_component(mastercopy);
2667     icalcomponent_add_component(itip, mastercopy);
2668 
2669     int do_send = 0;
2670     int is_update = 0;
2671 
2672     icalcomponent *oldmaster = find_attended_component(oldical, "", attendee);
2673     if (check_changes(oldmaster, mastercopy, attendee)) {
2674         /* we only force the send if the top level event has changed */
2675         if (!icalcomponent_is_historical(mastercopy, h_cutoff)) do_send = 1;
2676 
2677         if (oldmaster) {
2678             is_update = 1;
2679             if (!do_send && !icalcomponent_is_historical(oldmaster, h_cutoff))
2680                 do_send = 1;
2681         }
2682     }
2683 
2684     icalproperty *masteratt = find_attendee(mastercomp, attendee);
2685     icalparameter_scheduleforcesend force_send = get_forcesend(masteratt);
2686 
2687     /* force the matter */
2688     if (force_send != ICAL_SCHEDULEFORCESEND_NONE) do_send = 1;
2689 
2690     icalcomponent *comp = icalcomponent_get_first_real_component(newical);
2691     icalcomponent_kind kind = icalcomponent_isa(comp);
2692     for (; comp; comp = icalcomponent_get_next_component(newical, kind)) {
2693         /* this recurrenceid is attended by this attendee in the old data?
2694          * check if changed */
2695         icalproperty *prop =
2696             icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
2697         if (!prop) continue;
2698         const char *recurid = icalproperty_get_value_as_string(prop);
2699 
2700         /* we can't just use "find_attended_component" here, because a previous
2701          * sub component without this attendee is an old EXDATE for us, while
2702          * no previous sub component means it was just a regular recurrence
2703          * of the master event */
2704         icalcomponent *oldcomp = find_component(oldical, recurid);
2705 
2706         int has_old = !!find_attendee(oldcomp, attendee);
2707         if (has_old) is_update = 1;
2708         if (!oldcomp && oldmaster)
2709             is_update = 1;
2710 
2711         /* non matching are exdates on the master */
2712         if (!find_attendee(comp, attendee)) {
2713             schedule_set_exdate(mastercopy, comp);
2714 
2715             /* different from last time? */
2716             if ((!oldcomp || has_old) &&
2717                 !do_send && !icalcomponent_is_historical(comp, h_cutoff)) {
2718                 do_send = 1;
2719             }
2720             continue;
2721         }
2722 
2723         icalcomponent *copy = icalcomponent_clone(comp);
2724 
2725         /* we don't care if it's changed, just using this for the
2726          * side effect changes to RSVP */
2727         check_changes(oldcomp, copy, attendee);
2728 
2729         clean_component(copy);
2730         icalcomponent_add_component(itip, copy);
2731     }
2732 
2733     if (do_send) {
2734         struct sched_data sched =
2735             { 0, 0, is_update, itip, oldical, newical, force_send, schedule_addresses, NULL };
2736         sched_deliver(organizer, attendee, &sched, httpd_authstate);
2737 
2738         update_attendee_status(newical, NULL, attendee, sched.status);
2739     }
2740     else {
2741         /* just look for sub updates */
2742         schedule_sub_updates(schedule_addresses, organizer, attendee, h_cutoff, oldical, newical);
2743     }
2744 
2745     icalcomponent_free(itip);
2746 }
2747 
2748 /* sched_request() helper
2749  * handles scheduling for a single attendee */
schedule_one_attendee(const strarray_t * schedule_addresses,const char * organizer,const char * attendee,icaltimetype h_cutoff,icalcomponent * oldical,icalcomponent * newical)2750 static void schedule_one_attendee(const strarray_t *schedule_addresses,
2751                                   const char *organizer, const char *attendee,
2752                                   icaltimetype h_cutoff,
2753                                   icalcomponent *oldical, icalcomponent *newical)
2754 {
2755     /* case: this attendee is attending the master event */
2756     icalcomponent *mastercomp;
2757     if ((mastercomp = find_attended_component(newical, "", attendee))) {
2758         schedule_full_update(schedule_addresses, organizer, attendee,
2759                              mastercomp, h_cutoff, oldical, newical);
2760         return;
2761     }
2762 
2763     /* otherwise we need to cancel for each sub event and then we'll still
2764      * send the updates if any */
2765     if ((mastercomp = find_attended_component(oldical, "", attendee))) {
2766         schedule_full_cancel(schedule_addresses, organizer, attendee, mastercomp, h_cutoff, oldical, newical);
2767     }
2768     else {
2769         schedule_sub_cancels(schedule_addresses, organizer, attendee, h_cutoff, oldical, newical);
2770     }
2771 
2772     schedule_sub_updates(schedule_addresses, organizer, attendee, h_cutoff, oldical, newical);
2773 }
2774 
2775 
2776 /* Create and deliver an organizer scheduling request */
sched_request(const char * asuserid,const strarray_t * schedule_addresses,const char * organizer,icalcomponent * oldical,icalcomponent * newical)2777 void sched_request(const char *asuserid, const strarray_t *schedule_addresses,
2778                    const char *organizer,
2779                    icalcomponent *oldical, icalcomponent *newical)
2780 {
2781     /* Check ACL of auth'd user on userid's Scheduling Outbox */
2782     int rights = 0;
2783 
2784     mbentry_t *mbentry = NULL;
2785     char *outboxname = caldav_mboxname(asuserid, SCHED_OUTBOX);
2786     int r = mboxlist_lookup(outboxname, &mbentry, NULL);
2787     if (r) {
2788         syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s",
2789                outboxname, error_message(r));
2790     }
2791     else {
2792         rights = httpd_myrights(httpd_authstate, mbentry);
2793     }
2794     free(outboxname);
2795     mboxlist_entry_free(&mbentry);
2796 
2797     if (!(rights & DACL_INVITE)) {
2798         /* DAV:need-privileges */
2799         syslog(LOG_DEBUG, "No scheduling send ACL for user %s on Outbox %s",
2800                httpd_userid, organizer);
2801 
2802         update_attendee_status(newical, NULL, NULL, SCHEDSTAT_NOPRIVS);
2803 
2804         return;
2805     }
2806 
2807     /* ok, let's figure out who the attendees are */
2808     strarray_t attendees = STRARRAY_INITIALIZER;
2809     add_attendees(oldical, organizer, &attendees);
2810     add_attendees(newical, organizer, &attendees);
2811 
2812     icaltimetype h_cutoff = get_historical_cutoff();
2813 
2814     int i;
2815     for (i = 0; i < strarray_size(&attendees); i++) {
2816         const char *attendee = strarray_nth(&attendees, i);
2817         syslog(LOG_NOTICE, "iTIP scheduling request from %s to %s",
2818                organizer, attendee);
2819         schedule_one_attendee(schedule_addresses, organizer, attendee, h_cutoff, oldical, newical);
2820     }
2821 
2822     strarray_fini(&attendees);
2823 }
2824 
2825 /*******************************************************************/
2826 /* REPLIES */
2827 
2828 struct reply_data {
2829     icalcomponent *itip;
2830     const char *organizer;
2831     strarray_t *didparts;
2832     int master_send;
2833     int do_send;
2834     icalparameter_scheduleforcesend force_send;
2835 };
2836 
2837 
2838 /*
2839  * sched_reply() helper function
2840  *
2841  * Remove all attendees from 'comp' other than the one corresponding to 'match'
2842  */
trim_attendees(icalcomponent * comp,const char * match)2843 static void trim_attendees(icalcomponent *comp, const char *match)
2844 {
2845     icalproperty *prop;
2846     ptrarray_t remove = PTRARRAY_INITIALIZER;
2847 
2848     /* Locate userid in the attendee list (stripping others) */
2849     for (prop = icalcomponent_get_first_invitee(comp);
2850          prop;
2851          prop = icalcomponent_get_next_invitee(comp)) {
2852         const char *attendee = icalproperty_get_invitee(prop);
2853         if (!attendee) continue;
2854         if (!strncasecmp(attendee, "mailto:", 7)) attendee += 7;
2855 
2856         /* keep my attendee */
2857         if (!strcasecmp(attendee, match)) continue;
2858 
2859         /* Some other attendee, remove it */
2860         ptrarray_append(&remove, prop);
2861     }
2862 
2863     int i;
2864     for (i = 0; i < remove.count; i++) {
2865         icalcomponent_remove_invitee(comp, ptrarray_nth(&remove, i));
2866     }
2867     ptrarray_fini(&remove);
2868 }
2869 
2870 
2871 /*
2872  * sched_reply() helper function
2873  *
2874  * Attendee removed this component, mark it as declined for the organizer.
2875  */
reply_mark_declined(icalcomponent * comp)2876 static int reply_mark_declined(icalcomponent *comp)
2877 {
2878     icalproperty *myattendee;
2879     icalparameter *param;
2880 
2881     if (!comp) return 0;
2882 
2883     /* Don't send a decline for cancelled components */
2884     if (icalcomponent_get_status(comp) == ICAL_STATUS_CANCELLED)
2885         return 0;
2886 
2887     myattendee = icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY);
2888 
2889     param = icalparameter_new_partstat(ICAL_PARTSTAT_DECLINED);
2890     icalproperty_set_parameter(myattendee, param);
2891 
2892     return 1;
2893 }
2894 
2895 /* we've already tested that master contains this attendee */
update_organizer_status(icalcomponent * ical,strarray_t * onrecurids,const char * status)2896 static void update_organizer_status(icalcomponent *ical, strarray_t *onrecurids,
2897                                     const char *status)
2898 {
2899     icalcomponent *comp = icalcomponent_get_first_real_component(ical);
2900     icalcomponent_kind kind = icalcomponent_isa(comp);
2901 
2902     for (; comp; comp = icalcomponent_get_next_component(ical, kind)) {
2903         if (onrecurids) {
2904             icalproperty *prop =
2905                 icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
2906             const char *recurid = "";
2907             if (prop) recurid = icalproperty_get_value_as_string(prop);
2908             if (strarray_find(onrecurids, recurid, 0) < 0) continue;
2909         }
2910 
2911         icalproperty *prop =
2912             icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
2913 
2914         /* mark the status */
2915         icalparameter *param = icalparameter_new_schedulestatus(status);
2916         icalproperty_set_parameter(prop, param);
2917     }
2918 }
2919 
get_organizer(icalcomponent * comp)2920 static const char *get_organizer(icalcomponent *comp)
2921 {
2922     icalproperty *prop =
2923         icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
2924     const char *organizer = icalproperty_get_organizer(prop);
2925     if (!organizer) return NULL;
2926     if (!strncasecmp(organizer, "mailto:", 7)) organizer += 7;
2927     icalparameter *param = icalproperty_get_scheduleagent_parameter(prop);
2928     /* check if we're supposed to send replies to the organizer */
2929     if (param &&
2930         icalparameter_get_scheduleagent(param) != ICAL_SCHEDULEAGENT_SERVER)
2931         return NULL;
2932     return organizer;
2933 }
2934 
get_partstat(icalcomponent * comp,const char * attendee)2935 static icalparameter_partstat get_partstat(icalcomponent *comp,
2936                                            const char *attendee)
2937 {
2938     icalproperty *prop = find_attendee(comp, attendee);
2939     if (!prop) return ICAL_PARTSTAT_NEEDSACTION;
2940     icalparameter *param =
2941         icalproperty_get_first_parameter(prop, ICAL_PARTSTAT_PARAMETER);
2942     if (!param) return ICAL_PARTSTAT_NEEDSACTION;
2943     return icalparameter_get_partstat(param);
2944 }
2945 
partstat_changed(icalcomponent * oldcomp,icalcomponent * newcomp,const char * attendee)2946 static int partstat_changed(icalcomponent *oldcomp,
2947                             icalcomponent *newcomp, const char *attendee)
2948 {
2949     if (!attendee) return 1; // something weird is going on, treat it as a change
2950     return (get_partstat(oldcomp, attendee) != get_partstat(newcomp, attendee));
2951 }
2952 
schedule_sub_declines(const char * attendee,icaltimetype h_cutoff,icalcomponent * oldical,icalcomponent * newical,struct reply_data * reply)2953 static void schedule_sub_declines(const char *attendee,
2954                                   icaltimetype h_cutoff,
2955                                   icalcomponent *oldical, icalcomponent *newical,
2956                                   struct reply_data *reply)
2957 {
2958     if (!oldical) return;
2959 
2960     if (!reply->itip)
2961         reply->itip = make_itip(ICAL_METHOD_REPLY, oldical);
2962 
2963     icalcomponent *comp = icalcomponent_get_first_real_component(oldical);
2964     icalcomponent_kind kind = icalcomponent_isa(comp);
2965 
2966     for (; comp; comp = icalcomponent_get_next_component(oldical, kind)) {
2967         icalproperty *prop =
2968             icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
2969         if (!prop) continue;
2970         const char *recurid = icalproperty_get_value_as_string(prop);
2971 
2972         /* we weren't attending, nothing to do */
2973         if (!find_attendee(comp, attendee))
2974             continue;
2975 
2976         /* no organizer, can't do anything */
2977         const char *organizer = get_organizer(comp);
2978         if (!organizer) continue;
2979 
2980         /* this recurrenceid is attended by this attendee in the new data?
2981            don't decline, we've already replied if necessary */
2982         icalcomponent *newcomp =
2983             find_attended_component(newical, recurid, attendee);
2984         if (newcomp) continue;
2985 
2986         /* we need to send an update for this recurrence */
2987         icalcomponent *copy = icalcomponent_clone(comp);
2988         trim_attendees(copy, attendee);
2989         if (kind == ICAL_VPOLL_COMPONENT) sched_vpoll_reply(copy);
2990         clean_component(copy);
2991         reply_mark_declined(copy);
2992 
2993         icalcomponent_add_component(reply->itip, copy);
2994 
2995         if (!reply->do_send && !icalcomponent_is_historical(comp, h_cutoff))
2996             reply->do_send = 1;
2997     }
2998 }
2999 
3000 /* we've already tested that master does NOT contain this attendee */
schedule_sub_replies(const char * attendee,icaltimetype h_cutoff,icalcomponent * oldical,icalcomponent * newical,struct reply_data * reply)3001 static void schedule_sub_replies(const char *attendee,
3002                                  icaltimetype h_cutoff,
3003                                  icalcomponent *oldical, icalcomponent *newical,
3004                                  struct reply_data *reply)
3005 {
3006     if (!newical) return;
3007 
3008     if (!reply->itip) reply->itip = make_itip(ICAL_METHOD_REPLY, newical);
3009 
3010     icalcomponent *comp = icalcomponent_get_first_real_component(newical);
3011     icalcomponent_kind kind = icalcomponent_isa(comp);
3012 
3013     for (; comp; comp = icalcomponent_get_next_component(newical, kind)) {
3014         icalproperty *prop =
3015             icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
3016         if (!prop) continue;
3017         const char *recurid = icalproperty_get_value_as_string(prop);
3018 
3019         /* we're not attending, nothing to do */
3020         if (!find_attendee(comp, attendee))
3021             continue;
3022 
3023         /* no organizer, can't do anything */
3024         const char *organizer = get_organizer(comp);
3025         if (!organizer) continue;
3026 
3027         icalparameter_scheduleforcesend force_send =
3028             get_forcesend(icalcomponent_get_first_property(comp,
3029                                                            ICAL_ORGANIZER_PROPERTY));
3030 
3031         /* this recurrenceid is attended by this attendee in the old data? */
3032         icalcomponent *oldcomp =
3033             find_attended_component(oldical, recurid, attendee);
3034 
3035         /* unchanged partstat - we don't need to send anything */
3036         if (!reply->master_send && !partstat_changed(oldcomp, comp, attendee)) {
3037             if (force_send == ICAL_SCHEDULEFORCESEND_NONE)
3038                 continue;
3039         }
3040 
3041         /* XXX - test for changed between recurrences and error out?  Any point? */
3042         reply->force_send = force_send;
3043         reply->organizer = organizer;
3044 
3045         /* we need to send an update for this recurrence */
3046         icalcomponent *copy = icalcomponent_clone(comp);
3047         trim_attendees(copy, attendee);
3048         if (kind == ICAL_VPOLL_COMPONENT) sched_vpoll_reply(copy);
3049         clean_component(copy);
3050 
3051         icalcomponent_add_component(reply->itip, copy);
3052 
3053         if (!reply->master_send) {
3054             if (!reply->didparts) reply->didparts = strarray_new();
3055             strarray_add(reply->didparts, recurid);
3056         }
3057 
3058         if (!reply->do_send && !icalcomponent_is_historical(comp, h_cutoff))
3059             reply->do_send = 1;
3060     }
3061 }
3062 
schedule_full_decline(const char * attendee,icaltimetype h_cutoff,icalcomponent * oldical,icalcomponent * newical,struct reply_data * reply)3063 static void schedule_full_decline(const char *attendee,
3064                                   icaltimetype h_cutoff,
3065                                   icalcomponent *oldical, icalcomponent *newical __attribute__((unused)),
3066                                   struct reply_data *reply)
3067 {
3068     /* we only get called if newical doesn't have an attended mastercomp */
3069     icalcomponent *mastercomp = find_attended_component(oldical, "", attendee);
3070     if (!mastercomp) return;
3071 
3072     reply->organizer = get_organizer(mastercomp);
3073     if (!reply->organizer) return;
3074 
3075     /* we need to send a reply for this recurrence for sure, because we know that the
3076      * that new master doesn't have this attendee */
3077     if (!reply->itip) reply->itip = make_itip(ICAL_METHOD_REPLY, oldical);
3078 
3079     reply->force_send =
3080         get_forcesend(icalcomponent_get_first_property(mastercomp,
3081                                                        ICAL_ORGANIZER_PROPERTY));
3082 
3083     icalcomponent *mastercopy = icalcomponent_clone(mastercomp);
3084     trim_attendees(mastercopy, attendee);
3085     if (icalcomponent_isa(mastercomp) == ICAL_VPOLL_COMPONENT) sched_vpoll_reply(mastercopy);
3086     clean_component(mastercopy);
3087     reply_mark_declined(mastercopy);
3088 
3089     icalcomponent_add_component(reply->itip, mastercopy);
3090 
3091     if (!reply->do_send && !icalcomponent_is_historical(mastercomp, h_cutoff))
3092         reply->do_send = 1;
3093 
3094     /* force ALL sub parts to be added */
3095     reply->master_send = 1;
3096 }
3097 
3098 /* we've already tested that master contains this attendee */
schedule_full_reply(const char * attendee,icaltimetype h_cutoff,icalcomponent * oldical,icalcomponent * newical,struct reply_data * reply)3099 static void schedule_full_reply(const char *attendee,
3100                                 icaltimetype h_cutoff,
3101                                 icalcomponent *oldical, icalcomponent *newical,
3102                                 struct reply_data *reply)
3103 {
3104     icalcomponent *mastercomp = find_attended_component(newical, "", attendee);
3105     icalcomponent_kind kind;
3106     int add_master = 0;
3107 
3108     if (!mastercomp) {
3109         schedule_full_decline(attendee, h_cutoff, oldical, newical, reply);
3110         return;
3111     }
3112 
3113     reply->organizer = get_organizer(mastercomp);
3114     if (!reply->organizer) return;
3115 
3116     kind = icalcomponent_isa(mastercomp);
3117 
3118     reply->force_send =
3119         get_forcesend(icalcomponent_get_first_property(mastercomp,
3120                                                        ICAL_ORGANIZER_PROPERTY));
3121 
3122     /* calculate if we need to send a reply based on the master event */
3123 
3124     /* it's forced */
3125     if (reply->force_send != ICAL_SCHEDULEFORCESEND_NONE)
3126         add_master = 1;
3127 
3128     /* or it's a VPOLL */
3129     else if (kind == ICAL_VPOLL_COMPONENT)
3130         add_master = 1;
3131 
3132     else {
3133         /* or it's different */
3134         icalcomponent *oldmaster = find_attended_component(oldical, "", attendee);
3135         if (partstat_changed(oldmaster, mastercomp, attendee))
3136             add_master = 1;
3137 
3138         /* or it includes new EXDATEs */
3139         else {
3140             icalproperty *prop =
3141                 icalcomponent_get_first_property(mastercomp, ICAL_EXDATE_PROPERTY);
3142             for (; prop; prop = icalcomponent_get_next_property(mastercomp,
3143                                                                 ICAL_EXDATE_PROPERTY)) {
3144                 struct icaltimetype exdate = icalproperty_get_exdate(prop);
3145                 if (!has_exdate(oldmaster, exdate))
3146                     add_master = 1;
3147             }
3148         }
3149     }
3150 
3151     if (add_master) {
3152         if (!reply->itip) reply->itip = make_itip(ICAL_METHOD_REPLY, newical);
3153 
3154         /* add the master */
3155         icalcomponent *mastercopy = icalcomponent_clone(mastercomp);
3156         trim_attendees(mastercopy, attendee);
3157         if (kind == ICAL_VPOLL_COMPONENT) sched_vpoll_reply(mastercopy);
3158         clean_component(mastercopy);
3159         icalcomponent_add_component(reply->itip, mastercopy);
3160 
3161         /* force ALL sub parts to be added */
3162         reply->master_send = 1;
3163 
3164         /* master includes "recent" occurrence(s) - send it */
3165         if (!icalcomponent_is_historical(mastercopy, h_cutoff))
3166             reply->do_send = 1;
3167     }
3168 }
3169 
3170 /* Create and deliver an attendee scheduling reply */
sched_reply(const char * onuserid,const strarray_t * schedule_addresses,icalcomponent * oldical,icalcomponent * newical)3171 void sched_reply(const char *onuserid, const strarray_t *schedule_addresses,
3172                  icalcomponent *oldical, icalcomponent *newical)
3173 {
3174     /* Check ACL of auth'd user on userid's Scheduling Outbox */
3175     int rights = 0;
3176 
3177     mbentry_t *mbentry = NULL;
3178     char *outboxname = caldav_mboxname(onuserid, SCHED_OUTBOX);
3179     int r = mboxlist_lookup(outboxname, &mbentry, NULL);
3180     if (r) {
3181         syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s",
3182                outboxname, error_message(r));
3183     }
3184     else {
3185         rights = httpd_myrights(httpd_authstate, mbentry);
3186     }
3187     free(outboxname);
3188     mboxlist_entry_free(&mbentry);
3189 
3190     if (!(rights & DACL_REPLY)) {
3191         /* DAV:need-privileges */
3192         syslog(LOG_DEBUG, "No scheduling send ACL for user %s on Outbox %s",
3193                httpd_userid, onuserid);
3194         update_organizer_status(newical, NULL, SCHEDSTAT_NOPRIVS);
3195         return;
3196     }
3197 
3198     /* otherwise we need to decline for each sub event and then we'll still
3199      * send the accepts if any */
3200     icaltimetype h_cutoff = get_historical_cutoff();
3201 
3202     int i;
3203     for (i = 0; i < strarray_size(schedule_addresses); i++) {
3204         const char *attendee = strarray_nth(schedule_addresses, i);
3205         struct reply_data reply = { NULL, NULL, NULL, 0, 0, ICAL_SCHEDULEFORCESEND_NONE };
3206         if (!strncasecmp(attendee, "mailto:", 7)) attendee += 7;
3207 
3208         schedule_full_reply(attendee, h_cutoff, oldical, newical, &reply);
3209         schedule_sub_replies(attendee, h_cutoff, oldical, newical, &reply);
3210         schedule_sub_declines(attendee, h_cutoff, oldical, newical, &reply);
3211 
3212         if (reply.do_send) {
3213             struct sched_data sched =
3214                 { 0, 1, 0, reply.itip, oldical, newical, reply.force_send, schedule_addresses, NULL };
3215             syslog(LOG_NOTICE, "iTIP scheduling reply from %s to %s",
3216                    attendee, reply.organizer ? reply.organizer : "<unknown>");
3217             sched_deliver(attendee, reply.organizer, &sched, httpd_authstate);
3218             update_organizer_status(newical, reply.didparts, sched.status);
3219         }
3220 
3221         if (reply.didparts) strarray_free(reply.didparts);
3222         if (reply.itip) icalcomponent_free(reply.itip);
3223     }
3224 }
3225 
sched_param_fini(struct caldav_sched_param * sparam)3226 void sched_param_fini(struct caldav_sched_param *sparam)
3227 {
3228     free(sparam->userid);
3229     free(sparam->server);
3230 
3231     struct proplist *prop, *next;
3232     for (prop = sparam->props; prop; prop = next) {
3233         next = prop->next;
3234         free(prop);
3235     }
3236 
3237     memset(sparam, 0, sizeof(struct caldav_sched_param));
3238 }
3239 
3240 
get_schedule_addresses(hdrcache_t req_hdrs,const char * mboxname,const char * userid,strarray_t * addresses)3241 void get_schedule_addresses(hdrcache_t req_hdrs, const char *mboxname,
3242                             const char *userid, strarray_t *addresses)
3243 {
3244     struct buf buf = BUF_INITIALIZER;
3245 
3246     /* allow override of schedule-address per-message (FM specific) */
3247     const char **hdr = spool_getheader(req_hdrs, "Schedule-Address");
3248 
3249     if (hdr) {
3250         if (!strncasecmp(hdr[0], "mailto:", 7))
3251             strarray_add(addresses, hdr[0]+7);
3252         else
3253             strarray_add(addresses, hdr[0]);
3254     }
3255     else {
3256         /* find schedule address based on the destination calendar's user */
3257 
3258         /* check calendar-user-address-set for target user's mailbox */
3259         const char *annotname =
3260             DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-user-address-set";
3261         int r = annotatemore_lookupmask(mboxname, annotname,
3262                                         userid, &buf);
3263         if (r || !buf.len) {
3264             /* check calendar-user-address-set for target user's principal */
3265             char *calhomeset = caldav_mboxname(userid, NULL);
3266             buf_reset(&buf);
3267             r = annotatemore_lookupmask(calhomeset, annotname,
3268                                         userid, &buf);
3269             free(calhomeset);
3270         }
3271 
3272         if (!r && buf.len) {
3273             strarray_t *values =
3274                 strarray_split(buf_cstring(&buf), ",", STRARRAY_TRIM);
3275             int i;
3276             for (i = 0; i < strarray_size(values); i++) {
3277                 const char *item = strarray_nth(values, i);
3278                 if (!strncasecmp(item, "mailto:", 7)) item += 7;
3279                 strarray_add(addresses, item);
3280             }
3281             strarray_free(values);
3282         }
3283         else if (strchr(userid, '@')) {
3284             /* userid corresponding to target */
3285             strarray_add(addresses, userid);
3286         }
3287         else {
3288             /* append fully qualified userids */
3289             struct strlist *domains;
3290 
3291             for (domains = cua_domains; domains; domains = domains->next) {
3292                 buf_reset(&buf);
3293                 buf_printf(&buf, "%s@%s", userid, domains->s);
3294 
3295                 strarray_add(addresses, buf_cstring(&buf));
3296             }
3297         }
3298     }
3299 
3300     buf_free(&buf);
3301 }
3302