2 * This module implements iCalendar object processing and the Calendar>
3 * room on a Citadel server. It handles iCalendar objects using the
4 * iTIP protocol. See RFCs 2445 and 2446.
6 * Copyright (c) 1987-2018 by the citadel.org team
8 * This program is open source software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License version 3.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
17 #define PRODID "-//Citadel//NONSGML Citadel Calendar//EN"
19 #include "ctdl_module.h"
20 #include <libical/ical.h>
22 #include "internet_addressing.h"
23 #include "serv_calendar.h"
25 #include "euidindex.h"
26 #include "default_timezone.h"
29 struct ical_respond_data {
30 char desired_partnum[SIZ];
36 * Utility function to create a new VCALENDAR component with some of the
37 * required fields already set the way we like them.
39 icalcomponent *icalcomponent_new_citadel_vcalendar(void) {
40 icalcomponent *encaps;
42 encaps = icalcomponent_new_vcalendar();
44 syslog(LOG_ERR, "calendar: could not allocate component");
48 /* Set the Product ID */
49 icalcomponent_add_property(encaps, icalproperty_new_prodid(PRODID));
51 /* Set the Version Number */
52 icalcomponent_add_property(encaps, icalproperty_new_version("2.0"));
59 * Utility function to encapsulate a subcomponent into a full VCALENDAR
61 icalcomponent *ical_encapsulate_subcomponent(icalcomponent *subcomp) {
62 icalcomponent *encaps;
64 /* If we're already looking at a full VCALENDAR component,
65 * don't bother ... just return itself.
67 if (icalcomponent_isa(subcomp) == ICAL_VCALENDAR_COMPONENT) {
71 /* Encapsulate the VEVENT component into a complete VCALENDAR */
72 encaps = icalcomponent_new_citadel_vcalendar();
73 if (encaps == NULL) return NULL;
75 /* Encapsulate the subcomponent inside */
76 icalcomponent_add_component(encaps, subcomp);
78 /* Return the object we just created. */
84 * Write a calendar object into the specified user's calendar room.
85 * If the supplied user is NULL, this function writes the calendar object
86 * to the currently selected room.
88 void ical_write_to_cal(struct ctdluser *u, icalcomponent *cal) {
91 icalcomponent *encaps = NULL;
92 struct CtdlMessage *msg = NULL;
93 icalcomponent *tmp=NULL;
95 if (cal == NULL) return;
97 /* If the supplied object is a subcomponent, encapsulate it in
98 * a full VCALENDAR component, and save that instead.
100 if (icalcomponent_isa(cal) != ICAL_VCALENDAR_COMPONENT) {
101 tmp = icalcomponent_new_clone(cal);
102 encaps = ical_encapsulate_subcomponent(tmp);
103 ical_write_to_cal(u, encaps);
104 icalcomponent_free(tmp);
105 icalcomponent_free(encaps);
109 ser = icalcomponent_as_ical_string_r(cal);
110 if (ser == NULL) return;
112 serlen = strlen(ser);
114 /* If the caller supplied a user, write to that user's default calendar room */
116 /* This handy API function does all the work for us. */
117 CtdlWriteObject(USERCALENDARROOM, /* which room */
118 "text/calendar", /* MIME type */
120 serlen + 1, /* length */
123 0, /* don't delete others of this type */
128 /* If the caller did not supply a user, write to the currently selected room */
130 struct CitContext *CCC = CC;
133 msg = malloc(sizeof(struct CtdlMessage));
134 memset(msg, 0, sizeof(struct CtdlMessage));
135 msg->cm_magic = CTDLMESSAGE_MAGIC;
136 msg->cm_anon_type = MES_NORMAL;
137 msg->cm_format_type = 4;
138 CM_SetField(msg, eAuthor, CCC->user.fullname, strlen(CCC->user.fullname));
139 CM_SetField(msg, eOriginalRoom, CCC->room.QRname, strlen(CCC->room.QRname));
140 CM_SetField(msg, eHumanNode, CtdlGetConfigStr("c_humannode"), strlen(CtdlGetConfigStr("c_humannode")));
142 MsgBody = NewStrBufPlain(NULL, serlen + 100);
143 StrBufAppendBufPlain(MsgBody, HKEY("Content-type: text/calendar\r\n\r\n"), 0);
144 StrBufAppendBufPlain(MsgBody, ser, serlen, 0);
146 CM_SetAsFieldSB(msg, eMesageText, &MsgBody);
148 /* Now write the data */
149 CtdlSubmitMsg(msg, NULL, "", QP_EADDR);
153 /* In either case, now we can free the serialized calendar object */
159 * Send a reply to a meeting invitation.
161 * 'request' is the invitation to reply to.
162 * 'action' is the string "accept" or "decline" or "tentative".
165 void ical_send_a_reply(icalcomponent *request, char *action) {
166 icalcomponent *the_reply = NULL;
167 icalcomponent *vevent = NULL;
168 icalproperty *attendee = NULL;
169 char attendee_string[SIZ];
170 icalproperty *organizer = NULL;
171 char organizer_string[SIZ];
172 icalproperty *summary = NULL;
173 char summary_string[SIZ];
174 icalproperty *me_attend = NULL;
175 recptypes *recp = NULL;
176 icalparameter *partstat = NULL;
177 char *serialized_reply = NULL;
178 char *reply_message_text = NULL;
180 struct CtdlMessage *msg = NULL;
181 recptypes *valid = NULL;
183 *organizer_string = '\0';
184 strcpy(summary_string, "Calendar item");
186 if (request == NULL) {
187 syslog(LOG_ERR, "calendar: trying to reply to NULL event");
191 the_reply = icalcomponent_new_clone(request);
192 if (the_reply == NULL) {
193 syslog(LOG_ERR, "calendar: cannot clone request");
197 /* Change the method from REQUEST to REPLY */
198 icalcomponent_set_method(the_reply, ICAL_METHOD_REPLY);
200 vevent = icalcomponent_get_first_component(the_reply, ICAL_VEVENT_COMPONENT);
201 if (vevent != NULL) {
202 /* Hunt for attendees, removing ones that aren't us.
203 * (Actually, remove them all, cloning our own one so we can
204 * re-insert it later)
206 while (attendee = icalcomponent_get_first_property(vevent,
207 ICAL_ATTENDEE_PROPERTY), (attendee != NULL)
209 ch = icalproperty_get_attendee(attendee);
210 if ((ch != NULL) && !strncasecmp(ch, "MAILTO:", 7)) {
211 safestrncpy(attendee_string, ch + 7, sizeof (attendee_string));
212 striplt(attendee_string);
213 recp = validate_recipients(attendee_string, NULL, 0);
215 if (!strcasecmp(recp->recp_local, CC->user.fullname)) {
216 if (me_attend) icalproperty_free(me_attend);
217 me_attend = icalproperty_new_clone(attendee);
219 free_recipients(recp);
224 icalcomponent_remove_property(vevent, attendee);
225 icalproperty_free(attendee);
228 /* We found our own address in the attendee list. */
230 /* Change the partstat from NEEDS-ACTION to ACCEPT or DECLINE */
231 icalproperty_remove_parameter_by_kind(me_attend, ICAL_PARTSTAT_PARAMETER);
233 if (!strcasecmp(action, "accept")) {
234 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_ACCEPTED);
236 else if (!strcasecmp(action, "decline")) {
237 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_DECLINED);
239 else if (!strcasecmp(action, "tentative")) {
240 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_TENTATIVE);
243 if (partstat) icalproperty_add_parameter(me_attend, partstat);
245 /* Now insert it back into the vevent. */
246 icalcomponent_add_property(vevent, me_attend);
249 /* Figure out who to send this thing to */
250 organizer = icalcomponent_get_first_property(vevent, ICAL_ORGANIZER_PROPERTY);
251 if (organizer != NULL) {
252 if (icalproperty_get_organizer(organizer)) {
253 strcpy(organizer_string,
254 icalproperty_get_organizer(organizer) );
257 if (!strncasecmp(organizer_string, "MAILTO:", 7)) {
258 strcpy(organizer_string, &organizer_string[7]);
259 striplt(organizer_string);
261 strcpy(organizer_string, "");
264 /* Extract the summary string -- we'll use it as the
265 * message subject for the reply
267 summary = icalcomponent_get_first_property(vevent, ICAL_SUMMARY_PROPERTY);
268 if (summary != NULL) {
269 if (icalproperty_get_summary(summary)) {
270 strcpy(summary_string,
271 icalproperty_get_summary(summary) );
276 /* Now generate the reply message and send it out. */
277 serialized_reply = icalcomponent_as_ical_string_r(the_reply);
278 icalcomponent_free(the_reply); /* don't need this anymore */
279 if (serialized_reply == NULL) return;
281 reply_message_text = malloc(strlen(serialized_reply) + SIZ);
282 if (reply_message_text != NULL) {
283 sprintf(reply_message_text,
284 "Content-type: text/calendar; charset=\"utf-8\"\r\n\r\n%s\r\n",
288 msg = CtdlMakeMessage(&CC->user,
289 organizer_string, /* to */
291 CC->room.QRname, 0, FMT_RFC822,
294 summary_string, /* Use summary for subject */
300 valid = validate_recipients(organizer_string, NULL, 0);
301 CtdlSubmitMsg(msg, valid, "", QP_EADDR);
303 free_recipients(valid);
306 free(serialized_reply);
311 * Callback function for mime parser that hunts for calendar content types
312 * and turns them into calendar objects. If something is found, it is placed
313 * in ird->cal, and the caller now owns that memory and is responsible for freeing it.
315 void ical_locate_part(char *name, char *filename, char *partnum, char *disp,
316 void *content, char *cbtype, char *cbcharset, size_t length, char *encoding,
317 char *cbid, void *cbuserdata) {
319 struct ical_respond_data *ird = NULL;
321 ird = (struct ical_respond_data *) cbuserdata;
323 /* desired_partnum can be set to "_HUNT_" to have it just look for
324 * the first part with a content type of text/calendar. Otherwise
325 * we have to only process the right one.
327 if (strcasecmp(ird->desired_partnum, "_HUNT_")) {
328 if (strcasecmp(partnum, ird->desired_partnum)) {
333 if ( (strcasecmp(cbtype, "text/calendar"))
334 && (strcasecmp(cbtype, "application/ics")) ) {
338 if (ird->cal != NULL) {
339 icalcomponent_free(ird->cal);
343 ird->cal = icalcomponent_new_from_string(content);
348 * Respond to a meeting request.
350 void ical_respond(long msgnum, char *partnum, char *action) {
351 struct CtdlMessage *msg = NULL;
352 struct ical_respond_data ird;
355 (strcasecmp(action, "accept"))
356 && (strcasecmp(action, "decline"))
358 cprintf("%d Action must be 'accept' or 'decline'\n",
359 ERROR + ILLEGAL_VALUE
364 msg = CtdlFetchMessage(msgnum, 1, 1);
366 cprintf("%d Message %ld not found.\n",
367 ERROR + ILLEGAL_VALUE,
373 memset(&ird, 0, sizeof ird);
374 strcpy(ird.desired_partnum, partnum);
375 mime_parser(CM_RANGE(msg, eMesageText),
376 *ical_locate_part, /* callback function */
378 (void *) &ird, /* user data */
382 /* We're done with the incoming message, because we now have a
383 * calendar object in memory.
388 * Here is the real meat of this function. Handle the event.
390 if (ird.cal != NULL) {
391 /* Save this in the user's calendar if necessary */
392 if (!strcasecmp(action, "accept")) {
393 ical_write_to_cal(&CC->user, ird.cal);
396 /* Send a reply if necessary */
397 if (icalcomponent_get_method(ird.cal) == ICAL_METHOD_REQUEST) {
398 ical_send_a_reply(ird.cal, action);
401 /* We used to delete the invitation after handling it.
402 * We don't do that anymore, but here is the code that handled it:
403 * CtdlDeleteMessages(CC->room.QRname, &msgnum, 1, "");
406 /* Free the memory we allocated and return a response. */
407 icalcomponent_free(ird.cal);
409 cprintf("%d ok\n", CIT_OK);
413 cprintf("%d No calendar object found\n", ERROR + ROOM_NOT_FOUND);
417 /* should never get here */
422 * Figure out the UID of the calendar event being referred to in a
423 * REPLY object. This function is recursive.
425 void ical_learn_uid_of_reply(char *uidbuf, icalcomponent *cal) {
426 icalcomponent *subcomponent;
429 /* If this object is a REPLY, then extract the UID. */
430 if (icalcomponent_isa(cal) == ICAL_VEVENT_COMPONENT) {
431 p = icalcomponent_get_first_property(cal, ICAL_UID_PROPERTY);
433 strcpy(uidbuf, icalproperty_get_comment(p));
437 /* Otherwise, recurse through any VEVENT subcomponents. We do NOT want the
438 * UID of the reply; we want the UID of the invitation being replied to.
440 for (subcomponent = icalcomponent_get_first_component(cal, ICAL_VEVENT_COMPONENT);
441 subcomponent != NULL;
442 subcomponent = icalcomponent_get_next_component(cal, ICAL_VEVENT_COMPONENT) ) {
443 ical_learn_uid_of_reply(uidbuf, subcomponent);
449 * ical_update_my_calendar_with_reply() refers to this callback function; when we
450 * locate the message containing the calendar event we're replying to, this function
451 * gets called. It basically just sticks the message number in a supplied buffer.
453 void ical_hunt_for_event_to_update(long msgnum, void *data) {
456 msgnumptr = (long *) data;
461 struct original_event_container {
466 * Callback function for mime parser that hunts for calendar content types
467 * and turns them into calendar objects (called by ical_update_my_calendar_with_reply()
468 * to fetch the object being updated)
470 void ical_locate_original_event(char *name, char *filename, char *partnum, char *disp,
471 void *content, char *cbtype, char *cbcharset, size_t length, char *encoding,
472 char *cbid, void *cbuserdata) {
474 struct original_event_container *oec = NULL;
476 if ( (strcasecmp(cbtype, "text/calendar"))
477 && (strcasecmp(cbtype, "application/ics")) ) {
480 oec = (struct original_event_container *) cbuserdata;
481 if (oec->c != NULL) {
482 icalcomponent_free(oec->c);
484 oec->c = icalcomponent_new_from_string(content);
489 * Merge updated attendee information from a REPLY into an existing event.
491 void ical_merge_attendee_reply(icalcomponent *event, icalcomponent *reply) {
493 icalproperty *e_attendee, *r_attendee;
495 /* First things first. If we're not looking at a VEVENT component,
496 * recurse through subcomponents until we find one.
498 if (icalcomponent_isa(event) != ICAL_VEVENT_COMPONENT) {
499 for (c = icalcomponent_get_first_component(event, ICAL_VEVENT_COMPONENT);
501 c = icalcomponent_get_next_component(event, ICAL_VEVENT_COMPONENT) ) {
502 ical_merge_attendee_reply(c, reply);
507 /* Now do the same thing with the reply.
509 if (icalcomponent_isa(reply) != ICAL_VEVENT_COMPONENT) {
510 for (c = icalcomponent_get_first_component(reply, ICAL_VEVENT_COMPONENT);
512 c = icalcomponent_get_next_component(reply, ICAL_VEVENT_COMPONENT) ) {
513 ical_merge_attendee_reply(event, c);
518 /* Clone the reply, because we're going to rip its guts out. */
519 reply = icalcomponent_new_clone(reply);
521 /* At this point we're looking at the correct subcomponents.
522 * Iterate through the attendees looking for a match.
525 for (e_attendee = icalcomponent_get_first_property(event, ICAL_ATTENDEE_PROPERTY);
527 e_attendee = icalcomponent_get_next_property(event, ICAL_ATTENDEE_PROPERTY)) {
529 for (r_attendee = icalcomponent_get_first_property(reply, ICAL_ATTENDEE_PROPERTY);
531 r_attendee = icalcomponent_get_next_property(reply, ICAL_ATTENDEE_PROPERTY)) {
533 /* Check to see if these two attendees match...
536 e = icalproperty_get_attendee(e_attendee);
537 r = icalproperty_get_attendee(r_attendee);
542 /* ...and if they do, remove the attendee from the event
543 * and replace it with the attendee from the reply. (The
544 * reply's copy will have the same address, but an updated
547 icalcomponent_remove_property(event, e_attendee);
548 icalproperty_free(e_attendee);
549 icalcomponent_remove_property(reply, r_attendee);
550 icalcomponent_add_property(event, r_attendee);
552 /* Since we diddled both sets of attendees, we have to start
553 * the iteration over again. This will not create an infinite
554 * loop because we removed the attendee from the reply. (That's
555 * why we cloned the reply, and that's what we mean by "ripping
564 /* Free the *clone* of the reply. */
565 icalcomponent_free(reply);
570 * Handle an incoming RSVP (object with method==ICAL_METHOD_REPLY) for a
571 * calendar event. The object has already been deserialized for us; all
572 * we have to do here is hunt for the event in our calendar, merge in the
573 * updated attendee status, and save it again.
575 * This function returns 0 on success, 1 if the event was not found in the
576 * user's calendar, or 2 if an internal error occurred.
578 int ical_update_my_calendar_with_reply(icalcomponent *cal) {
580 char hold_rm[ROOMNAMELEN];
581 long msgnum_being_replaced = 0;
582 struct CtdlMessage *msg = NULL;
583 struct original_event_container oec;
584 icalcomponent *original_event;
585 char *serialized_event = NULL;
586 char roomname[ROOMNAMELEN];
587 char *message_text = NULL;
589 /* Figure out just what event it is we're dealing with */
590 strcpy(uid, "--==<< InVaLiD uId >>==--");
591 ical_learn_uid_of_reply(uid, cal);
592 syslog(LOG_DEBUG, "calendar: UID of event being replied to is <%s>", uid);
594 strcpy(hold_rm, CC->room.QRname); /* save current room */
596 if (CtdlGetRoom(&CC->room, USERCALENDARROOM) != 0) {
597 CtdlGetRoom(&CC->room, hold_rm);
598 syslog(LOG_ERR, "calendar: cannot get user calendar room");
603 * Look in the EUID index for a message with
604 * the Citadel EUID set to the value we're looking for. Since
605 * Citadel always sets the message EUID to the iCalendar UID of
606 * the event, this will work.
608 msgnum_being_replaced = CtdlLocateMessageByEuid(uid, &CC->room);
610 CtdlGetRoom(&CC->room, hold_rm); /* return to saved room */
612 syslog(LOG_DEBUG, "calendar: msgnum_being_replaced == %ld", msgnum_being_replaced);
613 if (msgnum_being_replaced == 0) {
614 return(1); /* no calendar event found */
617 /* Now we know the ID of the message containing the event being updated.
618 * We don't actually have to delete it; that'll get taken care of by the
619 * server when we save another event with the same UID. This just gives
620 * us the ability to load the event into memory so we can diddle the
623 msg = CtdlFetchMessage(msgnum_being_replaced, 1, 1);
625 return(2); /* internal error */
628 mime_parser(CM_RANGE(msg, eMesageText),
629 *ical_locate_original_event, /* callback function */
631 &oec, /* user data */
636 original_event = oec.c;
637 if (original_event == NULL) {
638 syslog(LOG_ERR, "calendar: original_component is NULL");
642 /* Merge the attendee's updated status into the event */
643 ical_merge_attendee_reply(original_event, cal);
646 serialized_event = icalcomponent_as_ical_string_r(original_event);
647 icalcomponent_free(original_event); /* Don't need this anymore. */
648 if (serialized_event == NULL) return(2);
650 CtdlMailboxName(roomname, sizeof roomname, &CC->user, USERCALENDARROOM);
652 message_text = malloc(strlen(serialized_event) + SIZ);
653 if (message_text != NULL) {
654 sprintf(message_text,
655 "Content-type: text/calendar; charset=\"utf-8\"\r\n\r\n%s\r\n",
659 msg = CtdlMakeMessage(&CC->user,
660 "", /* No recipient */
661 "", /* No recipient */
672 CIT_ICAL->avoid_sending_invitations = 1;
673 CtdlSubmitMsg(msg, NULL, roomname, QP_EADDR);
675 CIT_ICAL->avoid_sending_invitations = 0;
678 free(serialized_event);
684 * Handle an incoming RSVP for an event. (This is the server subcommand part; it
685 * simply extracts the calendar object from the message, deserializes it, and
686 * passes it up to ical_update_my_calendar_with_reply() for processing.
688 void ical_handle_rsvp(long msgnum, char *partnum, char *action) {
689 struct CtdlMessage *msg = NULL;
690 struct ical_respond_data ird;
694 (strcasecmp(action, "update"))
695 && (strcasecmp(action, "ignore"))
697 cprintf("%d Action must be 'update' or 'ignore'\n",
698 ERROR + ILLEGAL_VALUE
703 msg = CtdlFetchMessage(msgnum, 1, 1);
705 cprintf("%d Message %ld not found.\n",
706 ERROR + ILLEGAL_VALUE,
712 memset(&ird, 0, sizeof ird);
713 strcpy(ird.desired_partnum, partnum);
714 mime_parser(CM_RANGE(msg, eMesageText),
715 *ical_locate_part, /* callback function */
717 (void *) &ird, /* user data */
721 /* We're done with the incoming message, because we now have a
722 * calendar object in memory.
727 * Here is the real meat of this function. Handle the event.
729 if (ird.cal != NULL) {
730 /* Update the user's calendar if necessary */
731 if (!strcasecmp(action, "update")) {
732 ret = ical_update_my_calendar_with_reply(ird.cal);
734 cprintf("%d Your calendar has been updated with this reply.\n",
738 cprintf("%d This event does not exist in your calendar.\n",
739 ERROR + FILE_NOT_FOUND);
742 cprintf("%d An internal error occurred.\n",
743 ERROR + INTERNAL_ERROR);
747 cprintf("%d This reply has been ignored.\n", CIT_OK);
750 /* Now that we've processed this message, we don't need it
751 * anymore. So delete it. (Don't do this anymore.)
752 CtdlDeleteMessages(CC->room.QRname, &msgnum, 1, "");
755 /* Free the memory we allocated and return a response. */
756 icalcomponent_free(ird.cal);
761 cprintf("%d No calendar object found\n", ERROR + ROOM_NOT_FOUND);
765 /* should never get here */
770 * Search for a property in both the top level and in a VEVENT subcomponent
772 icalproperty *ical_ctdl_get_subprop(
774 icalproperty_kind which_prop
779 p = icalcomponent_get_first_property(cal, which_prop);
781 c = icalcomponent_get_first_component(cal,
782 ICAL_VEVENT_COMPONENT);
784 p = icalcomponent_get_first_property(c, which_prop);
792 * Check to see if two events overlap. Returns nonzero if they do.
793 * (This function is used in both Citadel and WebCit. If you change it in
794 * one place, change it in the other. Better yet, put it in a library.)
796 int ical_ctdl_is_overlap(
797 struct icaltimetype t1start,
798 struct icaltimetype t1end,
799 struct icaltimetype t2start,
800 struct icaltimetype t2end
802 if (icaltime_is_null_time(t1start)) return(0);
803 if (icaltime_is_null_time(t2start)) return(0);
805 /* if either event lacks end time, assume end = start */
806 if (icaltime_is_null_time(t1end))
807 memcpy(&t1end, &t1start, sizeof(struct icaltimetype));
809 if (t1end.is_date && icaltime_compare(t1start, t1end)) {
811 * the end date is non-inclusive so adjust it by one
812 * day because our test is inclusive, note that a day is
813 * not too much because we are talking about all day
815 * if start = end we assume that nevertheless the whole
818 icaltime_adjust(&t1end, -1, 0, 0, 0);
822 if (icaltime_is_null_time(t2end))
823 memcpy(&t2end, &t2start, sizeof(struct icaltimetype));
825 if (t2end.is_date && icaltime_compare(t2start, t2end)) {
826 icaltime_adjust(&t2end, -1, 0, 0, 0);
830 /* First, check for all-day events */
831 if (t1start.is_date || t2start.is_date) {
832 /* If event 1 ends before event 2 starts, we're in the clear. */
833 if (icaltime_compare_date_only(t1end, t2start) < 0) return(0);
835 /* If event 2 ends before event 1 starts, we're also ok. */
836 if (icaltime_compare_date_only(t2end, t1start) < 0) return(0);
841 /* syslog(LOG_DEBUG, "Comparing t1start %d:%d t1end %d:%d t2start %d:%d t2end %d:%d",
842 t1start.hour, t1start.minute, t1end.hour, t1end.minute,
843 t2start.hour, t2start.minute, t2end.hour, t2end.minute);
846 /* Now check for overlaps using date *and* time. */
848 /* If event 1 ends before event 2 starts, we're in the clear. */
849 if (icaltime_compare(t1end, t2start) <= 0) return(0);
850 /* syslog(LOG_DEBUG, "calendar: first passed"); */
852 /* If event 2 ends before event 1 starts, we're also ok. */
853 if (icaltime_compare(t2end, t1start) <= 0) return(0);
854 /* syslog(LOG_DEBUG, "calendar: second passed"); */
856 /* Otherwise, they overlap. */
862 * Phase 6 of "hunt for conflicts"
863 * called by ical_conflicts_phase5()
865 * Now both the proposed and existing events have been boiled down to start and end times.
866 * Check for overlap and output any conflicts.
868 * Returns nonzero if a conflict was reported. This allows the caller to stop iterating.
870 int ical_conflicts_phase6(struct icaltimetype t1start,
871 struct icaltimetype t1end,
872 struct icaltimetype t2start,
873 struct icaltimetype t2end,
874 long existing_msgnum,
875 char *conflict_event_uid,
876 char *conflict_event_summary,
879 int conflict_reported = 0;
883 tt = icaltime_as_timet_with_zone(t1start, t1start.zone);
884 syslog(LOG_DEBUG, "PROPOSED START: %s", ctime(&tt));
885 tt = icaltime_as_timet_with_zone(t1end, t1end.zone);
886 syslog(LOG_DEBUG, " PROPOSED END: %s", ctime(&tt));
887 tt = icaltime_as_timet_with_zone(t2start, t2start.zone);
888 syslog(LOG_DEBUG, "EXISTING START: %s", ctime(&tt));
889 tt = icaltime_as_timet_with_zone(t2end, t2end.zone);
890 syslog(LOG_DEBUG, " EXISTING END: %s", ctime(&tt));
893 /* compare and output */
895 if (ical_ctdl_is_overlap(t1start, t1end, t2start, t2end)) {
896 cprintf("%ld||%s|%s|%d|\n",
899 conflict_event_summary,
900 ( (!IsEmptyStr(compare_uid)
901 &&(!strcasecmp(compare_uid,
902 conflict_event_uid))) ? 1 : 0
905 conflict_reported = 1;
908 return(conflict_reported);
913 * Phase 5 of "hunt for conflicts"
914 * Called by ical_conflicts_phase4()
916 * We have the proposed event boiled down to start and end times.
917 * Now check it against an existing event.
919 void ical_conflicts_phase5(struct icaltimetype t1start,
920 struct icaltimetype t1end,
921 icalcomponent *existing_event,
922 long existing_msgnum,
925 char conflict_event_uid[SIZ];
926 char conflict_event_summary[SIZ];
927 struct icaltimetype t2start, t2end;
930 /* recur variables */
931 icalproperty *rrule = NULL;
932 struct icalrecurrencetype recur;
933 icalrecur_iterator *ritr = NULL;
934 struct icaldurationtype dur;
938 strcpy(conflict_event_uid, "");
939 strcpy(conflict_event_summary, "");
940 t2start = icaltime_null_time();
941 t2end = icaltime_null_time();
943 /* existing event stuff */
944 p = ical_ctdl_get_subprop(existing_event, ICAL_DTSTART_PROPERTY);
945 if (p == NULL) return;
946 if (p != NULL) t2start = icalproperty_get_dtstart(p);
947 if (icaltime_is_utc(t2start)) {
948 t2start.zone = icaltimezone_get_utc_timezone();
951 t2start.zone = icalcomponent_get_timezone(existing_event,
952 icalparameter_get_tzid(
953 icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER)
957 t2start.zone = get_default_icaltimezone();
961 p = ical_ctdl_get_subprop(existing_event, ICAL_DTEND_PROPERTY);
963 t2end = icalproperty_get_dtend(p);
965 if (icaltime_is_utc(t2end)) {
966 t2end.zone = icaltimezone_get_utc_timezone();
969 t2end.zone = icalcomponent_get_timezone(existing_event,
970 icalparameter_get_tzid(
971 icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER)
975 t2end.zone = get_default_icaltimezone();
978 dur = icaltime_subtract(t2end, t2start);
981 memset (&dur, 0, sizeof(struct icaldurationtype));
984 rrule = ical_ctdl_get_subprop(existing_event, ICAL_RRULE_PROPERTY);
986 recur = icalproperty_get_rrule(rrule);
987 ritr = icalrecur_iterator_new(recur, t2start);
991 p = ical_ctdl_get_subprop(existing_event, ICAL_UID_PROPERTY);
993 strcpy(conflict_event_uid, icalproperty_get_comment(p));
996 p = ical_ctdl_get_subprop(existing_event, ICAL_SUMMARY_PROPERTY);
998 strcpy(conflict_event_summary, icalproperty_get_comment(p));
1001 if (ical_conflicts_phase6(t1start, t1end, t2start, t2end,
1002 existing_msgnum, conflict_event_uid, conflict_event_summary, compare_uid))
1004 num_recur = MAX_RECUR + 1; /* force it out of scope, no need to continue */
1008 t2start = icalrecur_iterator_next(ritr);
1009 if (!icaltime_is_null_time(t2end)) {
1010 const icaltimezone *hold_zone = t2end.zone;
1011 t2end = icaltime_add(t2start, dur);
1012 t2end.zone = hold_zone;
1017 if (icaltime_compare(t2start, t1end) < 0) {
1018 num_recur = MAX_RECUR + 1; /* force it out of scope */
1021 } while ( (rrule) && (!icaltime_is_null_time(t2start)) && (num_recur < MAX_RECUR) );
1022 icalrecur_iterator_free(ritr);
1027 * Phase 4 of "hunt for conflicts"
1028 * Called by ical_hunt_for_conflicts_backend()
1030 * At this point we've got it boiled down to two icalcomponent events in memory.
1031 * If they conflict, output something to the client.
1033 void ical_conflicts_phase4(icalcomponent *proposed_event,
1034 icalcomponent *existing_event,
1035 long existing_msgnum)
1037 struct icaltimetype t1start, t1end;
1039 char compare_uid[SIZ];
1041 /* recur variables */
1042 icalproperty *rrule = NULL;
1043 struct icalrecurrencetype recur;
1044 icalrecur_iterator *ritr = NULL;
1045 struct icaldurationtype dur;
1048 /* initialization */
1049 t1end = icaltime_null_time();
1050 *compare_uid = '\0';
1052 /* proposed event stuff */
1054 p = ical_ctdl_get_subprop(proposed_event, ICAL_DTSTART_PROPERTY);
1058 t1start = icalproperty_get_dtstart(p);
1060 if (icaltime_is_utc(t1start)) {
1061 t1start.zone = icaltimezone_get_utc_timezone();
1064 t1start.zone = icalcomponent_get_timezone(proposed_event,
1065 icalparameter_get_tzid(
1066 icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER)
1069 if (!t1start.zone) {
1070 t1start.zone = get_default_icaltimezone();
1074 p = ical_ctdl_get_subprop(proposed_event, ICAL_DTEND_PROPERTY);
1076 t1end = icalproperty_get_dtend(p);
1078 if (icaltime_is_utc(t1end)) {
1079 t1end.zone = icaltimezone_get_utc_timezone();
1082 t1end.zone = icalcomponent_get_timezone(proposed_event,
1083 icalparameter_get_tzid(
1084 icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER)
1088 t1end.zone = get_default_icaltimezone();
1092 dur = icaltime_subtract(t1end, t1start);
1095 memset (&dur, 0, sizeof(struct icaldurationtype));
1098 rrule = ical_ctdl_get_subprop(proposed_event, ICAL_RRULE_PROPERTY);
1100 recur = icalproperty_get_rrule(rrule);
1101 ritr = icalrecur_iterator_new(recur, t1start);
1104 p = ical_ctdl_get_subprop(proposed_event, ICAL_UID_PROPERTY);
1106 strcpy(compare_uid, icalproperty_get_comment(p));
1110 ical_conflicts_phase5(t1start, t1end, existing_event, existing_msgnum, compare_uid);
1113 t1start = icalrecur_iterator_next(ritr);
1114 if (!icaltime_is_null_time(t1end)) {
1115 const icaltimezone *hold_zone = t1end.zone;
1116 t1end = icaltime_add(t1start, dur);
1117 t1end.zone = hold_zone;
1122 } while ( (rrule) && (!icaltime_is_null_time(t1start)) && (num_recur < MAX_RECUR) );
1123 icalrecur_iterator_free(ritr);
1128 * Phase 3 of "hunt for conflicts"
1129 * Called by ical_hunt_for_conflicts()
1131 void ical_hunt_for_conflicts_backend(long msgnum, void *data) {
1132 icalcomponent *proposed_event;
1133 struct CtdlMessage *msg = NULL;
1134 struct ical_respond_data ird;
1136 proposed_event = (icalcomponent *)data;
1138 msg = CtdlFetchMessage(msgnum, 1, 1);
1139 if (msg == NULL) return;
1140 memset(&ird, 0, sizeof ird);
1141 strcpy(ird.desired_partnum, "_HUNT_");
1142 mime_parser(CM_RANGE(msg, eMesageText),
1143 *ical_locate_part, /* callback function */
1145 (void *) &ird, /* user data */
1150 if (ird.cal == NULL) return;
1152 ical_conflicts_phase4(proposed_event, ird.cal, msgnum);
1153 icalcomponent_free(ird.cal);
1158 * Phase 2 of "hunt for conflicts" operation.
1159 * At this point we have a calendar object which represents the VEVENT that
1160 * is proposed for addition to the calendar. Now hunt through the user's
1161 * calendar room, and output zero or more existing VEVENTs which conflict
1164 void ical_hunt_for_conflicts(icalcomponent *cal) {
1165 char hold_rm[ROOMNAMELEN];
1167 strcpy(hold_rm, CC->room.QRname); /* save current room */
1169 if (CtdlGetRoom(&CC->room, USERCALENDARROOM) != 0) {
1170 CtdlGetRoom(&CC->room, hold_rm);
1171 cprintf("%d You do not have a calendar.\n", ERROR + ROOM_NOT_FOUND);
1175 cprintf("%d Conflicting events:\n", LISTING_FOLLOWS);
1177 CtdlForEachMessage(MSGS_ALL, 0, NULL,
1180 ical_hunt_for_conflicts_backend,
1185 CtdlGetRoom(&CC->room, hold_rm); /* return to saved room */
1191 * Hunt for conflicts (Phase 1 -- retrieve the object and call Phase 2)
1193 void ical_conflicts(long msgnum, char *partnum) {
1194 struct CtdlMessage *msg = NULL;
1195 struct ical_respond_data ird;
1197 msg = CtdlFetchMessage(msgnum, 1, 1);
1199 cprintf("%d Message %ld not found\n",
1200 ERROR + ILLEGAL_VALUE,
1206 memset(&ird, 0, sizeof ird);
1207 strcpy(ird.desired_partnum, partnum);
1208 mime_parser(CM_RANGE(msg, eMesageText),
1209 *ical_locate_part, /* callback function */
1211 (void *) &ird, /* user data */
1217 if (ird.cal != NULL) {
1218 ical_hunt_for_conflicts(ird.cal);
1219 icalcomponent_free(ird.cal);
1223 cprintf("%d No calendar object found\n", ERROR + ROOM_NOT_FOUND);
1228 * Look for busy time in a VEVENT and add it to the supplied VFREEBUSY.
1230 * fb The VFREEBUSY component to which we are appending
1231 * top_level_cal The top-level VCALENDAR component which contains a VEVENT to be added
1233 void ical_add_to_freebusy(icalcomponent *fb, icalcomponent *top_level_cal) {
1237 struct icalperiodtype this_event_period = icalperiodtype_null_period();
1238 icaltimetype dtstart;
1241 /* recur variables */
1242 icalproperty *rrule = NULL;
1243 struct icalrecurrencetype recur;
1244 icalrecur_iterator *ritr = NULL;
1245 struct icaldurationtype dur;
1248 if (!top_level_cal) return;
1250 /* Find the VEVENT component containing an event */
1251 cal = icalcomponent_get_first_component(top_level_cal, ICAL_VEVENT_COMPONENT);
1254 /* If this event is not opaque, the user isn't publishing it as
1255 * busy time, so don't bother doing anything else.
1257 p = icalcomponent_get_first_property(cal, ICAL_TRANSP_PROPERTY);
1259 v = icalproperty_get_value(p);
1261 if (icalvalue_get_transp(v) != ICAL_TRANSP_OPAQUE) {
1268 * Now begin calculating the event start and end times.
1270 p = icalcomponent_get_first_property(cal, ICAL_DTSTART_PROPERTY);
1272 dtstart = icalproperty_get_dtstart(p);
1274 if (icaltime_is_utc(dtstart)) {
1275 dtstart.zone = icaltimezone_get_utc_timezone();
1278 dtstart.zone = icalcomponent_get_timezone(top_level_cal,
1279 icalparameter_get_tzid(
1280 icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER)
1283 if (!dtstart.zone) {
1284 dtstart.zone = get_default_icaltimezone();
1288 dtend = icalcomponent_get_dtend(cal);
1289 if (!icaltime_is_null_time(dtend)) {
1290 dur = icaltime_subtract(dtend, dtstart);
1293 memset (&dur, 0, sizeof(struct icaldurationtype));
1296 /* Is a recurrence specified? If so, get ready to process it... */
1297 rrule = ical_ctdl_get_subprop(cal, ICAL_RRULE_PROPERTY);
1299 recur = icalproperty_get_rrule(rrule);
1300 ritr = icalrecur_iterator_new(recur, dtstart);
1304 /* Convert the DTSTART and DTEND properties to an icalperiod. */
1305 this_event_period.start = dtstart;
1307 if (!icaltime_is_null_time(dtend)) {
1308 this_event_period.end = dtend;
1311 /* Convert the timestamps to UTC. It's ok to do this because we've already expanded
1312 * recurrences and this data is never going to get used again.
1314 this_event_period.start = icaltime_convert_to_zone(
1315 this_event_period.start,
1316 icaltimezone_get_utc_timezone()
1318 this_event_period.end = icaltime_convert_to_zone(
1319 this_event_period.end,
1320 icaltimezone_get_utc_timezone()
1324 icalcomponent_add_property(fb, icalproperty_new_freebusy(this_event_period));
1326 /* Make sure the DTSTART property of the freebusy *list* is set to
1327 * the DTSTART property of the *earliest event*.
1329 p = icalcomponent_get_first_property(fb, ICAL_DTSTART_PROPERTY);
1331 icalcomponent_set_dtstart(fb, this_event_period.start);
1334 if (icaltime_compare(this_event_period.start, icalcomponent_get_dtstart(fb)) < 0) {
1335 icalcomponent_set_dtstart(fb, this_event_period.start);
1339 /* Make sure the DTEND property of the freebusy *list* is set to
1340 * the DTEND property of the *latest event*.
1342 p = icalcomponent_get_first_property(fb, ICAL_DTEND_PROPERTY);
1344 icalcomponent_set_dtend(fb, this_event_period.end);
1347 if (icaltime_compare(this_event_period.end, icalcomponent_get_dtend(fb)) > 0) {
1348 icalcomponent_set_dtend(fb, this_event_period.end);
1353 dtstart = icalrecur_iterator_next(ritr);
1354 if (!icaltime_is_null_time(dtend)) {
1355 dtend = icaltime_add(dtstart, dur);
1356 dtend.zone = dtstart.zone;
1361 } while ( (rrule) && (!icaltime_is_null_time(dtstart)) && (num_recur < MAX_RECUR) ) ;
1362 icalrecur_iterator_free(ritr);
1367 * Backend for ical_freebusy()
1369 * This function simply loads the messages in the user's calendar room,
1370 * which contain VEVENTs, then strips them of all non-freebusy data, and
1371 * adds them to the supplied VCALENDAR.
1374 void ical_freebusy_backend(long msgnum, void *data) {
1376 struct CtdlMessage *msg = NULL;
1377 struct ical_respond_data ird;
1379 fb = (icalcomponent *)data; /* User-supplied data will be the VFREEBUSY component */
1381 msg = CtdlFetchMessage(msgnum, 1, 1);
1382 if (msg == NULL) return;
1383 memset(&ird, 0, sizeof ird);
1384 strcpy(ird.desired_partnum, "_HUNT_");
1385 mime_parser(CM_RANGE(msg, eMesageText),
1386 *ical_locate_part, /* callback function */
1388 (void *) &ird, /* user data */
1394 ical_add_to_freebusy(fb, ird.cal); /* Add VEVENT times to VFREEBUSY */
1395 icalcomponent_free(ird.cal);
1401 * Grab another user's free/busy times
1403 void ical_freebusy(char *who) {
1404 struct ctdluser usbuf;
1405 char calendar_room_name[ROOMNAMELEN];
1406 char hold_rm[ROOMNAMELEN];
1407 char *serialized_request = NULL;
1408 icalcomponent *encaps = NULL;
1409 icalcomponent *fb = NULL;
1410 int found_user = (-1);
1411 recptypes *recp = NULL;
1416 int config_lines = 0;
1418 /* First try an exact match. */
1419 found_user = CtdlGetUser(&usbuf, who);
1421 /* If not found, try it as an unqualified email address. */
1422 if (found_user != 0) {
1424 recp = validate_recipients(buf, NULL, 0);
1425 syslog(LOG_DEBUG, "calendar: trying <%s>", buf);
1427 if (recp->num_local == 1) {
1428 found_user = CtdlGetUser(&usbuf, recp->recp_local);
1430 free_recipients(recp);
1434 /* If still not found, try it as an address qualified with the
1435 * primary FQDN of this Citadel node.
1437 if (found_user != 0) {
1438 snprintf(buf, sizeof buf, "%s@%s", who, CtdlGetConfigStr("c_fqdn"));
1439 syslog(LOG_DEBUG, "calendar: trying <%s>", buf);
1440 recp = validate_recipients(buf, NULL, 0);
1442 if (recp->num_local == 1) {
1443 found_user = CtdlGetUser(&usbuf, recp->recp_local);
1445 free_recipients(recp);
1449 /* Still not found? Try qualifying it with every domain we
1450 * might have addresses in.
1452 if (found_user != 0) {
1453 config_lines = num_tokens(inetcfg, '\n');
1454 for (i=0; ((i < config_lines) && (found_user != 0)); ++i) {
1455 extract_token(buf, inetcfg, i, '\n', sizeof buf);
1456 extract_token(host, buf, 0, '|', sizeof host);
1457 extract_token(type, buf, 1, '|', sizeof type);
1459 if ( (!strcasecmp(type, "localhost"))
1460 || (!strcasecmp(type, "directory")) ) {
1461 snprintf(buf, sizeof buf, "%s@%s", who, host);
1462 syslog(LOG_DEBUG, "calendar: trying <%s>", buf);
1463 recp = validate_recipients(buf, NULL, 0);
1465 if (recp->num_local == 1) {
1466 found_user = CtdlGetUser(&usbuf, recp->recp_local);
1468 free_recipients(recp);
1474 if (found_user != 0) {
1475 cprintf("%d No such user.\n", ERROR + NO_SUCH_USER);
1479 CtdlMailboxName(calendar_room_name, sizeof calendar_room_name,
1480 &usbuf, USERCALENDARROOM);
1482 strcpy(hold_rm, CC->room.QRname); /* save current room */
1484 if (CtdlGetRoom(&CC->room, calendar_room_name) != 0) {
1485 cprintf("%d Cannot open calendar\n", ERROR + ROOM_NOT_FOUND);
1486 CtdlGetRoom(&CC->room, hold_rm);
1490 /* Create a VFREEBUSY subcomponent */
1491 syslog(LOG_DEBUG, "calendar: creating VFREEBUSY component");
1492 fb = icalcomponent_new_vfreebusy();
1494 cprintf("%d Internal error: cannot allocate memory.\n", ERROR + INTERNAL_ERROR);
1495 CtdlGetRoom(&CC->room, hold_rm);
1499 /* Set the method to PUBLISH */
1500 icalcomponent_set_method(fb, ICAL_METHOD_PUBLISH);
1502 /* Set the DTSTAMP to right now. */
1503 icalcomponent_set_dtstamp(fb, icaltime_from_timet_with_zone(time(NULL), 0, icaltimezone_get_utc_timezone()));
1505 /* Add the user's email address as ORGANIZER */
1506 sprintf(buf, "MAILTO:%s", who);
1507 if (strchr(buf, '@') == NULL) {
1509 strcat(buf, CtdlGetConfigStr("c_fqdn"));
1511 for (i=0; buf[i]; ++i) {
1512 if (buf[i]==' ') buf[i] = '_';
1514 icalcomponent_add_property(fb, icalproperty_new_organizer(buf));
1516 /* Add busy time from events */
1517 syslog(LOG_DEBUG, "calendar: adding busy time from events");
1518 CtdlForEachMessage(MSGS_ALL, 0, NULL, NULL, NULL, ical_freebusy_backend, (void *)fb );
1520 /* If values for DTSTART and DTEND are still not present, set them
1521 * to yesterday and tomorrow as default values.
1523 if (icalcomponent_get_first_property(fb, ICAL_DTSTART_PROPERTY) == NULL) {
1524 icalcomponent_set_dtstart(fb, icaltime_from_timet_with_zone(time(NULL)-86400L, 0, icaltimezone_get_utc_timezone()));
1526 if (icalcomponent_get_first_property(fb, ICAL_DTEND_PROPERTY) == NULL) {
1527 icalcomponent_set_dtend(fb, icaltime_from_timet_with_zone(time(NULL)+86400L, 0, icaltimezone_get_utc_timezone()));
1530 /* Put the freebusy component into the calendar component */
1531 syslog(LOG_DEBUG, "calendar: encapsulating");
1532 encaps = ical_encapsulate_subcomponent(fb);
1533 if (encaps == NULL) {
1534 icalcomponent_free(fb);
1535 cprintf("%d Internal error: cannot allocate memory.\n",
1536 ERROR + INTERNAL_ERROR);
1537 CtdlGetRoom(&CC->room, hold_rm);
1541 /* Set the method to PUBLISH */
1542 syslog(LOG_DEBUG, "calendar: setting method");
1543 icalcomponent_set_method(encaps, ICAL_METHOD_PUBLISH);
1546 syslog(LOG_DEBUG, "calendar: serializing");
1547 serialized_request = icalcomponent_as_ical_string_r(encaps);
1548 icalcomponent_free(encaps); /* Don't need this anymore. */
1550 cprintf("%d Free/busy for %s\n", LISTING_FOLLOWS, usbuf.fullname);
1551 if (serialized_request != NULL) {
1552 client_write(serialized_request, strlen(serialized_request));
1553 free(serialized_request);
1557 /* Go back to the room from which we came... */
1558 CtdlGetRoom(&CC->room, hold_rm);
1563 * Backend for ical_getics()
1565 * This is a ForEachMessage() callback function that searches the current room
1566 * for calendar events and adds them each into one big calendar component.
1568 void ical_getics_backend(long msgnum, void *data) {
1569 icalcomponent *encaps, *c;
1570 struct CtdlMessage *msg = NULL;
1571 struct ical_respond_data ird;
1573 encaps = (icalcomponent *)data;
1574 if (encaps == NULL) return;
1576 /* Look for the calendar event... */
1578 msg = CtdlFetchMessage(msgnum, 1, 1);
1579 if (msg == NULL) return;
1580 memset(&ird, 0, sizeof ird);
1581 strcpy(ird.desired_partnum, "_HUNT_");
1582 mime_parser(CM_RANGE(msg, eMesageText),
1583 *ical_locate_part, /* callback function */
1585 (void *) &ird, /* user data */
1590 if (ird.cal == NULL) return;
1592 /* Here we go: put the VEVENT into the VCALENDAR. We now no longer
1593 * are responsible for "the_request"'s memory -- it will be freed
1594 * when we free "encaps".
1597 /* If the top-level component is *not* a VCALENDAR, we can drop it right
1598 * in. This will almost never happen.
1600 if (icalcomponent_isa(ird.cal) != ICAL_VCALENDAR_COMPONENT) {
1601 icalcomponent_add_component(encaps, ird.cal);
1604 * In the more likely event that we're looking at a VCALENDAR with the VEVENT
1605 * and other components encapsulated inside, we have to extract them.
1608 for (c = icalcomponent_get_first_component(ird.cal, ICAL_ANY_COMPONENT);
1610 c = icalcomponent_get_next_component(ird.cal, ICAL_ANY_COMPONENT)) {
1612 /* For VTIMEZONE components, suppress duplicates of the same tzid */
1614 if (icalcomponent_isa(c) == ICAL_VTIMEZONE_COMPONENT) {
1615 icalproperty *p = icalcomponent_get_first_property(c, ICAL_TZID_PROPERTY);
1617 const char *tzid = icalproperty_get_tzid(p);
1618 if (!icalcomponent_get_timezone(encaps, tzid)) {
1619 icalcomponent_add_component(encaps,
1620 icalcomponent_new_clone(c));
1625 /* All other types of components can go in verbatim */
1627 icalcomponent_add_component(encaps, icalcomponent_new_clone(c));
1630 icalcomponent_free(ird.cal);
1636 * Retrieve all of the calendar items in the current room, and output them
1637 * as a single icalendar object.
1639 void ical_getics(void)
1641 icalcomponent *encaps = NULL;
1644 if ( (CC->room.QRdefaultview != VIEW_CALENDAR)
1645 &&(CC->room.QRdefaultview != VIEW_TASKS) ) {
1646 cprintf("%d Not a calendar room\n", ERROR+NOT_HERE);
1647 return; /* Not an iCalendar-centric room */
1650 encaps = icalcomponent_new_vcalendar();
1651 if (encaps == NULL) {
1652 syslog(LOG_ERR, "calendar: could not allocate component!");
1653 cprintf("%d Could not allocate memory\n", ERROR+INTERNAL_ERROR);
1657 cprintf("%d one big calendar\n", LISTING_FOLLOWS);
1659 /* Set the Product ID */
1660 icalcomponent_add_property(encaps, icalproperty_new_prodid(PRODID));
1662 /* Set the Version Number */
1663 icalcomponent_add_property(encaps, icalproperty_new_version("2.0"));
1665 /* Set the method to PUBLISH */
1666 icalcomponent_set_method(encaps, ICAL_METHOD_PUBLISH);
1668 /* Now go through the room encapsulating all calendar items. */
1669 CtdlForEachMessage(MSGS_ALL, 0, NULL,
1672 ical_getics_backend,
1676 ser = icalcomponent_as_ical_string_r(encaps);
1677 icalcomponent_free(encaps); /* Don't need this anymore. */
1678 client_write(ser, strlen(ser));
1685 * Helper callback function for ical_putics() to discover which TZID's we need.
1686 * Simply put the tzid name string into a hash table. After the callbacks are
1687 * done we'll go through them and attach the ones that we have.
1689 void ical_putics_grabtzids(icalparameter *param, void *data)
1691 const char *tzid = icalparameter_get_tzid(param);
1692 HashList *keys = (HashList *) data;
1694 if ( (keys) && (tzid) && (!IsEmptyStr(tzid)) ) {
1695 Put(keys, tzid, strlen(tzid), strdup(tzid), NULL);
1701 * Delete all of the calendar items in the current room, and replace them
1702 * with calendar items from a client-supplied data stream.
1704 void ical_putics(void)
1706 char *calstream = NULL;
1709 icalcomponent *encaps = NULL;
1710 HashList *tzidlist = NULL;
1716 /* Only allow this operation if we're in a room containing a calendar or tasks view */
1717 if ( (CC->room.QRdefaultview != VIEW_CALENDAR)
1718 &&(CC->room.QRdefaultview != VIEW_TASKS) ) {
1719 cprintf("%d Not a calendar room\n", ERROR+NOT_HERE);
1723 /* Only allow this operation if we have permission to overwrite the existing calendar */
1724 if (!CtdlDoIHavePermissionToDeleteMessagesFromThisRoom()) {
1725 cprintf("%d Permission denied.\n", ERROR+HIGHER_ACCESS_REQUIRED);
1729 cprintf("%d Transmit data now\n", SEND_LISTING);
1730 calstream = CtdlReadMessageBody(HKEY("000"), CtdlGetConfigLong("c_maxmsglen"), NULL, 0);
1731 if (calstream == NULL) {
1735 cal = icalcomponent_new_from_string(calstream);
1738 /* We got our data stream -- now do something with it. */
1740 /* Delete the existing messages in the room, because we are overwriting
1741 * the entire calendar with an entire new (or updated) calendar.
1742 * (Careful: this opens an S_ROOMS critical section!)
1744 CtdlDeleteMessages(CC->room.QRname, NULL, 0, "");
1746 /* If the top-level component is *not* a VCALENDAR, we can drop it right
1747 * in. This will almost never happen.
1749 if (icalcomponent_isa(cal) != ICAL_VCALENDAR_COMPONENT) {
1750 ical_write_to_cal(NULL, cal);
1753 * In the more likely event that we're looking at a VCALENDAR with the VEVENT
1754 * and other components encapsulated inside, we have to extract them.
1757 for (c = icalcomponent_get_first_component(cal, ICAL_ANY_COMPONENT);
1759 c = icalcomponent_get_next_component(cal, ICAL_ANY_COMPONENT)) {
1761 /* Non-VTIMEZONE components each get written as individual messages.
1762 * But we also need to attach the relevant VTIMEZONE components to them.
1764 if ( (icalcomponent_isa(c) != ICAL_VTIMEZONE_COMPONENT)
1765 && (encaps = icalcomponent_new_vcalendar()) ) {
1766 icalcomponent_add_property(encaps, icalproperty_new_prodid(PRODID));
1767 icalcomponent_add_property(encaps, icalproperty_new_version("2.0"));
1768 icalcomponent_set_method(encaps, ICAL_METHOD_PUBLISH);
1770 /* Attach any needed timezones here */
1771 tzidlist = NewHash(1, NULL);
1773 icalcomponent_foreach_tzid(c, ical_putics_grabtzids, tzidlist);
1775 HashPos = GetNewHashPos(tzidlist, 0);
1777 while (GetNextHashPos(tzidlist, HashPos, &len, &Key, &Value)) {
1778 syslog(LOG_DEBUG, "calendar: attaching timezone '%s'", (char*) Value);
1779 icaltimezone *t = NULL;
1781 /* First look for a timezone attached to the original calendar */
1782 t = icalcomponent_get_timezone(cal, Value);
1784 /* Try built-in tzdata if the right one wasn't attached */
1786 t = icaltimezone_get_builtin_timezone(Value);
1789 /* I've got a valid timezone to attach. */
1791 icalcomponent_add_component(encaps,
1792 icalcomponent_new_clone(
1793 icaltimezone_get_component(t)
1799 DeleteHashPos(&HashPos);
1800 DeleteHash(&tzidlist);
1802 /* Now attach the component itself (usually a VEVENT or VTODO) */
1803 icalcomponent_add_component(encaps, icalcomponent_new_clone(c));
1805 /* Write it to the message store */
1806 ical_write_to_cal(NULL, encaps);
1807 icalcomponent_free(encaps);
1812 icalcomponent_free(cal);
1817 * All Citadel calendar commands from the client come through here.
1819 void cmd_ical(char *argbuf)
1827 extract_token(subcmd, argbuf, 0, '|', sizeof subcmd);
1829 /* Allow "test" and "freebusy" subcommands without logging in. */
1831 if (!strcasecmp(subcmd, "test")) {
1832 cprintf("%d This server supports calendaring\n", CIT_OK);
1836 if (!strcasecmp(subcmd, "freebusy")) {
1837 extract_token(who, argbuf, 1, '|', sizeof who);
1842 if (!strcasecmp(subcmd, "sgi")) {
1843 CIT_ICAL->server_generated_invitations = (extract_int(argbuf, 1) ? 1 : 0) ;
1844 cprintf("%d %d\n", CIT_OK, CIT_ICAL->server_generated_invitations);
1848 if (CtdlAccessCheck(ac_logged_in)) return;
1850 if (!strcasecmp(subcmd, "respond")) {
1851 msgnum = extract_long(argbuf, 1);
1852 extract_token(partnum, argbuf, 2, '|', sizeof partnum);
1853 extract_token(action, argbuf, 3, '|', sizeof action);
1854 ical_respond(msgnum, partnum, action);
1858 if (!strcasecmp(subcmd, "handle_rsvp")) {
1859 msgnum = extract_long(argbuf, 1);
1860 extract_token(partnum, argbuf, 2, '|', sizeof partnum);
1861 extract_token(action, argbuf, 3, '|', sizeof action);
1862 ical_handle_rsvp(msgnum, partnum, action);
1866 if (!strcasecmp(subcmd, "conflicts")) {
1867 msgnum = extract_long(argbuf, 1);
1868 extract_token(partnum, argbuf, 2, '|', sizeof partnum);
1869 ical_conflicts(msgnum, partnum);
1873 if (!strcasecmp(subcmd, "getics")) {
1878 if (!strcasecmp(subcmd, "putics")) {
1883 cprintf("%d Invalid subcommand\n", ERROR + CMD_NOT_SUPPORTED);
1888 * We don't know if the calendar room exists so we just create it at login
1890 void ical_CtdlCreateRoom(void)
1895 /* Create the calendar room if it doesn't already exist */
1896 CtdlCreateRoom(USERCALENDARROOM, 4, "", 0, 1, 0, VIEW_CALENDAR);
1898 /* Set expiration policy to manual; otherwise objects will be lost! */
1899 if (CtdlGetRoomLock(&qr, USERCALENDARROOM)) {
1900 syslog(LOG_ERR, "calendar: couldn't get the user calendar room");
1903 qr.QRep.expire_mode = EXPIRE_MANUAL;
1904 qr.QRdefaultview = VIEW_CALENDAR; /* 3 = calendar view */
1905 CtdlPutRoomLock(&qr);
1907 /* Set the view to a calendar view */
1908 CtdlGetRelationship(&vbuf, &CC->user, &qr);
1909 vbuf.v_view = VIEW_CALENDAR;
1910 CtdlSetRelationship(&vbuf, &CC->user, &qr);
1912 /* Create the tasks list room if it doesn't already exist */
1913 CtdlCreateRoom(USERTASKSROOM, 4, "", 0, 1, 0, VIEW_TASKS);
1915 /* Set expiration policy to manual; otherwise objects will be lost! */
1916 if (CtdlGetRoomLock(&qr, USERTASKSROOM)) {
1917 syslog(LOG_ERR, "calendar: couldn't get the user calendar room!");
1920 qr.QRep.expire_mode = EXPIRE_MANUAL;
1921 qr.QRdefaultview = VIEW_TASKS;
1922 CtdlPutRoomLock(&qr);
1924 /* Set the view to a task list view */
1925 CtdlGetRelationship(&vbuf, &CC->user, &qr);
1926 vbuf.v_view = VIEW_TASKS;
1927 CtdlSetRelationship(&vbuf, &CC->user, &qr);
1929 /* Create the notes room if it doesn't already exist */
1930 CtdlCreateRoom(USERNOTESROOM, 4, "", 0, 1, 0, VIEW_NOTES);
1932 /* Set expiration policy to manual; otherwise objects will be lost! */
1933 if (CtdlGetRoomLock(&qr, USERNOTESROOM)) {
1934 syslog(LOG_ERR, "calendar: couldn't get the user calendar room!");
1937 qr.QRep.expire_mode = EXPIRE_MANUAL;
1938 qr.QRdefaultview = VIEW_NOTES;
1939 CtdlPutRoomLock(&qr);
1941 /* Set the view to a notes view */
1942 CtdlGetRelationship(&vbuf, &CC->user, &qr);
1943 vbuf.v_view = VIEW_NOTES;
1944 CtdlSetRelationship(&vbuf, &CC->user, &qr);
1951 * ical_send_out_invitations() is called by ical_saving_vevent() when it finds a VEVENT.
1953 * top_level_cal is the highest available level calendar object.
1954 * cal is the subcomponent containing the VEVENT.
1956 * Note: if you change the encapsulation code here, change it in WebCit's ical_encapsulate_subcomponent()
1958 void ical_send_out_invitations(icalcomponent *top_level_cal, icalcomponent *cal) {
1959 icalcomponent *the_request = NULL;
1960 char *serialized_request = NULL;
1961 icalcomponent *encaps = NULL;
1962 char *request_message_text = NULL;
1963 struct CtdlMessage *msg = NULL;
1964 recptypes *valid = NULL;
1965 char attendees_string[SIZ];
1966 int num_attendees = 0;
1967 char this_attendee[256];
1968 icalproperty *attendee = NULL;
1969 char summary_string[SIZ];
1970 icalproperty *summary = NULL;
1973 struct icaltimetype t;
1974 const icaltimezone *attached_zones[5] = { NULL, NULL, NULL, NULL, NULL };
1976 const icaltimezone *z;
1977 int num_zones_attached = 0;
1978 int zone_already_attached;
1979 icalparameter *tzidp = NULL;
1980 const char *tzidc = NULL;
1983 syslog(LOG_ERR, "calendar: trying to reply to NULL event?");
1987 /* If this is a VCALENDAR component, look for a VEVENT subcomponent. */
1988 if (icalcomponent_isa(cal) == ICAL_VCALENDAR_COMPONENT) {
1989 ical_send_out_invitations(top_level_cal,
1990 icalcomponent_get_first_component(
1991 cal, ICAL_VEVENT_COMPONENT
1997 /* Clone the event */
1998 the_request = icalcomponent_new_clone(cal);
1999 if (the_request == NULL) {
2000 syslog(LOG_ERR, "calendar: cannot clone calendar object");
2004 /* Extract the summary string -- we'll use it as the
2005 * message subject for the request
2007 strcpy(summary_string, "Meeting request");
2008 summary = icalcomponent_get_first_property(the_request, ICAL_SUMMARY_PROPERTY);
2009 if (summary != NULL) {
2010 if (icalproperty_get_summary(summary)) {
2011 strcpy(summary_string,
2012 icalproperty_get_summary(summary) );
2016 /* Determine who the recipients of this message are (the attendees) */
2017 strcpy(attendees_string, "");
2018 for (attendee = icalcomponent_get_first_property(the_request, ICAL_ATTENDEE_PROPERTY); attendee != NULL; attendee = icalcomponent_get_next_property(the_request, ICAL_ATTENDEE_PROPERTY)) {
2019 const char *ch = icalproperty_get_attendee(attendee);
2020 if ((ch != NULL) && !strncasecmp(ch, "MAILTO:", 7)) {
2021 safestrncpy(this_attendee, ch + 7, sizeof(this_attendee));
2023 if (!CtdlIsMe(this_attendee, sizeof this_attendee)) { /* don't send an invitation to myself! */
2024 snprintf(&attendees_string[strlen(attendees_string)],
2025 sizeof(attendees_string) - strlen(attendees_string),
2034 syslog(LOG_DEBUG, "calendar: <%d> attendees: <%s>", num_attendees, attendees_string);
2036 /* If there are no attendees, there are no invitations to send, so...
2037 * don't bother putting one together! Punch out, Maverick!
2039 if (num_attendees == 0) {
2040 icalcomponent_free(the_request);
2044 /* Encapsulate the VEVENT component into a complete VCALENDAR */
2045 encaps = icalcomponent_new_vcalendar();
2046 if (encaps == NULL) {
2047 syslog(LOG_ERR, "calendar: could not allocate component!");
2048 icalcomponent_free(the_request);
2052 /* Set the Product ID */
2053 icalcomponent_add_property(encaps, icalproperty_new_prodid(PRODID));
2055 /* Set the Version Number */
2056 icalcomponent_add_property(encaps, icalproperty_new_version("2.0"));
2058 /* Set the method to REQUEST */
2059 icalcomponent_set_method(encaps, ICAL_METHOD_REQUEST);
2061 /* Look for properties containing timezone parameters, to see if we need to attach VTIMEZONEs */
2062 for (p = icalcomponent_get_first_property(the_request, ICAL_ANY_PROPERTY);
2064 p = icalcomponent_get_next_property(the_request, ICAL_ANY_PROPERTY))
2066 if ( (icalproperty_isa(p) == ICAL_COMPLETED_PROPERTY)
2067 || (icalproperty_isa(p) == ICAL_CREATED_PROPERTY)
2068 || (icalproperty_isa(p) == ICAL_DATEMAX_PROPERTY)
2069 || (icalproperty_isa(p) == ICAL_DATEMIN_PROPERTY)
2070 || (icalproperty_isa(p) == ICAL_DTEND_PROPERTY)
2071 || (icalproperty_isa(p) == ICAL_DTSTAMP_PROPERTY)
2072 || (icalproperty_isa(p) == ICAL_DTSTART_PROPERTY)
2073 || (icalproperty_isa(p) == ICAL_DUE_PROPERTY)
2074 || (icalproperty_isa(p) == ICAL_EXDATE_PROPERTY)
2075 || (icalproperty_isa(p) == ICAL_LASTMODIFIED_PROPERTY)
2076 || (icalproperty_isa(p) == ICAL_MAXDATE_PROPERTY)
2077 || (icalproperty_isa(p) == ICAL_MINDATE_PROPERTY)
2078 || (icalproperty_isa(p) == ICAL_RECURRENCEID_PROPERTY)
2080 t = icalproperty_get_dtstart(p); // it's safe to use dtstart for all of them
2082 /* Determine the tzid in order for some of the conditions below to work */
2083 tzidp = icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER);
2085 tzidc = icalparameter_get_tzid(tzidp);
2091 /* First see if there's a timezone attached to the data structure itself */
2092 if (icaltime_is_utc(t)) {
2093 z = icaltimezone_get_utc_timezone();
2096 z = icaltime_get_timezone(t);
2099 /* If not, try to determine the tzid from the parameter using attached zones */
2100 if ((!z) && (tzidc)) {
2101 z = icalcomponent_get_timezone(top_level_cal, tzidc);
2104 /* Still no good? Try our internal database */
2105 if ((!z) && (tzidc)) {
2106 z = icaltimezone_get_builtin_timezone_from_tzid(tzidc);
2110 /* We have a valid timezone. Good. Now we need to attach it. */
2112 zone_already_attached = 0;
2113 for (i=0; i<5; ++i) {
2114 if (z == attached_zones[i]) {
2115 /* We've already got this one, no need to attach another. */
2116 ++zone_already_attached;
2119 if ((!zone_already_attached) && (num_zones_attached < 5)) {
2120 /* This is a new one, so attach it. */
2121 attached_zones[num_zones_attached++] = z;
2124 icalproperty_set_parameter(p, icalparameter_new_tzid(icaltimezone_get_tzid(z))
2130 /* Encapsulate any timezones we need */
2131 if (num_zones_attached > 0) for (i=0; i<num_zones_attached; ++i) {
2133 zc = icalcomponent_new_clone(icaltimezone_get_component(attached_zones[i]));
2134 icalcomponent_add_component(encaps, zc);
2137 /* Here we go: encapsulate the VEVENT into the VCALENDAR. We now no longer
2138 * are responsible for "the_request"'s memory -- it will be freed
2139 * when we free "encaps".
2141 icalcomponent_add_component(encaps, the_request);
2144 serialized_request = icalcomponent_as_ical_string_r(encaps);
2145 icalcomponent_free(encaps); /* Don't need this anymore. */
2146 if (serialized_request == NULL) return;
2148 reqsize = strlen(serialized_request) + SIZ;
2149 request_message_text = malloc(reqsize);
2150 if (request_message_text != NULL) {
2151 snprintf(request_message_text, reqsize,
2152 "Content-type: text/calendar\r\n\r\n%s\r\n",
2156 msg = CtdlMakeMessage(
2158 NULL, /* No single recipient here */
2159 NULL, /* No single recipient here */
2165 summary_string, /* Use summary for subject */
2167 request_message_text,
2172 valid = validate_recipients(attendees_string, NULL, 0);
2173 CtdlSubmitMsg(msg, valid, "", QP_EADDR);
2175 free_recipients(valid);
2178 free(serialized_request);
2183 * When a calendar object is being saved, determine whether it's a VEVENT
2184 * and the user saving it is the organizer. If so, send out invitations
2185 * to any listed attendees.
2187 * This function is recursive. The caller can simply supply the same object
2188 * as both arguments. When it recurses it will alter the second argument
2189 * while holding on to the top level object. This allows us to go back and
2190 * grab things like time zones which might be attached.
2193 void ical_saving_vevent(icalcomponent *top_level_cal, icalcomponent *cal) {
2195 icalproperty *organizer = NULL;
2196 char organizer_string[SIZ];
2198 syslog(LOG_DEBUG, "calendar: ical_saving_vevent() has been called");
2200 /* Don't send out invitations unless the client wants us to. */
2201 if (CIT_ICAL->server_generated_invitations == 0) {
2205 /* Don't send out invitations if we've been asked not to. */
2206 if (CIT_ICAL->avoid_sending_invitations > 0) {
2210 strcpy(organizer_string, "");
2212 * The VEVENT subcomponent is the one we're interested in.
2213 * Send out invitations if, and only if, this user is the Organizer.
2215 if (icalcomponent_isa(cal) == ICAL_VEVENT_COMPONENT) {
2216 organizer = icalcomponent_get_first_property(cal, ICAL_ORGANIZER_PROPERTY);
2217 if (organizer != NULL) {
2218 if (icalproperty_get_organizer(organizer)) {
2219 strcpy(organizer_string,
2220 icalproperty_get_organizer(organizer));
2223 if (!strncasecmp(organizer_string, "MAILTO:", 7)) {
2224 strcpy(organizer_string, &organizer_string[7]);
2225 striplt(organizer_string);
2227 * If the user saving the event is listed as the
2228 * organizer, then send out invitations.
2230 if (CtdlIsMe(organizer_string, sizeof organizer_string)) {
2231 ical_send_out_invitations(top_level_cal, cal);
2236 /* If the component has subcomponents, recurse through them. */
2237 for (c = icalcomponent_get_first_component(cal, ICAL_ANY_COMPONENT);
2239 c = icalcomponent_get_next_component(cal, ICAL_ANY_COMPONENT)) {
2240 /* Recursively process subcomponent */
2241 ical_saving_vevent(top_level_cal, c);
2248 * Back end for ical_obj_beforesave()
2249 * This hunts for the UID of the calendar event (becomes Citadel msg EUID),
2250 * the summary of the event (becomes message subject),
2251 * and the start time (becomes message date/time).
2253 void ical_obj_beforesave_backend(char *name, char *filename, char *partnum,
2254 char *disp, void *content, char *cbtype, char *cbcharset, size_t length,
2255 char *encoding, char *cbid, void *cbuserdata)
2258 icalcomponent *cal, *nested_event, *nested_todo, *whole_cal;
2260 char new_uid[256] = "";
2261 struct CtdlMessage *msg = (struct CtdlMessage *) cbuserdata;
2265 /* We're only interested in calendar data. */
2266 if ( (strcasecmp(cbtype, "text/calendar"))
2267 && (strcasecmp(cbtype, "application/ics")) ) {
2271 /* Hunt for the UID and drop it in
2272 * the "user data" pointer for the MIME parser. When
2273 * ical_obj_beforesave() sees it there, it'll set the Exclusive msgid
2276 whole_cal = icalcomponent_new_from_string(content);
2279 if (icalcomponent_isa(cal) == ICAL_VCALENDAR_COMPONENT) {
2280 nested_event = icalcomponent_get_first_component(
2281 cal, ICAL_VEVENT_COMPONENT);
2282 if (nested_event != NULL) {
2286 nested_todo = icalcomponent_get_first_component(
2287 cal, ICAL_VTODO_COMPONENT);
2288 if (nested_todo != NULL) {
2296 /* Set the message EUID to the iCalendar UID */
2298 p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
2300 /* If there's no uid we must generate one */
2301 generate_uuid(new_uid);
2302 icalcomponent_add_property(cal, icalproperty_new_uid(new_uid));
2303 p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
2306 pch = icalproperty_get_comment(p);
2307 if (!IsEmptyStr(pch)) {
2308 CM_SetField(msg, eExclusiveID, pch, strlen(pch));
2309 syslog(LOG_DEBUG, "calendar: saving calendar UID <%s>", pch);
2313 /* Set the message subject to the iCalendar summary */
2315 p = ical_ctdl_get_subprop(cal, ICAL_SUMMARY_PROPERTY);
2317 pch = icalproperty_get_comment(p);
2318 if (!IsEmptyStr(pch)) {
2321 subj = rfc2047encode(pch, strlen(pch));
2322 CM_SetAsField(msg, eMsgSubject, &subj, strlen(subj));
2326 /* Set the message date/time to the iCalendar start time */
2328 p = ical_ctdl_get_subprop(cal, ICAL_DTSTART_PROPERTY);
2331 idtstart = icaltime_as_timet(icalproperty_get_dtstart(p));
2333 CM_SetFieldLONG(msg, eTimestamp, idtstart);
2338 icalcomponent_free(cal);
2339 if (whole_cal != cal) {
2340 icalcomponent_free(whole_cal);
2347 * See if we need to prevent the object from being saved (we don't allow
2348 * MIME types other than text/calendar in "calendar" or "tasks" rooms).
2350 * If the message is being saved, we also set various message header fields
2351 * using data found in the iCalendar object.
2353 int ical_obj_beforesave(struct CtdlMessage *msg, recptypes *recp)
2355 /* First determine if this is a calendar or tasks room */
2356 if ( (CC->room.QRdefaultview != VIEW_CALENDAR)
2357 && (CC->room.QRdefaultview != VIEW_TASKS)
2359 return(0); /* Not an iCalendar-centric room */
2362 /* It must be an RFC822 message! */
2363 if (msg->cm_format_type != 4) {
2364 syslog(LOG_DEBUG, "calendar: rejecting non-RFC822 message");
2365 return(1); /* You tried to save a non-RFC822 message! */
2368 if (CM_IsEmpty(msg, eMesageText)) {
2369 return(1); /* You tried to save a null message! */
2372 /* Do all of our lovely back-end parsing */
2373 mime_parser(CM_RANGE(msg, eMesageText),
2374 *ical_obj_beforesave_backend,
2385 * Things we need to do after saving a calendar event.
2387 void ical_obj_aftersave_backend(char *name, char *filename, char *partnum,
2388 char *disp, void *content, char *cbtype, char *cbcharset, size_t length,
2389 char *encoding, char *cbid, void *cbuserdata)
2393 /* We're only interested in calendar items here. */
2394 if ( (strcasecmp(cbtype, "text/calendar"))
2395 && (strcasecmp(cbtype, "application/ics")) ) {
2399 /* Hunt for the UID and drop it in
2400 * the "user data" pointer for the MIME parser. When
2401 * ical_obj_beforesave() sees it there, it'll set the Exclusive msgid
2404 if ( (!strcasecmp(cbtype, "text/calendar"))
2405 || (!strcasecmp(cbtype, "application/ics")) ) {
2406 cal = icalcomponent_new_from_string(content);
2408 ical_saving_vevent(cal, cal);
2409 icalcomponent_free(cal);
2416 * Things we need to do after saving a calendar event.
2417 * (This will start back end tasks such as automatic generation of invitations,
2418 * if such actions are appropriate.)
2420 int ical_obj_aftersave(struct CtdlMessage *msg, recptypes *recp)
2422 char roomname[ROOMNAMELEN];
2425 * If this isn't the Calendar> room, no further action is necessary.
2428 /* First determine if this is our room */
2429 CtdlMailboxName(roomname, sizeof roomname, &CC->user, USERCALENDARROOM);
2430 if (strcasecmp(roomname, CC->room.QRname)) {
2431 return(0); /* Not the Calendar room -- don't do anything. */
2434 /* It must be an RFC822 message! */
2435 if (msg->cm_format_type != 4) return(1);
2437 /* Reject null messages */
2438 if (CM_IsEmpty(msg, eMesageText)) return(1);
2440 /* Now recurse through it looking for our icalendar data */
2441 mime_parser(CM_RANGE(msg, eMesageText),
2442 *ical_obj_aftersave_backend,
2452 void ical_session_startup(void) {
2453 CIT_ICAL = malloc(sizeof(struct cit_ical));
2454 memset(CIT_ICAL, 0, sizeof(struct cit_ical));
2458 void ical_session_shutdown(void) {
2464 * Back end for ical_fixed_output()
2466 void ical_fixed_output_backend(icalcomponent *cal, int recursion_level) {
2472 p = icalcomponent_get_first_property(cal, ICAL_SUMMARY_PROPERTY);
2474 cprintf("%s\n", (const char *)icalproperty_get_comment(p));
2477 p = icalcomponent_get_first_property(cal, ICAL_LOCATION_PROPERTY);
2479 cprintf("%s\n", (const char *)icalproperty_get_comment(p));
2482 p = icalcomponent_get_first_property(cal, ICAL_DESCRIPTION_PROPERTY);
2484 cprintf("%s\n", (const char *)icalproperty_get_comment(p));
2487 /* If the component has attendees, iterate through them. */
2488 for (p = icalcomponent_get_first_property(cal, ICAL_ATTENDEE_PROPERTY); (p != NULL); p = icalcomponent_get_next_property(cal, ICAL_ATTENDEE_PROPERTY)) {
2489 ch = icalproperty_get_attendee(p);
2491 !strncasecmp(ch, "MAILTO:", 7)) {
2493 /* screen name or email address */
2494 safestrncpy(buf, ch + 7, sizeof(buf));
2496 cprintf("%s ", buf);
2501 /* If the component has subcomponents, recurse through them. */
2502 for (c = icalcomponent_get_first_component(cal, ICAL_ANY_COMPONENT);
2504 c = icalcomponent_get_next_component(cal, ICAL_ANY_COMPONENT)) {
2505 /* Recursively process subcomponent */
2506 ical_fixed_output_backend(c, recursion_level+1);
2512 * Function to output iCalendar data as plain text. Nobody uses MSG0
2513 * anymore, so really this is just so we expose the vCard data to the full
2516 void ical_fixed_output(char *ptr, int len) {
2520 stringy_cal = malloc(len + 1);
2521 safestrncpy(stringy_cal, ptr, len + 1);
2522 cal = icalcomponent_new_from_string(stringy_cal);
2529 ical_fixed_output_backend(cal, 0);
2531 /* Free the memory we obtained from libical's constructor */
2532 icalcomponent_free(cal);
2536 void serv_calendar_destroy(void) {
2537 icaltimezone_free_builtin_timezones();
2542 * Register this module with the Citadel server.
2544 CTDL_MODULE_INIT(calendar)
2549 /* Tell libical to return errors instead of aborting if it gets bad data */
2551 #ifdef LIBICAL_ICAL_EXPORT // cheap and sleazy way to detect libical >=2.0
2552 icalerror_set_errors_are_fatal(0);
2554 icalerror_errors_are_fatal = 0;
2557 /* Use our own application prefix in tzid's generated from system tzdata */
2558 icaltimezone_set_tzid_prefix("/citadel.org/");
2560 /* Initialize our hook functions */
2561 CtdlRegisterMessageHook(ical_obj_beforesave, EVT_BEFORESAVE);
2562 CtdlRegisterMessageHook(ical_obj_aftersave, EVT_AFTERSAVE);
2563 CtdlRegisterSessionHook(ical_CtdlCreateRoom, EVT_LOGIN, PRIO_LOGIN + 1);
2564 CtdlRegisterProtoHook(cmd_ical, "ICAL", "Citadel iCalendar commands");
2565 CtdlRegisterSessionHook(ical_session_startup, EVT_START, PRIO_START + 1);
2566 CtdlRegisterSessionHook(ical_session_shutdown, EVT_STOP, PRIO_STOP + 80);
2567 CtdlRegisterFixedOutputHook("text/calendar", ical_fixed_output);
2568 CtdlRegisterFixedOutputHook("application/ics", ical_fixed_output);
2569 CtdlRegisterCleanupHook(serv_calendar_destroy);
2572 /* return our module name for the log */