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 *me_attend = NULL;
120 struct recptypes *recp = NULL;
121 icalparameter *partstat = NULL;
122 char *serialized_reply = NULL;
123 char *reply_message_text = NULL;
124 struct CtdlMessage *msg = NULL;
125 struct recptypes *valid = NULL;
127 strcpy(organizer_string, "");
129 if (request == NULL) {
130 lprintf(3, "ERROR: trying to reply to NULL event?\n");
134 the_reply = icalcomponent_new_clone(request);
135 if (the_reply == NULL) {
136 lprintf(3, "ERROR: cannot clone request\n");
140 /* Change the method from REQUEST to REPLY */
141 icalcomponent_set_method(the_reply, ICAL_METHOD_REPLY);
143 vevent = icalcomponent_get_first_component(the_reply, ICAL_VEVENT_COMPONENT);
144 if (vevent != NULL) {
145 /* Hunt for attendees, removing ones that aren't us.
146 * (Actually, remove them all, cloning our own one so we can
147 * re-insert it later)
149 while (attendee = icalcomponent_get_first_property(vevent,
150 ICAL_ATTENDEE_PROPERTY), (attendee != NULL)
152 if (icalproperty_get_attendee(attendee)) {
153 strcpy(attendee_string,
154 icalproperty_get_attendee(attendee) );
155 if (!strncasecmp(attendee_string, "MAILTO:", 7)) {
156 strcpy(attendee_string, &attendee_string[7]);
157 striplt(attendee_string);
158 recp = validate_recipients(attendee_string);
160 if (!strcasecmp(recp->recp_local, CC->usersupp.fullname)) {
161 if (me_attend) icalproperty_free(me_attend);
162 me_attend = icalproperty_new_clone(attendee);
169 icalcomponent_remove_property(vevent, attendee);
172 /* We found our own address in the attendee list. */
174 /* Change the partstat from NEEDS-ACTION to ACCEPT or DECLINE */
175 icalproperty_remove_parameter(me_attend, ICAL_PARTSTAT_PARAMETER);
177 if (!strcasecmp(action, "accept")) {
178 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_ACCEPTED);
180 else if (!strcasecmp(action, "decline")) {
181 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_DECLINED);
183 else if (!strcasecmp(action, "tentative")) {
184 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_TENTATIVE);
187 if (partstat) icalproperty_add_parameter(me_attend, partstat);
189 /* Now insert it back into the vevent. */
190 icalcomponent_add_property(vevent, me_attend);
193 /* Figure out who to send this thing to */
194 organizer = icalcomponent_get_first_property(vevent, ICAL_ORGANIZER_PROPERTY);
195 if (organizer != NULL) {
196 if (icalproperty_get_organizer(organizer)) {
197 strcpy(organizer_string,
198 icalproperty_get_organizer(organizer) );
201 if (!strncasecmp(organizer, "MAILTO:", 7)) {
202 strcpy(organizer_string, &organizer_string[7]);
203 striplt(organizer_string);
205 strcpy(organizer_string, "");
209 /* Now generate the reply message and send it out. */
210 serialized_reply = strdoop(icalcomponent_as_ical_string(the_reply));
211 icalcomponent_free(the_reply); /* don't need this anymore */
212 if (serialized_reply == NULL) return;
214 reply_message_text = mallok(strlen(serialized_reply) + SIZ);
215 if (reply_message_text != NULL) {
216 sprintf(reply_message_text,
217 "Content-type: text/calendar\r\n\r\n%s\r\n",
221 /* FIXME this still causes crashy crashy badness. */
222 msg = CtdlMakeMessage(&CC->usersupp, organizer_string,
223 CC->quickroom.QRname, 0, FMT_RFC822,
224 NULL, "FIXME subject", reply_message_text);
227 valid = validate_recipients(organizer_string);
228 CtdlSubmitMsg(msg, valid, "");
229 CtdlFreeMessage(msg);
232 phree(serialized_reply);
238 * Callback function for mime parser that hunts for calendar content types
239 * and turns them into calendar objects
241 void ical_locate_part(char *name, char *filename, char *partnum, char *disp,
242 void *content, char *cbtype, size_t length, char *encoding,
245 struct ical_respond_data *ird = NULL;
247 ird = (struct ical_respond_data *) cbuserdata;
248 if (ird->cal != NULL) {
249 icalcomponent_free(ird->cal);
252 if (strcasecmp(partnum, ird->desired_partnum)) return;
253 ird->cal = icalcomponent_new_from_string(content);
258 * Respond to a meeting request.
260 void ical_respond(long msgnum, char *partnum, char *action) {
261 struct CtdlMessage *msg;
262 struct ical_respond_data ird;
265 (strcasecmp(action, "accept"))
266 && (strcasecmp(action, "decline"))
268 cprintf("%d Action must be 'accept' or 'decline'\n",
269 ERROR + ILLEGAL_VALUE
274 msg = CtdlFetchMessage(msgnum);
276 cprintf("%d Message %ld not found.\n",
283 memset(&ird, 0, sizeof ird);
284 strcpy(ird.desired_partnum, partnum);
285 mime_parser(msg->cm_fields['M'],
287 *ical_locate_part, /* callback function */
289 (void *) &ird, /* user data */
293 /* We're done with the incoming message, because we now have a
294 * calendar object in memory.
296 CtdlFreeMessage(msg);
299 * Here is the real meat of this function. Handle the event.
301 if (ird.cal != NULL) {
302 /* Save this in the user's calendar if necessary */
303 if (!strcasecmp(action, "accept")) {
304 ical_add(ird.cal, 0);
307 /* Send a reply if necessary */
308 if (icalcomponent_get_method(ird.cal) == ICAL_METHOD_REQUEST) {
309 ical_send_a_reply(ird.cal, action);
312 /* Delete the message from the inbox */
313 /* FIXME ... do this */
315 /* Free the memory we allocated and return a response. */
316 icalcomponent_free(ird.cal);
318 cprintf("%d ok\n", CIT_OK);
322 cprintf("%d No calendar object found\n", ERROR);
326 /* should never get here */
331 * Search for a property in both the top level and in a VEVENT subcomponent
333 icalproperty *ical_ctdl_get_subprop(
335 icalproperty_kind which_prop
340 p = icalcomponent_get_first_property(cal, which_prop);
342 c = icalcomponent_get_first_component(cal,
343 ICAL_VEVENT_COMPONENT);
345 p = icalcomponent_get_first_property(c, which_prop);
353 * Check to see if two events overlap. Returns nonzero if they do.
355 int ical_ctdl_is_overlap(
356 struct icaltimetype t1start,
357 struct icaltimetype t1end,
358 struct icaltimetype t2start,
359 struct icaltimetype t2end
362 if (icaltime_is_null_time(t1start)) return(0);
363 if (icaltime_is_null_time(t2start)) return(0);
365 /* First, check for all-day events */
366 if (t1start.is_date) {
367 if (!icaltime_compare_date_only(t1start, t2start)) {
370 if (!icaltime_is_null_time(t2end)) {
371 if (!icaltime_compare_date_only(t1start, t2end)) {
377 if (t2start.is_date) {
378 if (!icaltime_compare_date_only(t2start, t1start)) {
381 if (!icaltime_is_null_time(t1end)) {
382 if (!icaltime_compare_date_only(t2start, t1end)) {
388 /* Now check for overlaps using date *and* time. */
390 /* First, bail out if either event 1 or event 2 is missing end time. */
391 if (icaltime_is_null_time(t1end)) return(0);
392 if (icaltime_is_null_time(t2end)) return(0);
394 /* If event 1 ends before event 2 starts, we're in the clear. */
395 if (icaltime_compare(t1end, t2start) <= 0) return(0);
397 /* If event 2 ends before event 1 starts, we're also ok. */
398 if (icaltime_compare(t2end, t1start) <= 0) return(0);
400 /* Otherwise, they overlap. */
407 * Backend for ical_hunt_for_conflicts()
409 void ical_hunt_for_conflicts_backend(long msgnum, void *data) {
411 struct CtdlMessage *msg;
412 struct ical_respond_data ird;
413 struct icaltimetype t1start, t1end, t2start, t2end;
415 char conflict_event_uid[SIZ];
416 char conflict_event_summary[SIZ];
417 char compare_uid[SIZ];
419 cal = (icalcomponent *)data;
420 strcpy(compare_uid, "");
421 strcpy(conflict_event_uid, "");
422 strcpy(conflict_event_summary, "");
424 msg = CtdlFetchMessage(msgnum);
425 if (msg == NULL) return;
426 memset(&ird, 0, sizeof ird);
427 strcpy(ird.desired_partnum, "1"); /* hopefully it's always 1 */
428 mime_parser(msg->cm_fields['M'],
430 *ical_locate_part, /* callback function */
432 (void *) &ird, /* user data */
435 CtdlFreeMessage(msg);
437 if (ird.cal == NULL) return;
439 t1start = icaltime_null_time();
440 t1end = icaltime_null_time();
441 t2start = icaltime_null_time();
442 t1end = icaltime_null_time();
444 /* Now compare cal to ird.cal */
445 p = ical_ctdl_get_subprop(ird.cal, ICAL_DTSTART_PROPERTY);
446 if (p == NULL) return;
447 if (p != NULL) t2start = icalproperty_get_dtstart(p);
449 p = ical_ctdl_get_subprop(ird.cal, ICAL_DTEND_PROPERTY);
450 if (p != NULL) t2end = icalproperty_get_dtend(p);
452 p = ical_ctdl_get_subprop(cal, ICAL_DTSTART_PROPERTY);
453 if (p == NULL) return;
454 if (p != NULL) t1start = icalproperty_get_dtstart(p);
456 p = ical_ctdl_get_subprop(cal, ICAL_DTEND_PROPERTY);
457 if (p != NULL) t1end = icalproperty_get_dtend(p);
459 p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
461 strcpy(compare_uid, icalproperty_get_comment(p));
464 p = ical_ctdl_get_subprop(ird.cal, ICAL_UID_PROPERTY);
466 strcpy(conflict_event_uid, icalproperty_get_comment(p));
469 p = ical_ctdl_get_subprop(ird.cal, ICAL_SUMMARY_PROPERTY);
471 strcpy(conflict_event_summary, icalproperty_get_comment(p));
475 icalcomponent_free(ird.cal);
477 if (ical_ctdl_is_overlap(t1start, t1end, t2start, t2end)) {
478 cprintf("%ld||%s|%s|%d|\n",
481 conflict_event_summary,
482 ( ((strlen(compare_uid)>0)
483 &&(!strcasecmp(compare_uid,
484 conflict_event_uid))) ? 1 : 0
493 * Phase 2 of "hunt for conflicts" operation.
494 * At this point we have a calendar object which represents the VEVENT that
495 * we're considering adding to the calendar. Now hunt through the user's
496 * calendar room, and output zero or more existing VEVENTs which conflict
499 void ical_hunt_for_conflicts(icalcomponent *cal) {
500 char hold_rm[ROOMNAMELEN];
502 strcpy(hold_rm, CC->quickroom.QRname); /* save current room */
504 if (getroom(&CC->quickroom, USERCALENDARROOM) != 0) {
505 getroom(&CC->quickroom, hold_rm);
506 cprintf("%d You do not have a calendar.\n", ERROR);
510 cprintf("%d Conflicting events:\n", LISTING_FOLLOWS);
512 CtdlForEachMessage(MSGS_ALL, 0, "text/calendar",
514 ical_hunt_for_conflicts_backend,
519 getroom(&CC->quickroom, hold_rm); /* return to saved room */
526 * Hunt for conflicts (Phase 1 -- retrieve the object and call Phase 2)
528 void ical_conflicts(long msgnum, char *partnum) {
529 struct CtdlMessage *msg;
530 struct ical_respond_data ird;
532 msg = CtdlFetchMessage(msgnum);
534 cprintf("%d Message %ld not found.\n",
541 memset(&ird, 0, sizeof ird);
542 strcpy(ird.desired_partnum, partnum);
543 mime_parser(msg->cm_fields['M'],
545 *ical_locate_part, /* callback function */
547 (void *) &ird, /* user data */
551 CtdlFreeMessage(msg);
553 if (ird.cal != NULL) {
554 ical_hunt_for_conflicts(ird.cal);
555 icalcomponent_free(ird.cal);
559 cprintf("%d No calendar object found\n", ERROR);
563 /* should never get here */
570 * All Citadel calendar commands from the client come through here.
572 void cmd_ical(char *argbuf)
579 if (CtdlAccessCheck(ac_logged_in)) return;
581 extract(subcmd, argbuf, 0);
583 if (!strcmp(subcmd, "test")) {
584 cprintf("%d This server supports calendaring\n", CIT_OK);
588 else if (!strcmp(subcmd, "respond")) {
589 msgnum = extract_long(argbuf, 1);
590 extract(partnum, argbuf, 2);
591 extract(action, argbuf, 3);
592 ical_respond(msgnum, partnum, action);
595 else if (!strcmp(subcmd, "conflicts")) {
596 msgnum = extract_long(argbuf, 1);
597 extract(partnum, argbuf, 2);
598 ical_conflicts(msgnum, partnum);
602 cprintf("%d Invalid subcommand\n", ERROR+CMD_NOT_SUPPORTED);
606 /* should never get here */
612 * We don't know if the calendar room exists so we just create it at login
614 void ical_create_room(void)
619 /* Create the calendar room if it doesn't already exist */
620 create_room(USERCALENDARROOM, 4, "", 0, 1, 0);
622 /* Set expiration policy to manual; otherwise objects will be lost! */
623 if (lgetroom(&qr, USERCALENDARROOM)) {
624 lprintf(3, "Couldn't get the user calendar room!\n");
627 qr.QRep.expire_mode = EXPIRE_MANUAL;
630 /* Set the view to a calendar view */
631 CtdlGetRelationship(&vbuf, &CC->usersupp, &qr);
632 vbuf.v_view = 3; /* 3 = calendar */
633 CtdlSetRelationship(&vbuf, &CC->usersupp, &qr);
635 /* Create the tasks list room if it doesn't already exist */
636 create_room(USERTASKSROOM, 4, "", 0, 1, 0);
638 /* Set expiration policy to manual; otherwise objects will be lost! */
639 if (lgetroom(&qr, USERTASKSROOM)) {
640 lprintf(3, "Couldn't get the user calendar room!\n");
643 qr.QRep.expire_mode = EXPIRE_MANUAL;
646 /* Set the view to a task list view */
647 CtdlGetRelationship(&vbuf, &CC->usersupp, &qr);
648 vbuf.v_view = 4; /* 4 = tasks */
649 CtdlSetRelationship(&vbuf, &CC->usersupp, &qr);
657 * Back end for ical_obj_beforesave()
658 * This hunts for the UID of the calendar event.
660 void ical_ctdl_set_extended_msgid(char *name, char *filename, char *partnum,
661 char *disp, void *content, char *cbtype, size_t length,
662 char *encoding, void *cbuserdata)
667 /* If this is a text/calendar object, hunt for the UID and drop it in
668 * the "user data" pointer for the MIME parser. When
669 * ical_obj_beforesave() sees it there, it'll set the Extended msgid
672 if (!strcasecmp(cbtype, "text/calendar")) {
673 cal = icalcomponent_new_from_string(content);
675 p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
677 strcpy((char *)cbuserdata,
678 icalproperty_get_comment(p)
681 icalcomponent_free(cal);
691 * See if we need to prevent the object from being saved (we don't allow
692 * MIME types other than text/calendar in the Calendar> room). Also, when
693 * saving an event to the calendar, set the message's Citadel extended message
694 * ID to the UID of the object. This causes our replication checker to
695 * automatically delete any existing instances of the same object. (Isn't
698 int ical_obj_beforesave(struct CtdlMessage *msg)
700 char roomname[ROOMNAMELEN];
706 * Only messages with content-type text/calendar
707 * may be saved to Calendar>. If the message is bound for
708 * Calendar> but doesn't have this content-type, throw an error
709 * so that the message may not be posted.
712 /* First determine if this is our room */
713 MailboxName(roomname, sizeof roomname, &CC->usersupp, USERCALENDARROOM);
714 if (strcasecmp(roomname, CC->quickroom.QRname)) {
715 return 0; /* It's not the Calendar room. */
718 /* Then determine content-type of the message */
720 /* It must be an RFC822 message! */
721 /* FIXME: Not handling MIME multipart messages; implement with IMIP */
722 if (msg->cm_format_type != 4)
723 return 1; /* You tried to save a non-RFC822 message! */
725 /* Find the Content-Type: header */
726 p = msg->cm_fields['M'];
729 if (!strncasecmp(p, "Content-Type: ", 14)) { /* Found it */
730 if (!strncasecmp(p + 14, "text/calendar", 13)) {
732 mime_parser(msg->cm_fields['M'],
734 *ical_ctdl_set_extended_msgid,
739 if (strlen(eidbuf) > 0) {
740 if (msg->cm_fields['E'] != NULL) {
741 phree(msg->cm_fields['E']);
743 msg->cm_fields['E'] = strdoop(eidbuf);
754 /* Oops! No Content-Type in this message! How'd that happen? */
755 lprintf(7, "RFC822 message with no Content-Type header!\n");
760 #endif /* HAVE_ICAL_H */
763 * Register this module with the Citadel server.
765 char *Dynamic_Module_Init(void)
768 CtdlRegisterMessageHook(ical_obj_beforesave, EVT_BEFORESAVE);
769 CtdlRegisterSessionHook(ical_create_room, EVT_LOGIN);
770 CtdlRegisterProtoHook(cmd_ical, "ICAL", "Citadel iCal commands");