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• <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