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 void ical_send_a_reply(icalcomponent *request, char *action) {
110 icalcomponent *the_reply = NULL;
111 icalcomponent *vevent = NULL;
112 icalproperty *attendee = NULL;
113 char attendee_string[SIZ];
114 icalproperty *organizer = NULL;
115 char organizer_string[SIZ];
116 icalproperty *me_attend = NULL;
117 struct recptypes *recp = NULL;
118 icalparameter *partstat = NULL;
120 strcpy(organizer_string, "");
122 if (request == NULL) {
123 lprintf(3, "ERROR: trying to reply to NULL event?\n");
127 the_reply = icalcomponent_new_clone(request);
128 if (the_reply == NULL) {
129 lprintf(3, "ERROR: cannot clone request\n");
133 /* Change the method from REQUEST to REPLY */
134 icalcomponent_set_method(the_reply, ICAL_METHOD_REPLY);
136 vevent = icalcomponent_get_first_component(the_reply,
137 ICAL_VEVENT_COMPONENT);
138 if (vevent != NULL) {
139 /* Hunt for attendees, removing ones that aren't us.
140 * (Actually, remove them all, cloning our own one so we can
141 * re-insert it later)
143 while (attendee = icalcomponent_get_first_property(vevent,
144 ICAL_ATTENDEE_PROPERTY), (attendee != NULL)
146 if (icalproperty_get_attendee(attendee)) {
147 strcpy(attendee_string,
148 icalproperty_get_attendee(attendee) );
149 if (!strncasecmp(attendee_string, "MAILTO:",
151 strcpy(attendee_string, &attendee_string[7]);
152 striplt(attendee_string);
153 recp = validate_recipients(attendee_string);
155 if (!strcasecmp(recp->recp_local, CC->usersupp.fullname)) {
156 if (me_attend) icalproperty_free(me_attend);
157 me_attend = icalproperty_new_clone(attendee);
164 icalcomponent_remove_property(vevent, attendee);
167 /* We found our own address in the attendee list. */
169 /* Change the partstat from NEEDS-ACTION to ACCEPT or DECLINE */
170 icalproperty_remove_parameter(me_attend, ICAL_PARTSTAT_PARAMETER);
172 if (!strcasecmp(action, "accept")) {
173 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_ACCEPTED);
175 else if (!strcasecmp(action, "decline")) {
176 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_DECLINED);
178 else if (!strcasecmp(action, "tentative")) {
179 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_TENTATIVE);
182 if (partstat) icalproperty_add_parameter(me_attend, partstat);
184 /* Now insert it back into the vevent. */
185 icalcomponent_add_property(vevent, me_attend);
188 /* Figure out who to send this thing to */
189 organizer = icalcomponent_get_first_property(vevent, ICAL_ORGANIZER_PROPERTY);
190 if (organizer != NULL) {
191 if (icalproperty_get_organizer(organizer)) {
192 strcpy(organizer_string,
193 icalproperty_get_organizer(organizer) );
196 if (!strncasecmp(organizer, "MAILTO:", 7)) {
197 strcpy(organizer_string, &organizer_string[7]);
198 striplt(organizer_string);
200 strcpy(organizer_string, "");
204 /********* FIXME **********
205 All we have to do now is send the reply. Generate it with:
206 icalcomponent_as_ical_string(the_reply)
207 ...and send it to 'organizer_string'
208 (I'm just too tired to do it now)
209 **********************************/
212 icalcomponent_free(the_reply);
218 * Callback function for mime parser that hunts for calendar content types
219 * and turns them into calendar objects
221 void ical_locate_part(char *name, char *filename, char *partnum, char *disp,
222 void *content, char *cbtype, size_t length, char *encoding,
225 struct ical_respond_data *ird = NULL;
227 ird = (struct ical_respond_data *) cbuserdata;
228 if (ird->cal != NULL) {
229 icalcomponent_free(ird->cal);
232 if (strcasecmp(partnum, ird->desired_partnum)) return;
233 ird->cal = icalcomponent_new_from_string(content);
238 * Respond to a meeting request.
240 void ical_respond(long msgnum, char *partnum, char *action) {
241 struct CtdlMessage *msg;
242 struct ical_respond_data ird;
245 (strcasecmp(action, "accept"))
246 && (strcasecmp(action, "decline"))
248 cprintf("%d Action must be 'accept' or 'decline'\n",
249 ERROR + ILLEGAL_VALUE
254 msg = CtdlFetchMessage(msgnum);
256 cprintf("%d Message %ld not found.\n",
263 memset(&ird, 0, sizeof ird);
264 strcpy(ird.desired_partnum, partnum);
265 mime_parser(msg->cm_fields['M'],
267 *ical_locate_part, /* callback function */
269 (void *) &ird, /* user data */
273 /* We're done with the incoming message, because we now have a
274 * calendar object in memory.
276 CtdlFreeMessage(msg);
279 * Here is the real meat of this function. Handle the event.
281 if (ird.cal != NULL) {
282 /* Save this in the user's calendar if necessary */
283 if (!strcasecmp(action, "accept")) {
284 ical_add(ird.cal, 0);
287 /* Send a reply if necessary */
288 if (icalcomponent_get_method(ird.cal) == ICAL_METHOD_REQUEST) {
289 ical_send_a_reply(ird.cal, action);
292 /* Delete the message from the inbox */
293 /* FIXME ... do this */
295 /* Free the memory we allocated and return a response. */
296 icalcomponent_free(ird.cal);
298 cprintf("%d ok\n", CIT_OK);
302 cprintf("%d No calendar object found\n", ERROR);
306 /* should never get here */
311 * Search for a property in both the top level and in a VEVENT subcomponent
313 icalproperty *ical_ctdl_get_subprop(
315 icalproperty_kind which_prop
320 p = icalcomponent_get_first_property(cal, which_prop);
322 c = icalcomponent_get_first_component(cal,
323 ICAL_VEVENT_COMPONENT);
325 p = icalcomponent_get_first_property(c, which_prop);
333 * Check to see if two events overlap. Returns nonzero if they do.
335 int ical_ctdl_is_overlap(
336 struct icaltimetype t1start,
337 struct icaltimetype t1end,
338 struct icaltimetype t2start,
339 struct icaltimetype t2end
342 if (icaltime_is_null_time(t1start)) return(0);
343 if (icaltime_is_null_time(t2start)) return(0);
345 /* First, check for all-day events */
346 if (t1start.is_date) {
347 if (!icaltime_compare_date_only(t1start, t2start)) {
350 if (!icaltime_is_null_time(t2end)) {
351 if (!icaltime_compare_date_only(t1start, t2end)) {
357 if (t2start.is_date) {
358 if (!icaltime_compare_date_only(t2start, t1start)) {
361 if (!icaltime_is_null_time(t1end)) {
362 if (!icaltime_compare_date_only(t2start, t1end)) {
368 /* Now check for overlaps using date *and* time. */
370 /* First, bail out if either event 1 or event 2 is missing end time. */
371 if (icaltime_is_null_time(t1end)) return(0);
372 if (icaltime_is_null_time(t2end)) return(0);
374 /* If event 1 ends before event 2 starts, we're in the clear. */
375 if (icaltime_compare(t1end, t2start) <= 0) return(0);
377 /* If event 2 ends before event 1 starts, we're also ok. */
378 if (icaltime_compare(t2end, t1start) <= 0) return(0);
380 /* Otherwise, they overlap. */
387 * Backend for ical_hunt_for_conflicts()
389 void ical_hunt_for_conflicts_backend(long msgnum, void *data) {
391 struct CtdlMessage *msg;
392 struct ical_respond_data ird;
393 struct icaltimetype t1start, t1end, t2start, t2end;
395 char conflict_event_uid[SIZ];
396 char conflict_event_summary[SIZ];
397 char compare_uid[SIZ];
399 cal = (icalcomponent *)data;
400 strcpy(compare_uid, "");
401 strcpy(conflict_event_uid, "");
402 strcpy(conflict_event_summary, "");
404 msg = CtdlFetchMessage(msgnum);
405 if (msg == NULL) return;
406 memset(&ird, 0, sizeof ird);
407 strcpy(ird.desired_partnum, "1"); /* hopefully it's always 1 */
408 mime_parser(msg->cm_fields['M'],
410 *ical_locate_part, /* callback function */
412 (void *) &ird, /* user data */
415 CtdlFreeMessage(msg);
417 if (ird.cal == NULL) return;
419 t1start = icaltime_null_time();
420 t1end = icaltime_null_time();
421 t2start = icaltime_null_time();
422 t1end = icaltime_null_time();
424 /* Now compare cal to ird.cal */
425 p = ical_ctdl_get_subprop(ird.cal, ICAL_DTSTART_PROPERTY);
426 if (p == NULL) return;
427 if (p != NULL) t2start = icalproperty_get_dtstart(p);
429 p = ical_ctdl_get_subprop(ird.cal, ICAL_DTEND_PROPERTY);
430 if (p != NULL) t2end = icalproperty_get_dtend(p);
432 p = ical_ctdl_get_subprop(cal, ICAL_DTSTART_PROPERTY);
433 if (p == NULL) return;
434 if (p != NULL) t1start = icalproperty_get_dtstart(p);
436 p = ical_ctdl_get_subprop(cal, ICAL_DTEND_PROPERTY);
437 if (p != NULL) t1end = icalproperty_get_dtend(p);
439 p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
441 strcpy(compare_uid, icalproperty_get_comment(p));
444 p = ical_ctdl_get_subprop(ird.cal, ICAL_UID_PROPERTY);
446 strcpy(conflict_event_uid, icalproperty_get_comment(p));
449 p = ical_ctdl_get_subprop(ird.cal, ICAL_SUMMARY_PROPERTY);
451 strcpy(conflict_event_summary, icalproperty_get_comment(p));
455 icalcomponent_free(ird.cal);
457 if (ical_ctdl_is_overlap(t1start, t1end, t2start, t2end)) {
458 cprintf("%ld||%s|%s|%d|\n",
461 conflict_event_summary,
462 ( ((strlen(compare_uid)>0)
463 &&(!strcasecmp(compare_uid,
464 conflict_event_uid))) ? 1 : 0
473 * Phase 2 of "hunt for conflicts" operation.
474 * At this point we have a calendar object which represents the VEVENT that
475 * we're considering adding to the calendar. Now hunt through the user's
476 * calendar room, and output zero or more existing VEVENTs which conflict
479 void ical_hunt_for_conflicts(icalcomponent *cal) {
480 char hold_rm[ROOMNAMELEN];
482 strcpy(hold_rm, CC->quickroom.QRname); /* save current room */
484 if (getroom(&CC->quickroom, USERCALENDARROOM) != 0) {
485 getroom(&CC->quickroom, hold_rm);
486 cprintf("%d You do not have a calendar.\n", ERROR);
490 cprintf("%d Conflicting events:\n", LISTING_FOLLOWS);
492 CtdlForEachMessage(MSGS_ALL, 0, "text/calendar",
494 ical_hunt_for_conflicts_backend,
499 getroom(&CC->quickroom, hold_rm); /* return to saved room */
506 * Hunt for conflicts (Phase 1 -- retrieve the object and call Phase 2)
508 void ical_conflicts(long msgnum, char *partnum) {
509 struct CtdlMessage *msg;
510 struct ical_respond_data ird;
512 msg = CtdlFetchMessage(msgnum);
514 cprintf("%d Message %ld not found.\n",
521 memset(&ird, 0, sizeof ird);
522 strcpy(ird.desired_partnum, partnum);
523 mime_parser(msg->cm_fields['M'],
525 *ical_locate_part, /* callback function */
527 (void *) &ird, /* user data */
531 CtdlFreeMessage(msg);
533 if (ird.cal != NULL) {
534 ical_hunt_for_conflicts(ird.cal);
535 icalcomponent_free(ird.cal);
539 cprintf("%d No calendar object found\n", ERROR);
543 /* should never get here */
550 * All Citadel calendar commands from the client come through here.
552 void cmd_ical(char *argbuf)
559 if (CtdlAccessCheck(ac_logged_in)) return;
561 extract(subcmd, argbuf, 0);
563 if (!strcmp(subcmd, "test")) {
564 cprintf("%d This server supports calendaring\n", CIT_OK);
568 else if (!strcmp(subcmd, "respond")) {
569 msgnum = extract_long(argbuf, 1);
570 extract(partnum, argbuf, 2);
571 extract(action, argbuf, 3);
572 ical_respond(msgnum, partnum, action);
575 else if (!strcmp(subcmd, "conflicts")) {
576 msgnum = extract_long(argbuf, 1);
577 extract(partnum, argbuf, 2);
578 ical_conflicts(msgnum, partnum);
582 cprintf("%d Invalid subcommand\n", ERROR+CMD_NOT_SUPPORTED);
586 /* should never get here */
592 * We don't know if the calendar room exists so we just create it at login
594 void ical_create_room(void)
599 /* Create the calendar room if it doesn't already exist */
600 create_room(USERCALENDARROOM, 4, "", 0, 1, 0);
602 /* Set expiration policy to manual; otherwise objects will be lost! */
603 if (lgetroom(&qr, USERCALENDARROOM)) {
604 lprintf(3, "Couldn't get the user calendar room!\n");
607 qr.QRep.expire_mode = EXPIRE_MANUAL;
610 /* Set the view to a calendar view */
611 CtdlGetRelationship(&vbuf, &CC->usersupp, &qr);
612 vbuf.v_view = 3; /* 3 = calendar */
613 CtdlSetRelationship(&vbuf, &CC->usersupp, &qr);
615 /* Create the tasks list room if it doesn't already exist */
616 create_room(USERTASKSROOM, 4, "", 0, 1, 0);
618 /* Set expiration policy to manual; otherwise objects will be lost! */
619 if (lgetroom(&qr, USERTASKSROOM)) {
620 lprintf(3, "Couldn't get the user calendar room!\n");
623 qr.QRep.expire_mode = EXPIRE_MANUAL;
626 /* Set the view to a task list view */
627 CtdlGetRelationship(&vbuf, &CC->usersupp, &qr);
628 vbuf.v_view = 4; /* 4 = tasks */
629 CtdlSetRelationship(&vbuf, &CC->usersupp, &qr);
637 * Back end for ical_obj_beforesave()
638 * This hunts for the UID of the calendar event.
640 void ical_ctdl_set_extended_msgid(char *name, char *filename, char *partnum,
641 char *disp, void *content, char *cbtype, size_t length,
642 char *encoding, void *cbuserdata)
647 /* If this is a text/calendar object, hunt for the UID and drop it in
648 * the "user data" pointer for the MIME parser. When
649 * ical_obj_beforesave() sees it there, it'll set the Extended msgid
652 if (!strcasecmp(cbtype, "text/calendar")) {
653 cal = icalcomponent_new_from_string(content);
655 p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
657 strcpy((char *)cbuserdata,
658 icalproperty_get_comment(p)
661 icalcomponent_free(cal);
671 * See if we need to prevent the object from being saved (we don't allow
672 * MIME types other than text/calendar in the Calendar> room). Also, when
673 * saving an event to the calendar, set the message's Citadel extended message
674 * ID to the UID of the object. This causes our replication checker to
675 * automatically delete any existing instances of the same object. (Isn't
678 int ical_obj_beforesave(struct CtdlMessage *msg)
680 char roomname[ROOMNAMELEN];
686 * Only messages with content-type text/calendar
687 * may be saved to Calendar>. If the message is bound for
688 * Calendar> but doesn't have this content-type, throw an error
689 * so that the message may not be posted.
692 /* First determine if this is our room */
693 MailboxName(roomname, sizeof roomname, &CC->usersupp, USERCALENDARROOM);
694 if (strcasecmp(roomname, CC->quickroom.QRname)) {
695 return 0; /* It's not the Calendar room. */
698 /* Then determine content-type of the message */
700 /* It must be an RFC822 message! */
701 /* FIXME: Not handling MIME multipart messages; implement with IMIP */
702 if (msg->cm_format_type != 4)
703 return 1; /* You tried to save a non-RFC822 message! */
705 /* Find the Content-Type: header */
706 p = msg->cm_fields['M'];
709 if (!strncasecmp(p, "Content-Type: ", 14)) { /* Found it */
710 if (!strncasecmp(p + 14, "text/calendar", 13)) {
712 mime_parser(msg->cm_fields['M'],
714 *ical_ctdl_set_extended_msgid,
719 if (strlen(eidbuf) > 0) {
720 if (msg->cm_fields['E'] != NULL) {
721 phree(msg->cm_fields['E']);
723 msg->cm_fields['E'] = strdoop(eidbuf);
734 /* Oops! No Content-Type in this message! How'd that happen? */
735 lprintf(7, "RFC822 message with no Content-Type header!\n");
740 #endif /* HAVE_ICAL_H */
743 * Register this module with the Citadel server.
745 char *Dynamic_Module_Init(void)
748 CtdlRegisterMessageHook(ical_obj_beforesave, EVT_BEFORESAVE);
749 CtdlRegisterSessionHook(ical_create_room, EVT_LOGIN);
750 CtdlRegisterProtoHook(cmd_ical, "ICAL", "Citadel iCal commands");