4 * This module implements iCalendar object processing and the Calendar>
5 * room on a Citadel/UX server. It handles iCalendar objects using the
6 * iTIP protocol. See RFCs 2445 and 2446.
12 #include <sys/types.h>
19 #include "serv_calendar.h"
22 #include "citserver.h"
23 #include "sysdep_decls.h"
26 #include "dynloader.h"
31 #include "mime_parser.h"
38 struct ical_respond_data {
39 char desired_partnum[SIZ];
45 * Write a calendar object into the specified user's calendar room.
47 void ical_write_to_cal(struct usersupp *u, icalcomponent *cal) {
52 strcpy(temp, tmpnam(NULL));
53 ser = icalcomponent_as_ical_string(cal);
54 if (ser == NULL) return;
56 /* Make a temp file out of it */
57 fp = fopen(temp, "w");
58 if (fp == NULL) return;
59 fwrite(ser, strlen(ser), 1, fp);
62 /* This handy API function does all the work for us.
64 CtdlWriteObject(USERCALENDARROOM, /* which room */
65 "text/calendar", /* MIME type */
69 0, /* don't delete others of this type */
77 * Add a calendar object to the user's calendar
79 void ical_add(icalcomponent *cal, int recursion_level) {
83 * The VEVENT subcomponent is the one we're interested in saving.
85 if (icalcomponent_isa(cal) == ICAL_VEVENT_COMPONENT) {
87 ical_write_to_cal(&CC->usersupp, cal);
91 /* If the component has subcomponents, recurse through them. */
92 for (c = icalcomponent_get_first_component(cal, ICAL_ANY_COMPONENT);
94 c = icalcomponent_get_next_component(cal, ICAL_ANY_COMPONENT)) {
95 /* Recursively process subcomponent */
96 ical_add(c, recursion_level+1);
104 * Send a reply to a meeting invitation.
106 * 'request' is the invitation to reply to.
107 * 'action' is the string "accept" or "decline".
109 * (Sorry about this being more than 80 columns ... there was just
110 * no easy way to break it down sensibly.)
112 void ical_send_a_reply(icalcomponent *request, char *action) {
113 icalcomponent *the_reply = NULL;
114 icalcomponent *vevent = NULL;
115 icalproperty *attendee = NULL;
116 char attendee_string[SIZ];
117 icalproperty *organizer = NULL;
118 char organizer_string[SIZ];
119 icalproperty *summary = NULL;
120 char summary_string[SIZ];
121 icalproperty *me_attend = NULL;
122 struct recptypes *recp = NULL;
123 icalparameter *partstat = NULL;
124 char *serialized_reply = NULL;
125 char *reply_message_text = NULL;
126 struct CtdlMessage *msg = NULL;
127 struct recptypes *valid = NULL;
129 strcpy(organizer_string, "");
130 strcpy(summary_string, "Calendar item");
132 if (request == NULL) {
133 lprintf(3, "ERROR: trying to reply to NULL event?\n");
137 the_reply = icalcomponent_new_clone(request);
138 if (the_reply == NULL) {
139 lprintf(3, "ERROR: cannot clone request\n");
143 /* Change the method from REQUEST to REPLY */
144 icalcomponent_set_method(the_reply, ICAL_METHOD_REPLY);
146 vevent = icalcomponent_get_first_component(the_reply, ICAL_VEVENT_COMPONENT);
147 if (vevent != NULL) {
148 /* Hunt for attendees, removing ones that aren't us.
149 * (Actually, remove them all, cloning our own one so we can
150 * re-insert it later)
152 while (attendee = icalcomponent_get_first_property(vevent,
153 ICAL_ATTENDEE_PROPERTY), (attendee != NULL)
155 if (icalproperty_get_attendee(attendee)) {
156 strcpy(attendee_string,
157 icalproperty_get_attendee(attendee) );
158 if (!strncasecmp(attendee_string, "MAILTO:", 7)) {
159 strcpy(attendee_string, &attendee_string[7]);
160 striplt(attendee_string);
161 recp = validate_recipients(attendee_string);
163 if (!strcasecmp(recp->recp_local, CC->usersupp.fullname)) {
164 if (me_attend) icalproperty_free(me_attend);
165 me_attend = icalproperty_new_clone(attendee);
172 icalcomponent_remove_property(vevent, attendee);
175 /* We found our own address in the attendee list. */
177 /* Change the partstat from NEEDS-ACTION to ACCEPT or DECLINE */
178 icalproperty_remove_parameter(me_attend, ICAL_PARTSTAT_PARAMETER);
180 if (!strcasecmp(action, "accept")) {
181 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_ACCEPTED);
183 else if (!strcasecmp(action, "decline")) {
184 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_DECLINED);
186 else if (!strcasecmp(action, "tentative")) {
187 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_TENTATIVE);
190 if (partstat) icalproperty_add_parameter(me_attend, partstat);
192 /* Now insert it back into the vevent. */
193 icalcomponent_add_property(vevent, me_attend);
196 /* Figure out who to send this thing to */
197 organizer = icalcomponent_get_first_property(vevent, ICAL_ORGANIZER_PROPERTY);
198 if (organizer != NULL) {
199 if (icalproperty_get_organizer(organizer)) {
200 strcpy(organizer_string,
201 icalproperty_get_organizer(organizer) );
204 if (!strncasecmp(organizer_string, "MAILTO:", 7)) {
205 strcpy(organizer_string, &organizer_string[7]);
206 striplt(organizer_string);
208 strcpy(organizer_string, "");
211 /* Extract the summary string -- we'll use it as the
212 * message subject for the reply
214 summary = icalcomponent_get_first_property(vevent, ICAL_SUMMARY_PROPERTY);
215 if (summary != NULL) {
216 if (icalproperty_get_summary(summary)) {
217 strcpy(summary_string,
218 icalproperty_get_summary(summary) );
224 /* Now generate the reply message and send it out. */
225 serialized_reply = strdoop(icalcomponent_as_ical_string(the_reply));
226 icalcomponent_free(the_reply); /* don't need this anymore */
227 if (serialized_reply == NULL) return;
229 reply_message_text = mallok(strlen(serialized_reply) + SIZ);
230 if (reply_message_text != NULL) {
231 sprintf(reply_message_text,
232 "Content-type: text/calendar\r\n\r\n%s\r\n",
236 msg = CtdlMakeMessage(&CC->usersupp, organizer_string,
237 CC->quickroom.QRname, 0, FMT_RFC822,
239 summary_string, /* Use summary for subject */
243 valid = validate_recipients(organizer_string);
244 CtdlSubmitMsg(msg, valid, "");
245 CtdlFreeMessage(msg);
248 phree(serialized_reply);
254 * Callback function for mime parser that hunts for calendar content types
255 * and turns them into calendar objects
257 void ical_locate_part(char *name, char *filename, char *partnum, char *disp,
258 void *content, char *cbtype, size_t length, char *encoding,
261 struct ical_respond_data *ird = NULL;
263 ird = (struct ical_respond_data *) cbuserdata;
264 if (ird->cal != NULL) {
265 icalcomponent_free(ird->cal);
268 if (strcasecmp(partnum, ird->desired_partnum)) return;
269 ird->cal = icalcomponent_new_from_string(content);
274 * Respond to a meeting request.
276 void ical_respond(long msgnum, char *partnum, char *action) {
277 struct CtdlMessage *msg;
278 struct ical_respond_data ird;
281 (strcasecmp(action, "accept"))
282 && (strcasecmp(action, "decline"))
284 cprintf("%d Action must be 'accept' or 'decline'\n",
285 ERROR + ILLEGAL_VALUE
290 msg = CtdlFetchMessage(msgnum);
292 cprintf("%d Message %ld not found.\n",
299 memset(&ird, 0, sizeof ird);
300 strcpy(ird.desired_partnum, partnum);
301 mime_parser(msg->cm_fields['M'],
303 *ical_locate_part, /* callback function */
305 (void *) &ird, /* user data */
309 /* We're done with the incoming message, because we now have a
310 * calendar object in memory.
312 CtdlFreeMessage(msg);
315 * Here is the real meat of this function. Handle the event.
317 if (ird.cal != NULL) {
318 /* Save this in the user's calendar if necessary */
319 if (!strcasecmp(action, "accept")) {
320 ical_add(ird.cal, 0);
323 /* Send a reply if necessary */
324 if (icalcomponent_get_method(ird.cal) == ICAL_METHOD_REQUEST) {
325 ical_send_a_reply(ird.cal, action);
328 /* Now that we've processed this message, we don't need it
329 * anymore. So delete it.
331 CtdlDeleteMessages(CC->quickroom.QRname, msgnum, "");
333 /* Free the memory we allocated and return a response. */
334 icalcomponent_free(ird.cal);
336 cprintf("%d ok\n", CIT_OK);
340 cprintf("%d No calendar object found\n", ERROR);
344 /* should never get here */
349 * Search for a property in both the top level and in a VEVENT subcomponent
351 icalproperty *ical_ctdl_get_subprop(
353 icalproperty_kind which_prop
358 p = icalcomponent_get_first_property(cal, which_prop);
360 c = icalcomponent_get_first_component(cal,
361 ICAL_VEVENT_COMPONENT);
363 p = icalcomponent_get_first_property(c, which_prop);
371 * Check to see if two events overlap. Returns nonzero if they do.
373 int ical_ctdl_is_overlap(
374 struct icaltimetype t1start,
375 struct icaltimetype t1end,
376 struct icaltimetype t2start,
377 struct icaltimetype t2end
380 if (icaltime_is_null_time(t1start)) return(0);
381 if (icaltime_is_null_time(t2start)) return(0);
383 /* First, check for all-day events */
384 if (t1start.is_date) {
385 if (!icaltime_compare_date_only(t1start, t2start)) {
388 if (!icaltime_is_null_time(t2end)) {
389 if (!icaltime_compare_date_only(t1start, t2end)) {
395 if (t2start.is_date) {
396 if (!icaltime_compare_date_only(t2start, t1start)) {
399 if (!icaltime_is_null_time(t1end)) {
400 if (!icaltime_compare_date_only(t2start, t1end)) {
406 /* Now check for overlaps using date *and* time. */
408 /* First, bail out if either event 1 or event 2 is missing end time. */
409 if (icaltime_is_null_time(t1end)) return(0);
410 if (icaltime_is_null_time(t2end)) return(0);
412 /* If event 1 ends before event 2 starts, we're in the clear. */
413 if (icaltime_compare(t1end, t2start) <= 0) return(0);
415 /* If event 2 ends before event 1 starts, we're also ok. */
416 if (icaltime_compare(t2end, t1start) <= 0) return(0);
418 /* Otherwise, they overlap. */
425 * Backend for ical_hunt_for_conflicts()
427 void ical_hunt_for_conflicts_backend(long msgnum, void *data) {
429 struct CtdlMessage *msg;
430 struct ical_respond_data ird;
431 struct icaltimetype t1start, t1end, t2start, t2end;
433 char conflict_event_uid[SIZ];
434 char conflict_event_summary[SIZ];
435 char compare_uid[SIZ];
437 cal = (icalcomponent *)data;
438 strcpy(compare_uid, "");
439 strcpy(conflict_event_uid, "");
440 strcpy(conflict_event_summary, "");
442 msg = CtdlFetchMessage(msgnum);
443 if (msg == NULL) return;
444 memset(&ird, 0, sizeof ird);
445 strcpy(ird.desired_partnum, "1"); /* hopefully it's always 1 */
446 mime_parser(msg->cm_fields['M'],
448 *ical_locate_part, /* callback function */
450 (void *) &ird, /* user data */
453 CtdlFreeMessage(msg);
455 if (ird.cal == NULL) return;
457 t1start = icaltime_null_time();
458 t1end = icaltime_null_time();
459 t2start = icaltime_null_time();
460 t1end = icaltime_null_time();
462 /* Now compare cal to ird.cal */
463 p = ical_ctdl_get_subprop(ird.cal, ICAL_DTSTART_PROPERTY);
464 if (p == NULL) return;
465 if (p != NULL) t2start = icalproperty_get_dtstart(p);
467 p = ical_ctdl_get_subprop(ird.cal, ICAL_DTEND_PROPERTY);
468 if (p != NULL) t2end = icalproperty_get_dtend(p);
470 p = ical_ctdl_get_subprop(cal, ICAL_DTSTART_PROPERTY);
471 if (p == NULL) return;
472 if (p != NULL) t1start = icalproperty_get_dtstart(p);
474 p = ical_ctdl_get_subprop(cal, ICAL_DTEND_PROPERTY);
475 if (p != NULL) t1end = icalproperty_get_dtend(p);
477 p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
479 strcpy(compare_uid, icalproperty_get_comment(p));
482 p = ical_ctdl_get_subprop(ird.cal, ICAL_UID_PROPERTY);
484 strcpy(conflict_event_uid, icalproperty_get_comment(p));
487 p = ical_ctdl_get_subprop(ird.cal, ICAL_SUMMARY_PROPERTY);
489 strcpy(conflict_event_summary, icalproperty_get_comment(p));
493 icalcomponent_free(ird.cal);
495 if (ical_ctdl_is_overlap(t1start, t1end, t2start, t2end)) {
496 cprintf("%ld||%s|%s|%d|\n",
499 conflict_event_summary,
500 ( ((strlen(compare_uid)>0)
501 &&(!strcasecmp(compare_uid,
502 conflict_event_uid))) ? 1 : 0
511 * Phase 2 of "hunt for conflicts" operation.
512 * At this point we have a calendar object which represents the VEVENT that
513 * we're considering adding to the calendar. Now hunt through the user's
514 * calendar room, and output zero or more existing VEVENTs which conflict
517 void ical_hunt_for_conflicts(icalcomponent *cal) {
518 char hold_rm[ROOMNAMELEN];
520 strcpy(hold_rm, CC->quickroom.QRname); /* save current room */
522 if (getroom(&CC->quickroom, USERCALENDARROOM) != 0) {
523 getroom(&CC->quickroom, hold_rm);
524 cprintf("%d You do not have a calendar.\n", ERROR);
528 cprintf("%d Conflicting events:\n", LISTING_FOLLOWS);
530 CtdlForEachMessage(MSGS_ALL, 0, "text/calendar",
532 ical_hunt_for_conflicts_backend,
537 getroom(&CC->quickroom, hold_rm); /* return to saved room */
544 * Hunt for conflicts (Phase 1 -- retrieve the object and call Phase 2)
546 void ical_conflicts(long msgnum, char *partnum) {
547 struct CtdlMessage *msg;
548 struct ical_respond_data ird;
550 msg = CtdlFetchMessage(msgnum);
552 cprintf("%d Message %ld not found.\n",
559 memset(&ird, 0, sizeof ird);
560 strcpy(ird.desired_partnum, partnum);
561 mime_parser(msg->cm_fields['M'],
563 *ical_locate_part, /* callback function */
565 (void *) &ird, /* user data */
569 CtdlFreeMessage(msg);
571 if (ird.cal != NULL) {
572 ical_hunt_for_conflicts(ird.cal);
573 icalcomponent_free(ird.cal);
577 cprintf("%d No calendar object found\n", ERROR);
581 /* should never get here */
588 * All Citadel calendar commands from the client come through here.
590 void cmd_ical(char *argbuf)
597 if (CtdlAccessCheck(ac_logged_in)) return;
599 extract(subcmd, argbuf, 0);
601 if (!strcmp(subcmd, "test")) {
602 cprintf("%d This server supports calendaring\n", CIT_OK);
606 else if (!strcmp(subcmd, "respond")) {
607 msgnum = extract_long(argbuf, 1);
608 extract(partnum, argbuf, 2);
609 extract(action, argbuf, 3);
610 ical_respond(msgnum, partnum, action);
613 else if (!strcmp(subcmd, "conflicts")) {
614 msgnum = extract_long(argbuf, 1);
615 extract(partnum, argbuf, 2);
616 ical_conflicts(msgnum, partnum);
620 cprintf("%d Invalid subcommand\n", ERROR+CMD_NOT_SUPPORTED);
624 /* should never get here */
630 * We don't know if the calendar room exists so we just create it at login
632 void ical_create_room(void)
637 /* Create the calendar room if it doesn't already exist */
638 create_room(USERCALENDARROOM, 4, "", 0, 1, 0);
640 /* Set expiration policy to manual; otherwise objects will be lost! */
641 if (lgetroom(&qr, USERCALENDARROOM)) {
642 lprintf(3, "Couldn't get the user calendar room!\n");
645 qr.QRep.expire_mode = EXPIRE_MANUAL;
648 /* Set the view to a calendar view */
649 CtdlGetRelationship(&vbuf, &CC->usersupp, &qr);
650 vbuf.v_view = 3; /* 3 = calendar */
651 CtdlSetRelationship(&vbuf, &CC->usersupp, &qr);
653 /* Create the tasks list room if it doesn't already exist */
654 create_room(USERTASKSROOM, 4, "", 0, 1, 0);
656 /* Set expiration policy to manual; otherwise objects will be lost! */
657 if (lgetroom(&qr, USERTASKSROOM)) {
658 lprintf(3, "Couldn't get the user calendar room!\n");
661 qr.QRep.expire_mode = EXPIRE_MANUAL;
664 /* Set the view to a task list view */
665 CtdlGetRelationship(&vbuf, &CC->usersupp, &qr);
666 vbuf.v_view = 4; /* 4 = tasks */
667 CtdlSetRelationship(&vbuf, &CC->usersupp, &qr);
675 * Back end for ical_obj_beforesave()
676 * This hunts for the UID of the calendar event.
678 void ical_ctdl_set_extended_msgid(char *name, char *filename, char *partnum,
679 char *disp, void *content, char *cbtype, size_t length,
680 char *encoding, void *cbuserdata)
685 /* If this is a text/calendar object, hunt for the UID and drop it in
686 * the "user data" pointer for the MIME parser. When
687 * ical_obj_beforesave() sees it there, it'll set the Extended msgid
690 if (!strcasecmp(cbtype, "text/calendar")) {
691 cal = icalcomponent_new_from_string(content);
693 p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
695 strcpy((char *)cbuserdata,
696 icalproperty_get_comment(p)
699 icalcomponent_free(cal);
709 * See if we need to prevent the object from being saved (we don't allow
710 * MIME types other than text/calendar in the Calendar> room). Also, when
711 * saving an event to the calendar, set the message's Citadel extended message
712 * ID to the UID of the object. This causes our replication checker to
713 * automatically delete any existing instances of the same object. (Isn't
716 int ical_obj_beforesave(struct CtdlMessage *msg)
718 char roomname[ROOMNAMELEN];
724 * Only messages with content-type text/calendar
725 * may be saved to Calendar>. If the message is bound for
726 * Calendar> but doesn't have this content-type, throw an error
727 * so that the message may not be posted.
730 /* First determine if this is our room */
731 MailboxName(roomname, sizeof roomname, &CC->usersupp, USERCALENDARROOM);
732 if (strcasecmp(roomname, CC->quickroom.QRname)) {
733 return 0; /* It's not the Calendar room. */
736 /* Then determine content-type of the message */
738 /* It must be an RFC822 message! */
739 /* FIXME: Not handling MIME multipart messages; implement with IMIP */
740 if (msg->cm_format_type != 4)
741 return 1; /* You tried to save a non-RFC822 message! */
743 /* Find the Content-Type: header */
744 p = msg->cm_fields['M'];
747 if (!strncasecmp(p, "Content-Type: ", 14)) { /* Found it */
748 if (!strncasecmp(p + 14, "text/calendar", 13)) {
750 mime_parser(msg->cm_fields['M'],
752 *ical_ctdl_set_extended_msgid,
757 if (strlen(eidbuf) > 0) {
758 if (msg->cm_fields['E'] != NULL) {
759 phree(msg->cm_fields['E']);
761 msg->cm_fields['E'] = strdoop(eidbuf);
772 /* Oops! No Content-Type in this message! How'd that happen? */
773 lprintf(7, "RFC822 message with no Content-Type header!\n");
778 #endif /* HAVE_ICAL_H */
781 * Register this module with the Citadel server.
783 char *Dynamic_Module_Init(void)
786 CtdlRegisterMessageHook(ical_obj_beforesave, EVT_BEFORESAVE);
787 CtdlRegisterSessionHook(ical_create_room, EVT_LOGIN);
788 CtdlRegisterProtoHook(cmd_ical, "ICAL", "Citadel iCal commands");