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);
173 icalproperty_free(attendee);
176 /* We found our own address in the attendee list. */
178 /* Change the partstat from NEEDS-ACTION to ACCEPT or DECLINE */
179 icalproperty_remove_parameter(me_attend, ICAL_PARTSTAT_PARAMETER);
181 if (!strcasecmp(action, "accept")) {
182 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_ACCEPTED);
184 else if (!strcasecmp(action, "decline")) {
185 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_DECLINED);
187 else if (!strcasecmp(action, "tentative")) {
188 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_TENTATIVE);
191 if (partstat) icalproperty_add_parameter(me_attend, partstat);
193 /* Now insert it back into the vevent. */
194 icalcomponent_add_property(vevent, me_attend);
197 /* Figure out who to send this thing to */
198 organizer = icalcomponent_get_first_property(vevent, ICAL_ORGANIZER_PROPERTY);
199 if (organizer != NULL) {
200 if (icalproperty_get_organizer(organizer)) {
201 strcpy(organizer_string,
202 icalproperty_get_organizer(organizer) );
205 if (!strncasecmp(organizer_string, "MAILTO:", 7)) {
206 strcpy(organizer_string, &organizer_string[7]);
207 striplt(organizer_string);
209 strcpy(organizer_string, "");
212 /* Extract the summary string -- we'll use it as the
213 * message subject for the reply
215 summary = icalcomponent_get_first_property(vevent, ICAL_SUMMARY_PROPERTY);
216 if (summary != NULL) {
217 if (icalproperty_get_summary(summary)) {
218 strcpy(summary_string,
219 icalproperty_get_summary(summary) );
225 /* Now generate the reply message and send it out. */
226 serialized_reply = strdoop(icalcomponent_as_ical_string(the_reply));
227 icalcomponent_free(the_reply); /* don't need this anymore */
228 if (serialized_reply == NULL) return;
230 reply_message_text = mallok(strlen(serialized_reply) + SIZ);
231 if (reply_message_text != NULL) {
232 sprintf(reply_message_text,
233 "Content-type: text/calendar\r\n\r\n%s\r\n",
237 msg = CtdlMakeMessage(&CC->usersupp, organizer_string,
238 CC->quickroom.QRname, 0, FMT_RFC822,
240 summary_string, /* Use summary for subject */
244 valid = validate_recipients(organizer_string);
245 CtdlSubmitMsg(msg, valid, "");
246 CtdlFreeMessage(msg);
249 phree(serialized_reply);
255 * Callback function for mime parser that hunts for calendar content types
256 * and turns them into calendar objects
258 void ical_locate_part(char *name, char *filename, char *partnum, char *disp,
259 void *content, char *cbtype, size_t length, char *encoding,
262 struct ical_respond_data *ird = NULL;
264 ird = (struct ical_respond_data *) cbuserdata;
265 if (ird->cal != NULL) {
266 icalcomponent_free(ird->cal);
269 if (strcasecmp(partnum, ird->desired_partnum)) return;
270 ird->cal = icalcomponent_new_from_string(content);
275 * Respond to a meeting request.
277 void ical_respond(long msgnum, char *partnum, char *action) {
278 struct CtdlMessage *msg;
279 struct ical_respond_data ird;
282 (strcasecmp(action, "accept"))
283 && (strcasecmp(action, "decline"))
285 cprintf("%d Action must be 'accept' or 'decline'\n",
286 ERROR + ILLEGAL_VALUE
291 msg = CtdlFetchMessage(msgnum);
293 cprintf("%d Message %ld not found.\n",
300 memset(&ird, 0, sizeof ird);
301 strcpy(ird.desired_partnum, partnum);
302 mime_parser(msg->cm_fields['M'],
304 *ical_locate_part, /* callback function */
306 (void *) &ird, /* user data */
310 /* We're done with the incoming message, because we now have a
311 * calendar object in memory.
313 CtdlFreeMessage(msg);
316 * Here is the real meat of this function. Handle the event.
318 if (ird.cal != NULL) {
319 /* Save this in the user's calendar if necessary */
320 if (!strcasecmp(action, "accept")) {
321 ical_add(ird.cal, 0);
324 /* Send a reply if necessary */
325 if (icalcomponent_get_method(ird.cal) == ICAL_METHOD_REQUEST) {
326 ical_send_a_reply(ird.cal, action);
329 /* Now that we've processed this message, we don't need it
330 * anymore. So delete it.
332 CtdlDeleteMessages(CC->quickroom.QRname, msgnum, "");
334 /* Free the memory we allocated and return a response. */
335 icalcomponent_free(ird.cal);
337 cprintf("%d ok\n", CIT_OK);
341 cprintf("%d No calendar object found\n", ERROR);
345 /* should never get here */
350 * Search for a property in both the top level and in a VEVENT subcomponent
352 icalproperty *ical_ctdl_get_subprop(
354 icalproperty_kind which_prop
359 p = icalcomponent_get_first_property(cal, which_prop);
361 c = icalcomponent_get_first_component(cal,
362 ICAL_VEVENT_COMPONENT);
364 p = icalcomponent_get_first_property(c, which_prop);
372 * Check to see if two events overlap. Returns nonzero if they do.
374 int ical_ctdl_is_overlap(
375 struct icaltimetype t1start,
376 struct icaltimetype t1end,
377 struct icaltimetype t2start,
378 struct icaltimetype t2end
381 if (icaltime_is_null_time(t1start)) return(0);
382 if (icaltime_is_null_time(t2start)) return(0);
384 /* First, check for all-day events */
385 if (t1start.is_date) {
386 if (!icaltime_compare_date_only(t1start, t2start)) {
389 if (!icaltime_is_null_time(t2end)) {
390 if (!icaltime_compare_date_only(t1start, t2end)) {
396 if (t2start.is_date) {
397 if (!icaltime_compare_date_only(t2start, t1start)) {
400 if (!icaltime_is_null_time(t1end)) {
401 if (!icaltime_compare_date_only(t2start, t1end)) {
407 /* Now check for overlaps using date *and* time. */
409 /* First, bail out if either event 1 or event 2 is missing end time. */
410 if (icaltime_is_null_time(t1end)) return(0);
411 if (icaltime_is_null_time(t2end)) return(0);
413 /* If event 1 ends before event 2 starts, we're in the clear. */
414 if (icaltime_compare(t1end, t2start) <= 0) return(0);
416 /* If event 2 ends before event 1 starts, we're also ok. */
417 if (icaltime_compare(t2end, t1start) <= 0) return(0);
419 /* Otherwise, they overlap. */
426 * Backend for ical_hunt_for_conflicts()
428 void ical_hunt_for_conflicts_backend(long msgnum, void *data) {
430 struct CtdlMessage *msg;
431 struct ical_respond_data ird;
432 struct icaltimetype t1start, t1end, t2start, t2end;
434 char conflict_event_uid[SIZ];
435 char conflict_event_summary[SIZ];
436 char compare_uid[SIZ];
438 cal = (icalcomponent *)data;
439 strcpy(compare_uid, "");
440 strcpy(conflict_event_uid, "");
441 strcpy(conflict_event_summary, "");
443 msg = CtdlFetchMessage(msgnum);
444 if (msg == NULL) return;
445 memset(&ird, 0, sizeof ird);
446 strcpy(ird.desired_partnum, "1"); /* hopefully it's always 1 */
447 mime_parser(msg->cm_fields['M'],
449 *ical_locate_part, /* callback function */
451 (void *) &ird, /* user data */
454 CtdlFreeMessage(msg);
456 if (ird.cal == NULL) return;
458 t1start = icaltime_null_time();
459 t1end = icaltime_null_time();
460 t2start = icaltime_null_time();
461 t1end = icaltime_null_time();
463 /* Now compare cal to ird.cal */
464 p = ical_ctdl_get_subprop(ird.cal, ICAL_DTSTART_PROPERTY);
465 if (p == NULL) return;
466 if (p != NULL) t2start = icalproperty_get_dtstart(p);
468 p = ical_ctdl_get_subprop(ird.cal, ICAL_DTEND_PROPERTY);
469 if (p != NULL) t2end = icalproperty_get_dtend(p);
471 p = ical_ctdl_get_subprop(cal, ICAL_DTSTART_PROPERTY);
472 if (p == NULL) return;
473 if (p != NULL) t1start = icalproperty_get_dtstart(p);
475 p = ical_ctdl_get_subprop(cal, ICAL_DTEND_PROPERTY);
476 if (p != NULL) t1end = icalproperty_get_dtend(p);
478 p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
480 strcpy(compare_uid, icalproperty_get_comment(p));
483 p = ical_ctdl_get_subprop(ird.cal, ICAL_UID_PROPERTY);
485 strcpy(conflict_event_uid, icalproperty_get_comment(p));
488 p = ical_ctdl_get_subprop(ird.cal, ICAL_SUMMARY_PROPERTY);
490 strcpy(conflict_event_summary, icalproperty_get_comment(p));
494 icalcomponent_free(ird.cal);
496 if (ical_ctdl_is_overlap(t1start, t1end, t2start, t2end)) {
497 cprintf("%ld||%s|%s|%d|\n",
500 conflict_event_summary,
501 ( ((strlen(compare_uid)>0)
502 &&(!strcasecmp(compare_uid,
503 conflict_event_uid))) ? 1 : 0
512 * Phase 2 of "hunt for conflicts" operation.
513 * At this point we have a calendar object which represents the VEVENT that
514 * we're considering adding to the calendar. Now hunt through the user's
515 * calendar room, and output zero or more existing VEVENTs which conflict
518 void ical_hunt_for_conflicts(icalcomponent *cal) {
519 char hold_rm[ROOMNAMELEN];
521 strcpy(hold_rm, CC->quickroom.QRname); /* save current room */
523 if (getroom(&CC->quickroom, USERCALENDARROOM) != 0) {
524 getroom(&CC->quickroom, hold_rm);
525 cprintf("%d You do not have a calendar.\n", ERROR);
529 cprintf("%d Conflicting events:\n", LISTING_FOLLOWS);
531 CtdlForEachMessage(MSGS_ALL, 0, "text/calendar",
533 ical_hunt_for_conflicts_backend,
538 getroom(&CC->quickroom, hold_rm); /* return to saved room */
545 * Hunt for conflicts (Phase 1 -- retrieve the object and call Phase 2)
547 void ical_conflicts(long msgnum, char *partnum) {
548 struct CtdlMessage *msg;
549 struct ical_respond_data ird;
551 msg = CtdlFetchMessage(msgnum);
553 cprintf("%d Message %ld not found.\n",
560 memset(&ird, 0, sizeof ird);
561 strcpy(ird.desired_partnum, partnum);
562 mime_parser(msg->cm_fields['M'],
564 *ical_locate_part, /* callback function */
566 (void *) &ird, /* user data */
570 CtdlFreeMessage(msg);
572 if (ird.cal != NULL) {
573 ical_hunt_for_conflicts(ird.cal);
574 icalcomponent_free(ird.cal);
578 cprintf("%d No calendar object found\n", ERROR);
582 /* should never get here */
589 * All Citadel calendar commands from the client come through here.
591 void cmd_ical(char *argbuf)
598 if (CtdlAccessCheck(ac_logged_in)) return;
600 extract(subcmd, argbuf, 0);
602 if (!strcmp(subcmd, "test")) {
603 cprintf("%d This server supports calendaring\n", CIT_OK);
607 else if (!strcmp(subcmd, "respond")) {
608 msgnum = extract_long(argbuf, 1);
609 extract(partnum, argbuf, 2);
610 extract(action, argbuf, 3);
611 ical_respond(msgnum, partnum, action);
614 else if (!strcmp(subcmd, "conflicts")) {
615 msgnum = extract_long(argbuf, 1);
616 extract(partnum, argbuf, 2);
617 ical_conflicts(msgnum, partnum);
621 cprintf("%d Invalid subcommand\n", ERROR+CMD_NOT_SUPPORTED);
625 /* should never get here */
631 * We don't know if the calendar room exists so we just create it at login
633 void ical_create_room(void)
638 /* Create the calendar room if it doesn't already exist */
639 create_room(USERCALENDARROOM, 4, "", 0, 1, 0);
641 /* Set expiration policy to manual; otherwise objects will be lost! */
642 if (lgetroom(&qr, USERCALENDARROOM)) {
643 lprintf(3, "Couldn't get the user calendar room!\n");
646 qr.QRep.expire_mode = EXPIRE_MANUAL;
649 /* Set the view to a calendar view */
650 CtdlGetRelationship(&vbuf, &CC->usersupp, &qr);
651 vbuf.v_view = 3; /* 3 = calendar */
652 CtdlSetRelationship(&vbuf, &CC->usersupp, &qr);
654 /* Create the tasks list room if it doesn't already exist */
655 create_room(USERTASKSROOM, 4, "", 0, 1, 0);
657 /* Set expiration policy to manual; otherwise objects will be lost! */
658 if (lgetroom(&qr, USERTASKSROOM)) {
659 lprintf(3, "Couldn't get the user calendar room!\n");
662 qr.QRep.expire_mode = EXPIRE_MANUAL;
665 /* Set the view to a task list view */
666 CtdlGetRelationship(&vbuf, &CC->usersupp, &qr);
667 vbuf.v_view = 4; /* 4 = tasks */
668 CtdlSetRelationship(&vbuf, &CC->usersupp, &qr);
676 * Back end for ical_obj_beforesave()
677 * This hunts for the UID of the calendar event.
679 void ical_ctdl_set_extended_msgid(char *name, char *filename, char *partnum,
680 char *disp, void *content, char *cbtype, size_t length,
681 char *encoding, void *cbuserdata)
686 /* If this is a text/calendar object, hunt for the UID and drop it in
687 * the "user data" pointer for the MIME parser. When
688 * ical_obj_beforesave() sees it there, it'll set the Extended msgid
691 if (!strcasecmp(cbtype, "text/calendar")) {
692 cal = icalcomponent_new_from_string(content);
694 p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
696 strcpy((char *)cbuserdata,
697 icalproperty_get_comment(p)
700 icalcomponent_free(cal);
710 * See if we need to prevent the object from being saved (we don't allow
711 * MIME types other than text/calendar in the Calendar> room). Also, when
712 * saving an event to the calendar, set the message's Citadel extended message
713 * ID to the UID of the object. This causes our replication checker to
714 * automatically delete any existing instances of the same object. (Isn't
717 int ical_obj_beforesave(struct CtdlMessage *msg)
719 char roomname[ROOMNAMELEN];
725 * Only messages with content-type text/calendar
726 * may be saved to Calendar>. If the message is bound for
727 * Calendar> but doesn't have this content-type, throw an error
728 * so that the message may not be posted.
731 /* First determine if this is our room */
732 MailboxName(roomname, sizeof roomname, &CC->usersupp, USERCALENDARROOM);
733 if (strcasecmp(roomname, CC->quickroom.QRname)) {
734 return 0; /* It's not the Calendar room. */
737 /* Then determine content-type of the message */
739 /* It must be an RFC822 message! */
740 /* FIXME: Not handling MIME multipart messages; implement with IMIP */
741 if (msg->cm_format_type != 4)
742 return 1; /* You tried to save a non-RFC822 message! */
744 /* Find the Content-Type: header */
745 p = msg->cm_fields['M'];
748 if (!strncasecmp(p, "Content-Type: ", 14)) { /* Found it */
749 if (!strncasecmp(p + 14, "text/calendar", 13)) {
751 mime_parser(msg->cm_fields['M'],
753 *ical_ctdl_set_extended_msgid,
758 if (strlen(eidbuf) > 0) {
759 if (msg->cm_fields['E'] != NULL) {
760 phree(msg->cm_fields['E']);
762 msg->cm_fields['E'] = strdoop(eidbuf);
773 /* Oops! No Content-Type in this message! How'd that happen? */
774 lprintf(7, "RFC822 message with no Content-Type header!\n");
779 #endif /* HAVE_ICAL_H */
782 * Register this module with the Citadel server.
784 char *Dynamic_Module_Init(void)
787 CtdlRegisterMessageHook(ical_obj_beforesave, EVT_BEFORESAVE);
788 CtdlRegisterSessionHook(ical_create_room, EVT_LOGIN);
789 CtdlRegisterProtoHook(cmd_ical, "ICAL", "Citadel iCal commands");