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-2020 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));
141 MsgBody = NewStrBufPlain(NULL, serlen + 100);
142 StrBufAppendBufPlain(MsgBody, HKEY("Content-type: text/calendar\r\n\r\n"), 0);
143 StrBufAppendBufPlain(MsgBody, ser, serlen, 0);
145 CM_SetAsFieldSB(msg, eMesageText, &MsgBody);
147 /* Now write the data */
148 CtdlSubmitMsg(msg, NULL, "", QP_EADDR);
152 /* In either case, now we can free the serialized calendar object */
158 * Send a reply to a meeting invitation.
160 * 'request' is the invitation to reply to.
161 * 'action' is the string "accept" or "decline" or "tentative".
164 void ical_send_a_reply(icalcomponent *request, char *action) {
165 icalcomponent *the_reply = NULL;
166 icalcomponent *vevent = NULL;
167 icalproperty *attendee = NULL;
168 char attendee_string[SIZ];
169 icalproperty *organizer = NULL;
170 char organizer_string[SIZ];
171 icalproperty *summary = NULL;
172 char summary_string[SIZ];
173 icalproperty *me_attend = NULL;
174 recptypes *recp = NULL;
175 icalparameter *partstat = NULL;
176 char *serialized_reply = NULL;
177 char *reply_message_text = NULL;
179 struct CtdlMessage *msg = NULL;
180 recptypes *valid = NULL;
182 *organizer_string = '\0';
183 strcpy(summary_string, "Calendar item");
185 if (request == NULL) {
186 syslog(LOG_ERR, "calendar: trying to reply to NULL event");
190 the_reply = icalcomponent_new_clone(request);
191 if (the_reply == NULL) {
192 syslog(LOG_ERR, "calendar: cannot clone request");
196 /* Change the method from REQUEST to REPLY */
197 icalcomponent_set_method(the_reply, ICAL_METHOD_REPLY);
199 vevent = icalcomponent_get_first_component(the_reply, ICAL_VEVENT_COMPONENT);
200 if (vevent != NULL) {
201 /* Hunt for attendees, removing ones that aren't us.
202 * (Actually, remove them all, cloning our own one so we can
203 * re-insert it later)
205 while (attendee = icalcomponent_get_first_property(vevent,
206 ICAL_ATTENDEE_PROPERTY), (attendee != NULL)
208 ch = icalproperty_get_attendee(attendee);
209 if ((ch != NULL) && !strncasecmp(ch, "MAILTO:", 7)) {
210 safestrncpy(attendee_string, ch + 7, sizeof (attendee_string));
211 striplt(attendee_string);
212 recp = validate_recipients(attendee_string, NULL, 0);
214 if (!strcasecmp(recp->recp_local, CC->user.fullname)) {
215 if (me_attend) icalproperty_free(me_attend);
216 me_attend = icalproperty_new_clone(attendee);
218 free_recipients(recp);
223 icalcomponent_remove_property(vevent, attendee);
224 icalproperty_free(attendee);
227 /* We found our own address in the attendee list. */
229 /* Change the partstat from NEEDS-ACTION to ACCEPT or DECLINE */
230 icalproperty_remove_parameter_by_kind(me_attend, ICAL_PARTSTAT_PARAMETER);
232 if (!strcasecmp(action, "accept")) {
233 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_ACCEPTED);
235 else if (!strcasecmp(action, "decline")) {
236 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_DECLINED);
238 else if (!strcasecmp(action, "tentative")) {
239 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_TENTATIVE);
242 if (partstat) icalproperty_add_parameter(me_attend, partstat);
244 /* Now insert it back into the vevent. */
245 icalcomponent_add_property(vevent, me_attend);
248 /* Figure out who to send this thing to */
249 organizer = icalcomponent_get_first_property(vevent, ICAL_ORGANIZER_PROPERTY);
250 if (organizer != NULL) {
251 if (icalproperty_get_organizer(organizer)) {
252 strcpy(organizer_string,
253 icalproperty_get_organizer(organizer) );
256 if (!strncasecmp(organizer_string, "MAILTO:", 7)) {
257 strcpy(organizer_string, &organizer_string[7]);
258 striplt(organizer_string);
260 strcpy(organizer_string, "");
263 /* Extract the summary string -- we'll use it as the
264 * message subject for the reply
266 summary = icalcomponent_get_first_property(vevent, ICAL_SUMMARY_PROPERTY);
267 if (summary != NULL) {
268 if (icalproperty_get_summary(summary)) {
269 strcpy(summary_string,
270 icalproperty_get_summary(summary) );
275 /* Now generate the reply message and send it out. */
276 serialized_reply = icalcomponent_as_ical_string_r(the_reply);
277 icalcomponent_free(the_reply); /* don't need this anymore */
278 if (serialized_reply == NULL) return;
280 reply_message_text = malloc(strlen(serialized_reply) + SIZ);
281 if (reply_message_text != NULL) {
282 sprintf(reply_message_text,
283 "Content-type: text/calendar; charset=\"utf-8\"\r\n\r\n%s\r\n",
287 msg = CtdlMakeMessage(&CC->user,
288 organizer_string, /* to */
290 CC->room.QRname, 0, FMT_RFC822,
293 summary_string, /* Use summary for subject */
299 valid = validate_recipients(organizer_string, NULL, 0);
300 CtdlSubmitMsg(msg, valid, "", QP_EADDR);
302 free_recipients(valid);
305 free(serialized_reply);
310 * Callback function for mime parser that hunts for calendar content types
311 * and turns them into calendar objects. If something is found, it is placed
312 * in ird->cal, and the caller now owns that memory and is responsible for freeing it.
314 void ical_locate_part(char *name, char *filename, char *partnum, char *disp,
315 void *content, char *cbtype, char *cbcharset, size_t length, char *encoding,
316 char *cbid, void *cbuserdata) {
318 struct ical_respond_data *ird = NULL;
320 ird = (struct ical_respond_data *) cbuserdata;
322 /* desired_partnum can be set to "_HUNT_" to have it just look for
323 * the first part with a content type of text/calendar. Otherwise
324 * we have to only process the right one.
326 if (strcasecmp(ird->desired_partnum, "_HUNT_")) {
327 if (strcasecmp(partnum, ird->desired_partnum)) {
332 if ( (strcasecmp(cbtype, "text/calendar"))
333 && (strcasecmp(cbtype, "application/ics")) ) {
337 if (ird->cal != NULL) {
338 icalcomponent_free(ird->cal);
342 ird->cal = icalcomponent_new_from_string(content);
347 * Respond to a meeting request.
349 void ical_respond(long msgnum, char *partnum, char *action) {
350 struct CtdlMessage *msg = NULL;
351 struct ical_respond_data ird;
354 (strcasecmp(action, "accept"))
355 && (strcasecmp(action, "decline"))
357 cprintf("%d Action must be 'accept' or 'decline'\n",
358 ERROR + ILLEGAL_VALUE
363 msg = CtdlFetchMessage(msgnum, 1, 1);
365 cprintf("%d Message %ld not found.\n",
366 ERROR + ILLEGAL_VALUE,
372 memset(&ird, 0, sizeof ird);
373 strcpy(ird.desired_partnum, partnum);
374 mime_parser(CM_RANGE(msg, eMesageText),
375 *ical_locate_part, /* callback function */
377 (void *) &ird, /* user data */
381 /* We're done with the incoming message, because we now have a
382 * calendar object in memory.
387 * Here is the real meat of this function. Handle the event.
389 if (ird.cal != NULL) {
390 /* Save this in the user's calendar if necessary */
391 if (!strcasecmp(action, "accept")) {
392 ical_write_to_cal(&CC->user, ird.cal);
395 /* Send a reply if necessary */
396 if (icalcomponent_get_method(ird.cal) == ICAL_METHOD_REQUEST) {
397 ical_send_a_reply(ird.cal, action);
400 /* We used to delete the invitation after handling it.
401 * We don't do that anymore, but here is the code that handled it:
402 * CtdlDeleteMessages(CC->room.QRname, &msgnum, 1, "");
405 /* Free the memory we allocated and return a response. */
406 icalcomponent_free(ird.cal);
408 cprintf("%d ok\n", CIT_OK);
412 cprintf("%d No calendar object found\n", ERROR + ROOM_NOT_FOUND);
416 /* should never get here */
421 * Figure out the UID of the calendar event being referred to in a
422 * REPLY object. This function is recursive.
424 void ical_learn_uid_of_reply(char *uidbuf, icalcomponent *cal) {
425 icalcomponent *subcomponent;
428 /* If this object is a REPLY, then extract the UID. */
429 if (icalcomponent_isa(cal) == ICAL_VEVENT_COMPONENT) {
430 p = icalcomponent_get_first_property(cal, ICAL_UID_PROPERTY);
432 strcpy(uidbuf, icalproperty_get_comment(p));
436 /* Otherwise, recurse through any VEVENT subcomponents. We do NOT want the
437 * UID of the reply; we want the UID of the invitation being replied to.
439 for (subcomponent = icalcomponent_get_first_component(cal, ICAL_VEVENT_COMPONENT);
440 subcomponent != NULL;
441 subcomponent = icalcomponent_get_next_component(cal, ICAL_VEVENT_COMPONENT) ) {
442 ical_learn_uid_of_reply(uidbuf, subcomponent);
448 * ical_update_my_calendar_with_reply() refers to this callback function; when we
449 * locate the message containing the calendar event we're replying to, this function
450 * gets called. It basically just sticks the message number in a supplied buffer.
452 void ical_hunt_for_event_to_update(long msgnum, void *data) {
455 msgnumptr = (long *) data;
460 struct original_event_container {
465 * Callback function for mime parser that hunts for calendar content types
466 * and turns them into calendar objects (called by ical_update_my_calendar_with_reply()
467 * to fetch the object being updated)
469 void ical_locate_original_event(char *name, char *filename, char *partnum, char *disp,
470 void *content, char *cbtype, char *cbcharset, size_t length, char *encoding,
471 char *cbid, void *cbuserdata) {
473 struct original_event_container *oec = NULL;
475 if ( (strcasecmp(cbtype, "text/calendar"))
476 && (strcasecmp(cbtype, "application/ics")) ) {
479 oec = (struct original_event_container *) cbuserdata;
480 if (oec->c != NULL) {
481 icalcomponent_free(oec->c);
483 oec->c = icalcomponent_new_from_string(content);
488 * Merge updated attendee information from a REPLY into an existing event.
490 void ical_merge_attendee_reply(icalcomponent *event, icalcomponent *reply) {
492 icalproperty *e_attendee, *r_attendee;
494 /* First things first. If we're not looking at a VEVENT component,
495 * recurse through subcomponents until we find one.
497 if (icalcomponent_isa(event) != ICAL_VEVENT_COMPONENT) {
498 for (c = icalcomponent_get_first_component(event, ICAL_VEVENT_COMPONENT);
500 c = icalcomponent_get_next_component(event, ICAL_VEVENT_COMPONENT) ) {
501 ical_merge_attendee_reply(c, reply);
506 /* Now do the same thing with the reply.
508 if (icalcomponent_isa(reply) != ICAL_VEVENT_COMPONENT) {
509 for (c = icalcomponent_get_first_component(reply, ICAL_VEVENT_COMPONENT);
511 c = icalcomponent_get_next_component(reply, ICAL_VEVENT_COMPONENT) ) {
512 ical_merge_attendee_reply(event, c);
517 /* Clone the reply, because we're going to rip its guts out. */
518 reply = icalcomponent_new_clone(reply);
520 /* At this point we're looking at the correct subcomponents.
521 * Iterate through the attendees looking for a match.
524 for (e_attendee = icalcomponent_get_first_property(event, ICAL_ATTENDEE_PROPERTY);
526 e_attendee = icalcomponent_get_next_property(event, ICAL_ATTENDEE_PROPERTY)) {
528 for (r_attendee = icalcomponent_get_first_property(reply, ICAL_ATTENDEE_PROPERTY);
530 r_attendee = icalcomponent_get_next_property(reply, ICAL_ATTENDEE_PROPERTY)) {
532 /* Check to see if these two attendees match...
535 e = icalproperty_get_attendee(e_attendee);
536 r = icalproperty_get_attendee(r_attendee);
541 /* ...and if they do, remove the attendee from the event
542 * and replace it with the attendee from the reply. (The
543 * reply's copy will have the same address, but an updated
546 icalcomponent_remove_property(event, e_attendee);
547 icalproperty_free(e_attendee);
548 icalcomponent_remove_property(reply, r_attendee);
549 icalcomponent_add_property(event, r_attendee);
551 /* Since we diddled both sets of attendees, we have to start
552 * the iteration over again. This will not create an infinite
553 * loop because we removed the attendee from the reply. (That's
554 * why we cloned the reply, and that's what we mean by "ripping
563 /* Free the *clone* of the reply. */
564 icalcomponent_free(reply);
569 * Handle an incoming RSVP (object with method==ICAL_METHOD_REPLY) for a
570 * calendar event. The object has already been deserialized for us; all
571 * we have to do here is hunt for the event in our calendar, merge in the
572 * updated attendee status, and save it again.
574 * This function returns 0 on success, 1 if the event was not found in the
575 * user's calendar, or 2 if an internal error occurred.
577 int ical_update_my_calendar_with_reply(icalcomponent *cal) {
579 char hold_rm[ROOMNAMELEN];
580 long msgnum_being_replaced = 0;
581 struct CtdlMessage *msg = NULL;
582 struct original_event_container oec;
583 icalcomponent *original_event;
584 char *serialized_event = NULL;
585 char roomname[ROOMNAMELEN];
586 char *message_text = NULL;
588 /* Figure out just what event it is we're dealing with */
589 strcpy(uid, "--==<< InVaLiD uId >>==--");
590 ical_learn_uid_of_reply(uid, cal);
591 syslog(LOG_DEBUG, "calendar: UID of event being replied to is <%s>", uid);
593 strcpy(hold_rm, CC->room.QRname); /* save current room */
595 if (CtdlGetRoom(&CC->room, USERCALENDARROOM) != 0) {
596 CtdlGetRoom(&CC->room, hold_rm);
597 syslog(LOG_ERR, "calendar: cannot get user calendar room");
602 * Look in the EUID index for a message with
603 * the Citadel EUID set to the value we're looking for. Since
604 * Citadel always sets the message EUID to the iCalendar UID of
605 * the event, this will work.
607 msgnum_being_replaced = CtdlLocateMessageByEuid(uid, &CC->room);
609 CtdlGetRoom(&CC->room, hold_rm); /* return to saved room */
611 syslog(LOG_DEBUG, "calendar: msgnum_being_replaced == %ld", msgnum_being_replaced);
612 if (msgnum_being_replaced == 0) {
613 return(1); /* no calendar event found */
616 /* Now we know the ID of the message containing the event being updated.
617 * We don't actually have to delete it; that'll get taken care of by the
618 * server when we save another event with the same UID. This just gives
619 * us the ability to load the event into memory so we can diddle the
622 msg = CtdlFetchMessage(msgnum_being_replaced, 1, 1);
624 return(2); /* internal error */
627 mime_parser(CM_RANGE(msg, eMesageText),
628 *ical_locate_original_event, /* callback function */
630 &oec, /* user data */
635 original_event = oec.c;
636 if (original_event == NULL) {
637 syslog(LOG_ERR, "calendar: original_component is NULL");
641 /* Merge the attendee's updated status into the event */
642 ical_merge_attendee_reply(original_event, cal);
645 serialized_event = icalcomponent_as_ical_string_r(original_event);
646 icalcomponent_free(original_event); /* Don't need this anymore. */
647 if (serialized_event == NULL) return(2);
649 CtdlMailboxName(roomname, sizeof roomname, &CC->user, USERCALENDARROOM);
651 message_text = malloc(strlen(serialized_event) + SIZ);
652 if (message_text != NULL) {
653 sprintf(message_text,
654 "Content-type: text/calendar; charset=\"utf-8\"\r\n\r\n%s\r\n",
658 msg = CtdlMakeMessage(&CC->user,
659 "", /* No recipient */
660 "", /* No recipient */
671 CIT_ICAL->avoid_sending_invitations = 1;
672 CtdlSubmitMsg(msg, NULL, roomname, QP_EADDR);
674 CIT_ICAL->avoid_sending_invitations = 0;
677 free(serialized_event);
683 * Handle an incoming RSVP for an event. (This is the server subcommand part; it
684 * simply extracts the calendar object from the message, deserializes it, and
685 * passes it up to ical_update_my_calendar_with_reply() for processing.
687 void ical_handle_rsvp(long msgnum, char *partnum, char *action) {
688 struct CtdlMessage *msg = NULL;
689 struct ical_respond_data ird;
693 (strcasecmp(action, "update"))
694 && (strcasecmp(action, "ignore"))
696 cprintf("%d Action must be 'update' or 'ignore'\n",
697 ERROR + ILLEGAL_VALUE
702 msg = CtdlFetchMessage(msgnum, 1, 1);
704 cprintf("%d Message %ld not found.\n",
705 ERROR + ILLEGAL_VALUE,
711 memset(&ird, 0, sizeof ird);
712 strcpy(ird.desired_partnum, partnum);
713 mime_parser(CM_RANGE(msg, eMesageText),
714 *ical_locate_part, /* callback function */
716 (void *) &ird, /* user data */
720 /* We're done with the incoming message, because we now have a
721 * calendar object in memory.
726 * Here is the real meat of this function. Handle the event.
728 if (ird.cal != NULL) {
729 /* Update the user's calendar if necessary */
730 if (!strcasecmp(action, "update")) {
731 ret = ical_update_my_calendar_with_reply(ird.cal);
733 cprintf("%d Your calendar has been updated with this reply.\n",
737 cprintf("%d This event does not exist in your calendar.\n",
738 ERROR + FILE_NOT_FOUND);
741 cprintf("%d An internal error occurred.\n",
742 ERROR + INTERNAL_ERROR);
746 cprintf("%d This reply has been ignored.\n", CIT_OK);
749 /* Now that we've processed this message, we don't need it
750 * anymore. So delete it. (Don't do this anymore.)
751 CtdlDeleteMessages(CC->room.QRname, &msgnum, 1, "");
754 /* Free the memory we allocated and return a response. */
755 icalcomponent_free(ird.cal);
760 cprintf("%d No calendar object found\n", ERROR + ROOM_NOT_FOUND);
764 /* should never get here */
769 * Search for a property in both the top level and in a VEVENT subcomponent
771 icalproperty *ical_ctdl_get_subprop(
773 icalproperty_kind which_prop
778 p = icalcomponent_get_first_property(cal, which_prop);
780 c = icalcomponent_get_first_component(cal,
781 ICAL_VEVENT_COMPONENT);
783 p = icalcomponent_get_first_property(c, which_prop);
791 * Check to see if two events overlap. Returns nonzero if they do.
792 * (This function is used in both Citadel and WebCit. If you change it in
793 * one place, change it in the other. Better yet, put it in a library.)
795 int ical_ctdl_is_overlap(
796 struct icaltimetype t1start,
797 struct icaltimetype t1end,
798 struct icaltimetype t2start,
799 struct icaltimetype t2end
801 if (icaltime_is_null_time(t1start)) return(0);
802 if (icaltime_is_null_time(t2start)) return(0);
804 /* if either event lacks end time, assume end = start */
805 if (icaltime_is_null_time(t1end))
806 memcpy(&t1end, &t1start, sizeof(struct icaltimetype));
808 if (t1end.is_date && icaltime_compare(t1start, t1end)) {
810 * the end date is non-inclusive so adjust it by one
811 * day because our test is inclusive, note that a day is
812 * not too much because we are talking about all day
814 * if start = end we assume that nevertheless the whole
817 icaltime_adjust(&t1end, -1, 0, 0, 0);
821 if (icaltime_is_null_time(t2end))
822 memcpy(&t2end, &t2start, sizeof(struct icaltimetype));
824 if (t2end.is_date && icaltime_compare(t2start, t2end)) {
825 icaltime_adjust(&t2end, -1, 0, 0, 0);
829 /* First, check for all-day events */
830 if (t1start.is_date || t2start.is_date) {
831 /* If event 1 ends before event 2 starts, we're in the clear. */
832 if (icaltime_compare_date_only(t1end, t2start) < 0) return(0);
834 /* If event 2 ends before event 1 starts, we're also ok. */
835 if (icaltime_compare_date_only(t2end, t1start) < 0) return(0);
840 /* syslog(LOG_DEBUG, "Comparing t1start %d:%d t1end %d:%d t2start %d:%d t2end %d:%d",
841 t1start.hour, t1start.minute, t1end.hour, t1end.minute,
842 t2start.hour, t2start.minute, t2end.hour, t2end.minute);
845 /* Now check for overlaps using date *and* time. */
847 /* If event 1 ends before event 2 starts, we're in the clear. */
848 if (icaltime_compare(t1end, t2start) <= 0) return(0);
849 /* syslog(LOG_DEBUG, "calendar: first passed"); */
851 /* If event 2 ends before event 1 starts, we're also ok. */
852 if (icaltime_compare(t2end, t1start) <= 0) return(0);
853 /* syslog(LOG_DEBUG, "calendar: second passed"); */
855 /* Otherwise, they overlap. */
861 * Phase 6 of "hunt for conflicts"
862 * called by ical_conflicts_phase5()
864 * Now both the proposed and existing events have been boiled down to start and end times.
865 * Check for overlap and output any conflicts.
867 * Returns nonzero if a conflict was reported. This allows the caller to stop iterating.
869 int ical_conflicts_phase6(struct icaltimetype t1start,
870 struct icaltimetype t1end,
871 struct icaltimetype t2start,
872 struct icaltimetype t2end,
873 long existing_msgnum,
874 char *conflict_event_uid,
875 char *conflict_event_summary,
878 int conflict_reported = 0;
882 tt = icaltime_as_timet_with_zone(t1start, t1start.zone);
883 syslog(LOG_DEBUG, "PROPOSED START: %s", ctime(&tt));
884 tt = icaltime_as_timet_with_zone(t1end, t1end.zone);
885 syslog(LOG_DEBUG, " PROPOSED END: %s", ctime(&tt));
886 tt = icaltime_as_timet_with_zone(t2start, t2start.zone);
887 syslog(LOG_DEBUG, "EXISTING START: %s", ctime(&tt));
888 tt = icaltime_as_timet_with_zone(t2end, t2end.zone);
889 syslog(LOG_DEBUG, " EXISTING END: %s", ctime(&tt));
892 /* compare and output */
894 if (ical_ctdl_is_overlap(t1start, t1end, t2start, t2end)) {
895 cprintf("%ld||%s|%s|%d|\n",
898 conflict_event_summary,
899 ( (!IsEmptyStr(compare_uid)
900 &&(!strcasecmp(compare_uid,
901 conflict_event_uid))) ? 1 : 0
904 conflict_reported = 1;
907 return(conflict_reported);
912 * Phase 5 of "hunt for conflicts"
913 * Called by ical_conflicts_phase4()
915 * We have the proposed event boiled down to start and end times.
916 * Now check it against an existing event.
918 void ical_conflicts_phase5(struct icaltimetype t1start,
919 struct icaltimetype t1end,
920 icalcomponent *existing_event,
921 long existing_msgnum,
924 char conflict_event_uid[SIZ];
925 char conflict_event_summary[SIZ];
926 struct icaltimetype t2start, t2end;
929 /* recur variables */
930 icalproperty *rrule = NULL;
931 struct icalrecurrencetype recur;
932 icalrecur_iterator *ritr = NULL;
933 struct icaldurationtype dur;
937 strcpy(conflict_event_uid, "");
938 strcpy(conflict_event_summary, "");
939 t2start = icaltime_null_time();
940 t2end = icaltime_null_time();
942 /* existing event stuff */
943 p = ical_ctdl_get_subprop(existing_event, ICAL_DTSTART_PROPERTY);
944 if (p == NULL) return;
945 if (p != NULL) t2start = icalproperty_get_dtstart(p);
946 if (icaltime_is_utc(t2start)) {
947 t2start.zone = icaltimezone_get_utc_timezone();
950 t2start.zone = icalcomponent_get_timezone(existing_event,
951 icalparameter_get_tzid(
952 icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER)
956 t2start.zone = get_default_icaltimezone();
960 p = ical_ctdl_get_subprop(existing_event, ICAL_DTEND_PROPERTY);
962 t2end = icalproperty_get_dtend(p);
964 if (icaltime_is_utc(t2end)) {
965 t2end.zone = icaltimezone_get_utc_timezone();
968 t2end.zone = icalcomponent_get_timezone(existing_event,
969 icalparameter_get_tzid(
970 icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER)
974 t2end.zone = get_default_icaltimezone();
977 dur = icaltime_subtract(t2end, t2start);
980 memset (&dur, 0, sizeof(struct icaldurationtype));
983 rrule = ical_ctdl_get_subprop(existing_event, ICAL_RRULE_PROPERTY);
985 recur = icalproperty_get_rrule(rrule);
986 ritr = icalrecur_iterator_new(recur, t2start);
990 p = ical_ctdl_get_subprop(existing_event, ICAL_UID_PROPERTY);
992 strcpy(conflict_event_uid, icalproperty_get_comment(p));
995 p = ical_ctdl_get_subprop(existing_event, ICAL_SUMMARY_PROPERTY);
997 strcpy(conflict_event_summary, icalproperty_get_comment(p));
1000 if (ical_conflicts_phase6(t1start, t1end, t2start, t2end,
1001 existing_msgnum, conflict_event_uid, conflict_event_summary, compare_uid))
1003 num_recur = MAX_RECUR + 1; /* force it out of scope, no need to continue */
1007 t2start = icalrecur_iterator_next(ritr);
1008 if (!icaltime_is_null_time(t2end)) {
1009 const icaltimezone *hold_zone = t2end.zone;
1010 t2end = icaltime_add(t2start, dur);
1011 t2end.zone = hold_zone;
1016 if (icaltime_compare(t2start, t1end) < 0) {
1017 num_recur = MAX_RECUR + 1; /* force it out of scope */
1020 } while ( (rrule) && (!icaltime_is_null_time(t2start)) && (num_recur < MAX_RECUR) );
1021 icalrecur_iterator_free(ritr);
1026 * Phase 4 of "hunt for conflicts"
1027 * Called by ical_hunt_for_conflicts_backend()
1029 * At this point we've got it boiled down to two icalcomponent events in memory.
1030 * If they conflict, output something to the client.
1032 void ical_conflicts_phase4(icalcomponent *proposed_event,
1033 icalcomponent *existing_event,
1034 long existing_msgnum)
1036 struct icaltimetype t1start, t1end;
1038 char compare_uid[SIZ];
1040 /* recur variables */
1041 icalproperty *rrule = NULL;
1042 struct icalrecurrencetype recur;
1043 icalrecur_iterator *ritr = NULL;
1044 struct icaldurationtype dur;
1047 /* initialization */
1048 t1end = icaltime_null_time();
1049 *compare_uid = '\0';
1051 /* proposed event stuff */
1053 p = ical_ctdl_get_subprop(proposed_event, ICAL_DTSTART_PROPERTY);
1057 t1start = icalproperty_get_dtstart(p);
1059 if (icaltime_is_utc(t1start)) {
1060 t1start.zone = icaltimezone_get_utc_timezone();
1063 t1start.zone = icalcomponent_get_timezone(proposed_event,
1064 icalparameter_get_tzid(
1065 icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER)
1068 if (!t1start.zone) {
1069 t1start.zone = get_default_icaltimezone();
1073 p = ical_ctdl_get_subprop(proposed_event, ICAL_DTEND_PROPERTY);
1075 t1end = icalproperty_get_dtend(p);
1077 if (icaltime_is_utc(t1end)) {
1078 t1end.zone = icaltimezone_get_utc_timezone();
1081 t1end.zone = icalcomponent_get_timezone(proposed_event,
1082 icalparameter_get_tzid(
1083 icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER)
1087 t1end.zone = get_default_icaltimezone();
1091 dur = icaltime_subtract(t1end, t1start);
1094 memset (&dur, 0, sizeof(struct icaldurationtype));
1097 rrule = ical_ctdl_get_subprop(proposed_event, ICAL_RRULE_PROPERTY);
1099 recur = icalproperty_get_rrule(rrule);
1100 ritr = icalrecur_iterator_new(recur, t1start);
1103 p = ical_ctdl_get_subprop(proposed_event, ICAL_UID_PROPERTY);
1105 strcpy(compare_uid, icalproperty_get_comment(p));
1109 ical_conflicts_phase5(t1start, t1end, existing_event, existing_msgnum, compare_uid);
1112 t1start = icalrecur_iterator_next(ritr);
1113 if (!icaltime_is_null_time(t1end)) {
1114 const icaltimezone *hold_zone = t1end.zone;
1115 t1end = icaltime_add(t1start, dur);
1116 t1end.zone = hold_zone;
1121 } while ( (rrule) && (!icaltime_is_null_time(t1start)) && (num_recur < MAX_RECUR) );
1122 icalrecur_iterator_free(ritr);
1127 * Phase 3 of "hunt for conflicts"
1128 * Called by ical_hunt_for_conflicts()
1130 void ical_hunt_for_conflicts_backend(long msgnum, void *data) {
1131 icalcomponent *proposed_event;
1132 struct CtdlMessage *msg = NULL;
1133 struct ical_respond_data ird;
1135 proposed_event = (icalcomponent *)data;
1137 msg = CtdlFetchMessage(msgnum, 1, 1);
1138 if (msg == NULL) return;
1139 memset(&ird, 0, sizeof ird);
1140 strcpy(ird.desired_partnum, "_HUNT_");
1141 mime_parser(CM_RANGE(msg, eMesageText),
1142 *ical_locate_part, /* callback function */
1144 (void *) &ird, /* user data */
1149 if (ird.cal == NULL) return;
1151 ical_conflicts_phase4(proposed_event, ird.cal, msgnum);
1152 icalcomponent_free(ird.cal);
1157 * Phase 2 of "hunt for conflicts" operation.
1158 * At this point we have a calendar object which represents the VEVENT that
1159 * is proposed for addition to the calendar. Now hunt through the user's
1160 * calendar room, and output zero or more existing VEVENTs which conflict
1163 void ical_hunt_for_conflicts(icalcomponent *cal) {
1164 char hold_rm[ROOMNAMELEN];
1166 strcpy(hold_rm, CC->room.QRname); /* save current room */
1168 if (CtdlGetRoom(&CC->room, USERCALENDARROOM) != 0) {
1169 CtdlGetRoom(&CC->room, hold_rm);
1170 cprintf("%d You do not have a calendar.\n", ERROR + ROOM_NOT_FOUND);
1174 cprintf("%d Conflicting events:\n", LISTING_FOLLOWS);
1176 CtdlForEachMessage(MSGS_ALL, 0, NULL,
1179 ical_hunt_for_conflicts_backend,
1184 CtdlGetRoom(&CC->room, hold_rm); /* return to saved room */
1190 * Hunt for conflicts (Phase 1 -- retrieve the object and call Phase 2)
1192 void ical_conflicts(long msgnum, char *partnum) {
1193 struct CtdlMessage *msg = NULL;
1194 struct ical_respond_data ird;
1196 msg = CtdlFetchMessage(msgnum, 1, 1);
1198 cprintf("%d Message %ld not found\n",
1199 ERROR + ILLEGAL_VALUE,
1205 memset(&ird, 0, sizeof ird);
1206 strcpy(ird.desired_partnum, partnum);
1207 mime_parser(CM_RANGE(msg, eMesageText),
1208 *ical_locate_part, /* callback function */
1210 (void *) &ird, /* user data */
1216 if (ird.cal != NULL) {
1217 ical_hunt_for_conflicts(ird.cal);
1218 icalcomponent_free(ird.cal);
1222 cprintf("%d No calendar object found\n", ERROR + ROOM_NOT_FOUND);
1227 * Look for busy time in a VEVENT and add it to the supplied VFREEBUSY.
1229 * fb The VFREEBUSY component to which we are appending
1230 * top_level_cal The top-level VCALENDAR component which contains a VEVENT to be added
1232 void ical_add_to_freebusy(icalcomponent *fb, icalcomponent *top_level_cal) {
1236 struct icalperiodtype this_event_period = icalperiodtype_null_period();
1237 icaltimetype dtstart;
1240 /* recur variables */
1241 icalproperty *rrule = NULL;
1242 struct icalrecurrencetype recur;
1243 icalrecur_iterator *ritr = NULL;
1244 struct icaldurationtype dur;
1247 if (!top_level_cal) return;
1249 /* Find the VEVENT component containing an event */
1250 cal = icalcomponent_get_first_component(top_level_cal, ICAL_VEVENT_COMPONENT);
1253 /* If this event is not opaque, the user isn't publishing it as
1254 * busy time, so don't bother doing anything else.
1256 p = icalcomponent_get_first_property(cal, ICAL_TRANSP_PROPERTY);
1258 v = icalproperty_get_value(p);
1260 if (icalvalue_get_transp(v) != ICAL_TRANSP_OPAQUE) {
1267 * Now begin calculating the event start and end times.
1269 p = icalcomponent_get_first_property(cal, ICAL_DTSTART_PROPERTY);
1271 dtstart = icalproperty_get_dtstart(p);
1273 if (icaltime_is_utc(dtstart)) {
1274 dtstart.zone = icaltimezone_get_utc_timezone();
1277 dtstart.zone = icalcomponent_get_timezone(top_level_cal,
1278 icalparameter_get_tzid(
1279 icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER)
1282 if (!dtstart.zone) {
1283 dtstart.zone = get_default_icaltimezone();
1287 dtend = icalcomponent_get_dtend(cal);
1288 if (!icaltime_is_null_time(dtend)) {
1289 dur = icaltime_subtract(dtend, dtstart);
1292 memset (&dur, 0, sizeof(struct icaldurationtype));
1295 /* Is a recurrence specified? If so, get ready to process it... */
1296 rrule = ical_ctdl_get_subprop(cal, ICAL_RRULE_PROPERTY);
1298 recur = icalproperty_get_rrule(rrule);
1299 ritr = icalrecur_iterator_new(recur, dtstart);
1303 /* Convert the DTSTART and DTEND properties to an icalperiod. */
1304 this_event_period.start = dtstart;
1306 if (!icaltime_is_null_time(dtend)) {
1307 this_event_period.end = dtend;
1310 /* Convert the timestamps to UTC. It's ok to do this because we've already expanded
1311 * recurrences and this data is never going to get used again.
1313 this_event_period.start = icaltime_convert_to_zone(
1314 this_event_period.start,
1315 icaltimezone_get_utc_timezone()
1317 this_event_period.end = icaltime_convert_to_zone(
1318 this_event_period.end,
1319 icaltimezone_get_utc_timezone()
1323 icalcomponent_add_property(fb, icalproperty_new_freebusy(this_event_period));
1325 /* Make sure the DTSTART property of the freebusy *list* is set to
1326 * the DTSTART property of the *earliest event*.
1328 p = icalcomponent_get_first_property(fb, ICAL_DTSTART_PROPERTY);
1330 icalcomponent_set_dtstart(fb, this_event_period.start);
1333 if (icaltime_compare(this_event_period.start, icalcomponent_get_dtstart(fb)) < 0) {
1334 icalcomponent_set_dtstart(fb, this_event_period.start);
1338 /* Make sure the DTEND property of the freebusy *list* is set to
1339 * the DTEND property of the *latest event*.
1341 p = icalcomponent_get_first_property(fb, ICAL_DTEND_PROPERTY);
1343 icalcomponent_set_dtend(fb, this_event_period.end);
1346 if (icaltime_compare(this_event_period.end, icalcomponent_get_dtend(fb)) > 0) {
1347 icalcomponent_set_dtend(fb, this_event_period.end);
1352 dtstart = icalrecur_iterator_next(ritr);
1353 if (!icaltime_is_null_time(dtend)) {
1354 dtend = icaltime_add(dtstart, dur);
1355 dtend.zone = dtstart.zone;
1360 } while ( (rrule) && (!icaltime_is_null_time(dtstart)) && (num_recur < MAX_RECUR) ) ;
1361 icalrecur_iterator_free(ritr);
1366 * Backend for ical_freebusy()
1368 * This function simply loads the messages in the user's calendar room,
1369 * which contain VEVENTs, then strips them of all non-freebusy data, and
1370 * adds them to the supplied VCALENDAR.
1373 void ical_freebusy_backend(long msgnum, void *data) {
1375 struct CtdlMessage *msg = NULL;
1376 struct ical_respond_data ird;
1378 fb = (icalcomponent *)data; /* User-supplied data will be the VFREEBUSY component */
1380 msg = CtdlFetchMessage(msgnum, 1, 1);
1381 if (msg == NULL) return;
1382 memset(&ird, 0, sizeof ird);
1383 strcpy(ird.desired_partnum, "_HUNT_");
1384 mime_parser(CM_RANGE(msg, eMesageText),
1385 *ical_locate_part, /* callback function */
1387 (void *) &ird, /* user data */
1393 ical_add_to_freebusy(fb, ird.cal); /* Add VEVENT times to VFREEBUSY */
1394 icalcomponent_free(ird.cal);
1400 * Grab another user's free/busy times
1402 void ical_freebusy(char *who) {
1403 struct ctdluser usbuf;
1404 char calendar_room_name[ROOMNAMELEN];
1405 char hold_rm[ROOMNAMELEN];
1406 char *serialized_request = NULL;
1407 icalcomponent *encaps = NULL;
1408 icalcomponent *fb = NULL;
1409 int found_user = (-1);
1410 recptypes *recp = NULL;
1415 int config_lines = 0;
1417 /* First try an exact match. */
1418 found_user = CtdlGetUser(&usbuf, who);
1420 /* If not found, try it as an unqualified email address. */
1421 if (found_user != 0) {
1423 recp = validate_recipients(buf, NULL, 0);
1424 syslog(LOG_DEBUG, "calendar: trying <%s>", buf);
1426 if (recp->num_local == 1) {
1427 found_user = CtdlGetUser(&usbuf, recp->recp_local);
1429 free_recipients(recp);
1433 /* If still not found, try it as an address qualified with the
1434 * primary FQDN of this Citadel node.
1436 if (found_user != 0) {
1437 snprintf(buf, sizeof buf, "%s@%s", who, CtdlGetConfigStr("c_fqdn"));
1438 syslog(LOG_DEBUG, "calendar: trying <%s>", buf);
1439 recp = validate_recipients(buf, NULL, 0);
1441 if (recp->num_local == 1) {
1442 found_user = CtdlGetUser(&usbuf, recp->recp_local);
1444 free_recipients(recp);
1448 /* Still not found? Try qualifying it with every domain we
1449 * might have addresses in.
1451 if (found_user != 0) {
1452 config_lines = num_tokens(inetcfg, '\n');
1453 for (i=0; ((i < config_lines) && (found_user != 0)); ++i) {
1454 extract_token(buf, inetcfg, i, '\n', sizeof buf);
1455 extract_token(host, buf, 0, '|', sizeof host);
1456 extract_token(type, buf, 1, '|', sizeof type);
1458 if ( (!strcasecmp(type, "localhost"))
1459 || (!strcasecmp(type, "directory")) ) {
1460 snprintf(buf, sizeof buf, "%s@%s", who, host);
1461 syslog(LOG_DEBUG, "calendar: trying <%s>", buf);
1462 recp = validate_recipients(buf, NULL, 0);
1464 if (recp->num_local == 1) {
1465 found_user = CtdlGetUser(&usbuf, recp->recp_local);
1467 free_recipients(recp);
1473 if (found_user != 0) {
1474 cprintf("%d No such user.\n", ERROR + NO_SUCH_USER);
1478 CtdlMailboxName(calendar_room_name, sizeof calendar_room_name,
1479 &usbuf, USERCALENDARROOM);
1481 strcpy(hold_rm, CC->room.QRname); /* save current room */
1483 if (CtdlGetRoom(&CC->room, calendar_room_name) != 0) {
1484 cprintf("%d Cannot open calendar\n", ERROR + ROOM_NOT_FOUND);
1485 CtdlGetRoom(&CC->room, hold_rm);
1489 /* Create a VFREEBUSY subcomponent */
1490 syslog(LOG_DEBUG, "calendar: creating VFREEBUSY component");
1491 fb = icalcomponent_new_vfreebusy();
1493 cprintf("%d Internal error: cannot allocate memory.\n", ERROR + INTERNAL_ERROR);
1494 CtdlGetRoom(&CC->room, hold_rm);
1498 /* Set the method to PUBLISH */
1499 icalcomponent_set_method(fb, ICAL_METHOD_PUBLISH);
1501 /* Set the DTSTAMP to right now. */
1502 icalcomponent_set_dtstamp(fb, icaltime_from_timet_with_zone(time(NULL), 0, icaltimezone_get_utc_timezone()));
1504 /* Add the user's email address as ORGANIZER */
1505 sprintf(buf, "MAILTO:%s", who);
1506 if (strchr(buf, '@') == NULL) {
1508 strcat(buf, CtdlGetConfigStr("c_fqdn"));
1510 for (i=0; buf[i]; ++i) {
1511 if (buf[i]==' ') buf[i] = '_';
1513 icalcomponent_add_property(fb, icalproperty_new_organizer(buf));
1515 /* Add busy time from events */
1516 syslog(LOG_DEBUG, "calendar: adding busy time from events");
1517 CtdlForEachMessage(MSGS_ALL, 0, NULL, NULL, NULL, ical_freebusy_backend, (void *)fb );
1519 /* If values for DTSTART and DTEND are still not present, set them
1520 * to yesterday and tomorrow as default values.
1522 if (icalcomponent_get_first_property(fb, ICAL_DTSTART_PROPERTY) == NULL) {
1523 icalcomponent_set_dtstart(fb, icaltime_from_timet_with_zone(time(NULL)-86400L, 0, icaltimezone_get_utc_timezone()));
1525 if (icalcomponent_get_first_property(fb, ICAL_DTEND_PROPERTY) == NULL) {
1526 icalcomponent_set_dtend(fb, icaltime_from_timet_with_zone(time(NULL)+86400L, 0, icaltimezone_get_utc_timezone()));
1529 /* Put the freebusy component into the calendar component */
1530 syslog(LOG_DEBUG, "calendar: encapsulating");
1531 encaps = ical_encapsulate_subcomponent(fb);
1532 if (encaps == NULL) {
1533 icalcomponent_free(fb);
1534 cprintf("%d Internal error: cannot allocate memory.\n",
1535 ERROR + INTERNAL_ERROR);
1536 CtdlGetRoom(&CC->room, hold_rm);
1540 /* Set the method to PUBLISH */
1541 syslog(LOG_DEBUG, "calendar: setting method");
1542 icalcomponent_set_method(encaps, ICAL_METHOD_PUBLISH);
1545 syslog(LOG_DEBUG, "calendar: serializing");
1546 serialized_request = icalcomponent_as_ical_string_r(encaps);
1547 icalcomponent_free(encaps); /* Don't need this anymore. */
1549 cprintf("%d Free/busy for %s\n", LISTING_FOLLOWS, usbuf.fullname);
1550 if (serialized_request != NULL) {
1551 client_write(serialized_request, strlen(serialized_request));
1552 free(serialized_request);
1556 /* Go back to the room from which we came... */
1557 CtdlGetRoom(&CC->room, hold_rm);
1562 * Backend for ical_getics()
1564 * This is a ForEachMessage() callback function that searches the current room
1565 * for calendar events and adds them each into one big calendar component.
1567 void ical_getics_backend(long msgnum, void *data) {
1568 icalcomponent *encaps, *c;
1569 struct CtdlMessage *msg = NULL;
1570 struct ical_respond_data ird;
1572 encaps = (icalcomponent *)data;
1573 if (encaps == NULL) return;
1575 /* Look for the calendar event... */
1577 msg = CtdlFetchMessage(msgnum, 1, 1);
1578 if (msg == NULL) return;
1579 memset(&ird, 0, sizeof ird);
1580 strcpy(ird.desired_partnum, "_HUNT_");
1581 mime_parser(CM_RANGE(msg, eMesageText),
1582 *ical_locate_part, /* callback function */
1584 (void *) &ird, /* user data */
1589 if (ird.cal == NULL) return;
1591 /* Here we go: put the VEVENT into the VCALENDAR. We now no longer
1592 * are responsible for "the_request"'s memory -- it will be freed
1593 * when we free "encaps".
1596 /* If the top-level component is *not* a VCALENDAR, we can drop it right
1597 * in. This will almost never happen.
1599 if (icalcomponent_isa(ird.cal) != ICAL_VCALENDAR_COMPONENT) {
1600 icalcomponent_add_component(encaps, ird.cal);
1603 * In the more likely event that we're looking at a VCALENDAR with the VEVENT
1604 * and other components encapsulated inside, we have to extract them.
1607 for (c = icalcomponent_get_first_component(ird.cal, ICAL_ANY_COMPONENT);
1609 c = icalcomponent_get_next_component(ird.cal, ICAL_ANY_COMPONENT)) {
1611 /* For VTIMEZONE components, suppress duplicates of the same tzid */
1613 if (icalcomponent_isa(c) == ICAL_VTIMEZONE_COMPONENT) {
1614 icalproperty *p = icalcomponent_get_first_property(c, ICAL_TZID_PROPERTY);
1616 const char *tzid = icalproperty_get_tzid(p);
1617 if (!icalcomponent_get_timezone(encaps, tzid)) {
1618 icalcomponent_add_component(encaps,
1619 icalcomponent_new_clone(c));
1624 /* All other types of components can go in verbatim */
1626 icalcomponent_add_component(encaps, icalcomponent_new_clone(c));
1629 icalcomponent_free(ird.cal);
1635 * Retrieve all of the calendar items in the current room, and output them
1636 * as a single icalendar object.
1638 void ical_getics(void)
1640 icalcomponent *encaps = NULL;
1643 if ( (CC->room.QRdefaultview != VIEW_CALENDAR)
1644 &&(CC->room.QRdefaultview != VIEW_TASKS) ) {
1645 cprintf("%d Not a calendar room\n", ERROR+NOT_HERE);
1646 return; /* Not an iCalendar-centric room */
1649 encaps = icalcomponent_new_vcalendar();
1650 if (encaps == NULL) {
1651 syslog(LOG_ERR, "calendar: could not allocate component!");
1652 cprintf("%d Could not allocate memory\n", ERROR+INTERNAL_ERROR);
1656 cprintf("%d one big calendar\n", LISTING_FOLLOWS);
1658 /* Set the Product ID */
1659 icalcomponent_add_property(encaps, icalproperty_new_prodid(PRODID));
1661 /* Set the Version Number */
1662 icalcomponent_add_property(encaps, icalproperty_new_version("2.0"));
1664 /* Set the method to PUBLISH */
1665 icalcomponent_set_method(encaps, ICAL_METHOD_PUBLISH);
1667 /* Now go through the room encapsulating all calendar items. */
1668 CtdlForEachMessage(MSGS_ALL, 0, NULL,
1671 ical_getics_backend,
1675 ser = icalcomponent_as_ical_string_r(encaps);
1676 icalcomponent_free(encaps); /* Don't need this anymore. */
1677 client_write(ser, strlen(ser));
1684 * Helper callback function for ical_putics() to discover which TZID's we need.
1685 * Simply put the tzid name string into a hash table. After the callbacks are
1686 * done we'll go through them and attach the ones that we have.
1688 void ical_putics_grabtzids(icalparameter *param, void *data)
1690 const char *tzid = icalparameter_get_tzid(param);
1691 HashList *keys = (HashList *) data;
1693 if ( (keys) && (tzid) && (!IsEmptyStr(tzid)) ) {
1694 Put(keys, tzid, strlen(tzid), strdup(tzid), NULL);
1700 * Delete all of the calendar items in the current room, and replace them
1701 * with calendar items from a client-supplied data stream.
1703 void ical_putics(void)
1705 char *calstream = NULL;
1708 icalcomponent *encaps = NULL;
1709 HashList *tzidlist = NULL;
1715 /* Only allow this operation if we're in a room containing a calendar or tasks view */
1716 if ( (CC->room.QRdefaultview != VIEW_CALENDAR)
1717 &&(CC->room.QRdefaultview != VIEW_TASKS) ) {
1718 cprintf("%d Not a calendar room\n", ERROR+NOT_HERE);
1722 /* Only allow this operation if we have permission to overwrite the existing calendar */
1723 if (!CtdlDoIHavePermissionToDeleteMessagesFromThisRoom()) {
1724 cprintf("%d Permission denied.\n", ERROR+HIGHER_ACCESS_REQUIRED);
1728 cprintf("%d Transmit data now\n", SEND_LISTING);
1729 calstream = CtdlReadMessageBody(HKEY("000"), CtdlGetConfigLong("c_maxmsglen"), NULL, 0);
1730 if (calstream == NULL) {
1734 cal = icalcomponent_new_from_string(calstream);
1737 /* We got our data stream -- now do something with it. */
1739 /* Delete the existing messages in the room, because we are overwriting
1740 * the entire calendar with an entire new (or updated) calendar.
1741 * (Careful: this opens an S_ROOMS critical section!)
1743 CtdlDeleteMessages(CC->room.QRname, NULL, 0, "");
1745 /* If the top-level component is *not* a VCALENDAR, we can drop it right
1746 * in. This will almost never happen.
1748 if (icalcomponent_isa(cal) != ICAL_VCALENDAR_COMPONENT) {
1749 ical_write_to_cal(NULL, cal);
1752 * In the more likely event that we're looking at a VCALENDAR with the VEVENT
1753 * and other components encapsulated inside, we have to extract them.
1756 for (c = icalcomponent_get_first_component(cal, ICAL_ANY_COMPONENT);
1758 c = icalcomponent_get_next_component(cal, ICAL_ANY_COMPONENT)) {
1760 /* Non-VTIMEZONE components each get written as individual messages.
1761 * But we also need to attach the relevant VTIMEZONE components to them.
1763 if ( (icalcomponent_isa(c) != ICAL_VTIMEZONE_COMPONENT)
1764 && (encaps = icalcomponent_new_vcalendar()) ) {
1765 icalcomponent_add_property(encaps, icalproperty_new_prodid(PRODID));
1766 icalcomponent_add_property(encaps, icalproperty_new_version("2.0"));
1767 icalcomponent_set_method(encaps, ICAL_METHOD_PUBLISH);
1769 /* Attach any needed timezones here */
1770 tzidlist = NewHash(1, NULL);
1772 icalcomponent_foreach_tzid(c, ical_putics_grabtzids, tzidlist);
1774 HashPos = GetNewHashPos(tzidlist, 0);
1776 while (GetNextHashPos(tzidlist, HashPos, &len, &Key, &Value)) {
1777 syslog(LOG_DEBUG, "calendar: attaching timezone '%s'", (char*) Value);
1778 icaltimezone *t = NULL;
1780 /* First look for a timezone attached to the original calendar */
1781 t = icalcomponent_get_timezone(cal, Value);
1783 /* Try built-in tzdata if the right one wasn't attached */
1785 t = icaltimezone_get_builtin_timezone(Value);
1788 /* I've got a valid timezone to attach. */
1790 icalcomponent_add_component(encaps,
1791 icalcomponent_new_clone(
1792 icaltimezone_get_component(t)
1798 DeleteHashPos(&HashPos);
1799 DeleteHash(&tzidlist);
1801 /* Now attach the component itself (usually a VEVENT or VTODO) */
1802 icalcomponent_add_component(encaps, icalcomponent_new_clone(c));
1804 /* Write it to the message store */
1805 ical_write_to_cal(NULL, encaps);
1806 icalcomponent_free(encaps);
1811 icalcomponent_free(cal);
1816 * All Citadel calendar commands from the client come through here.
1818 void cmd_ical(char *argbuf)
1826 extract_token(subcmd, argbuf, 0, '|', sizeof subcmd);
1828 /* Allow "test" and "freebusy" subcommands without logging in. */
1830 if (!strcasecmp(subcmd, "test")) {
1831 cprintf("%d This server supports calendaring\n", CIT_OK);
1835 if (!strcasecmp(subcmd, "freebusy")) {
1836 extract_token(who, argbuf, 1, '|', sizeof who);
1841 if (!strcasecmp(subcmd, "sgi")) {
1842 CIT_ICAL->server_generated_invitations = (extract_int(argbuf, 1) ? 1 : 0) ;
1843 cprintf("%d %d\n", CIT_OK, CIT_ICAL->server_generated_invitations);
1847 if (CtdlAccessCheck(ac_logged_in)) return;
1849 if (!strcasecmp(subcmd, "respond")) {
1850 msgnum = extract_long(argbuf, 1);
1851 extract_token(partnum, argbuf, 2, '|', sizeof partnum);
1852 extract_token(action, argbuf, 3, '|', sizeof action);
1853 ical_respond(msgnum, partnum, action);
1857 if (!strcasecmp(subcmd, "handle_rsvp")) {
1858 msgnum = extract_long(argbuf, 1);
1859 extract_token(partnum, argbuf, 2, '|', sizeof partnum);
1860 extract_token(action, argbuf, 3, '|', sizeof action);
1861 ical_handle_rsvp(msgnum, partnum, action);
1865 if (!strcasecmp(subcmd, "conflicts")) {
1866 msgnum = extract_long(argbuf, 1);
1867 extract_token(partnum, argbuf, 2, '|', sizeof partnum);
1868 ical_conflicts(msgnum, partnum);
1872 if (!strcasecmp(subcmd, "getics")) {
1877 if (!strcasecmp(subcmd, "putics")) {
1882 cprintf("%d Invalid subcommand\n", ERROR + CMD_NOT_SUPPORTED);
1887 * We don't know if the calendar room exists so we just create it at login
1889 void ical_CtdlCreateRoom(void)
1894 /* Create the calendar room if it doesn't already exist */
1895 CtdlCreateRoom(USERCALENDARROOM, 4, "", 0, 1, 0, VIEW_CALENDAR);
1897 /* Set expiration policy to manual; otherwise objects will be lost! */
1898 if (CtdlGetRoomLock(&qr, USERCALENDARROOM)) {
1899 syslog(LOG_ERR, "calendar: couldn't get the user calendar room");
1902 qr.QRep.expire_mode = EXPIRE_MANUAL;
1903 qr.QRdefaultview = VIEW_CALENDAR; /* 3 = calendar view */
1904 CtdlPutRoomLock(&qr);
1906 /* Set the view to a calendar view */
1907 CtdlGetRelationship(&vbuf, &CC->user, &qr);
1908 vbuf.v_view = VIEW_CALENDAR;
1909 CtdlSetRelationship(&vbuf, &CC->user, &qr);
1911 /* Create the tasks list room if it doesn't already exist */
1912 CtdlCreateRoom(USERTASKSROOM, 4, "", 0, 1, 0, VIEW_TASKS);
1914 /* Set expiration policy to manual; otherwise objects will be lost! */
1915 if (CtdlGetRoomLock(&qr, USERTASKSROOM)) {
1916 syslog(LOG_ERR, "calendar: couldn't get the user calendar room!");
1919 qr.QRep.expire_mode = EXPIRE_MANUAL;
1920 qr.QRdefaultview = VIEW_TASKS;
1921 CtdlPutRoomLock(&qr);
1923 /* Set the view to a task list view */
1924 CtdlGetRelationship(&vbuf, &CC->user, &qr);
1925 vbuf.v_view = VIEW_TASKS;
1926 CtdlSetRelationship(&vbuf, &CC->user, &qr);
1928 /* Create the notes room if it doesn't already exist */
1929 CtdlCreateRoom(USERNOTESROOM, 4, "", 0, 1, 0, VIEW_NOTES);
1931 /* Set expiration policy to manual; otherwise objects will be lost! */
1932 if (CtdlGetRoomLock(&qr, USERNOTESROOM)) {
1933 syslog(LOG_ERR, "calendar: couldn't get the user calendar room!");
1936 qr.QRep.expire_mode = EXPIRE_MANUAL;
1937 qr.QRdefaultview = VIEW_NOTES;
1938 CtdlPutRoomLock(&qr);
1940 /* Set the view to a notes view */
1941 CtdlGetRelationship(&vbuf, &CC->user, &qr);
1942 vbuf.v_view = VIEW_NOTES;
1943 CtdlSetRelationship(&vbuf, &CC->user, &qr);
1950 * ical_send_out_invitations() is called by ical_saving_vevent() when it finds a VEVENT.
1952 * top_level_cal is the highest available level calendar object.
1953 * cal is the subcomponent containing the VEVENT.
1955 * Note: if you change the encapsulation code here, change it in WebCit's ical_encapsulate_subcomponent()
1957 void ical_send_out_invitations(icalcomponent *top_level_cal, icalcomponent *cal) {
1958 icalcomponent *the_request = NULL;
1959 char *serialized_request = NULL;
1960 icalcomponent *encaps = NULL;
1961 char *request_message_text = NULL;
1962 struct CtdlMessage *msg = NULL;
1963 recptypes *valid = NULL;
1964 char attendees_string[SIZ];
1965 int num_attendees = 0;
1966 char this_attendee[256];
1967 icalproperty *attendee = NULL;
1968 char summary_string[SIZ];
1969 icalproperty *summary = NULL;
1972 struct icaltimetype t;
1973 const icaltimezone *attached_zones[5] = { NULL, NULL, NULL, NULL, NULL };
1975 const icaltimezone *z;
1976 int num_zones_attached = 0;
1977 int zone_already_attached;
1978 icalparameter *tzidp = NULL;
1979 const char *tzidc = NULL;
1982 syslog(LOG_ERR, "calendar: trying to reply to NULL event?");
1986 /* If this is a VCALENDAR component, look for a VEVENT subcomponent. */
1987 if (icalcomponent_isa(cal) == ICAL_VCALENDAR_COMPONENT) {
1988 ical_send_out_invitations(top_level_cal,
1989 icalcomponent_get_first_component(
1990 cal, ICAL_VEVENT_COMPONENT
1996 /* Clone the event */
1997 the_request = icalcomponent_new_clone(cal);
1998 if (the_request == NULL) {
1999 syslog(LOG_ERR, "calendar: cannot clone calendar object");
2003 /* Extract the summary string -- we'll use it as the
2004 * message subject for the request
2006 strcpy(summary_string, "Meeting request");
2007 summary = icalcomponent_get_first_property(the_request, ICAL_SUMMARY_PROPERTY);
2008 if (summary != NULL) {
2009 if (icalproperty_get_summary(summary)) {
2010 strcpy(summary_string,
2011 icalproperty_get_summary(summary) );
2015 /* Determine who the recipients of this message are (the attendees) */
2016 strcpy(attendees_string, "");
2017 for (attendee = icalcomponent_get_first_property(the_request, ICAL_ATTENDEE_PROPERTY); attendee != NULL; attendee = icalcomponent_get_next_property(the_request, ICAL_ATTENDEE_PROPERTY)) {
2018 const char *ch = icalproperty_get_attendee(attendee);
2019 if ((ch != NULL) && !strncasecmp(ch, "MAILTO:", 7)) {
2020 safestrncpy(this_attendee, ch + 7, sizeof(this_attendee));
2022 if (!CtdlIsMe(this_attendee, sizeof this_attendee)) { /* don't send an invitation to myself! */
2023 snprintf(&attendees_string[strlen(attendees_string)],
2024 sizeof(attendees_string) - strlen(attendees_string),
2033 syslog(LOG_DEBUG, "calendar: <%d> attendees: <%s>", num_attendees, attendees_string);
2035 /* If there are no attendees, there are no invitations to send, so...
2036 * don't bother putting one together! Punch out, Maverick!
2038 if (num_attendees == 0) {
2039 icalcomponent_free(the_request);
2043 /* Encapsulate the VEVENT component into a complete VCALENDAR */
2044 encaps = icalcomponent_new_vcalendar();
2045 if (encaps == NULL) {
2046 syslog(LOG_ERR, "calendar: could not allocate component!");
2047 icalcomponent_free(the_request);
2051 /* Set the Product ID */
2052 icalcomponent_add_property(encaps, icalproperty_new_prodid(PRODID));
2054 /* Set the Version Number */
2055 icalcomponent_add_property(encaps, icalproperty_new_version("2.0"));
2057 /* Set the method to REQUEST */
2058 icalcomponent_set_method(encaps, ICAL_METHOD_REQUEST);
2060 /* Look for properties containing timezone parameters, to see if we need to attach VTIMEZONEs */
2061 for (p = icalcomponent_get_first_property(the_request, ICAL_ANY_PROPERTY);
2063 p = icalcomponent_get_next_property(the_request, ICAL_ANY_PROPERTY))
2065 if ( (icalproperty_isa(p) == ICAL_COMPLETED_PROPERTY)
2066 || (icalproperty_isa(p) == ICAL_CREATED_PROPERTY)
2067 || (icalproperty_isa(p) == ICAL_DATEMAX_PROPERTY)
2068 || (icalproperty_isa(p) == ICAL_DATEMIN_PROPERTY)
2069 || (icalproperty_isa(p) == ICAL_DTEND_PROPERTY)
2070 || (icalproperty_isa(p) == ICAL_DTSTAMP_PROPERTY)
2071 || (icalproperty_isa(p) == ICAL_DTSTART_PROPERTY)
2072 || (icalproperty_isa(p) == ICAL_DUE_PROPERTY)
2073 || (icalproperty_isa(p) == ICAL_EXDATE_PROPERTY)
2074 || (icalproperty_isa(p) == ICAL_LASTMODIFIED_PROPERTY)
2075 || (icalproperty_isa(p) == ICAL_MAXDATE_PROPERTY)
2076 || (icalproperty_isa(p) == ICAL_MINDATE_PROPERTY)
2077 || (icalproperty_isa(p) == ICAL_RECURRENCEID_PROPERTY)
2079 t = icalproperty_get_dtstart(p); // it's safe to use dtstart for all of them
2081 /* Determine the tzid in order for some of the conditions below to work */
2082 tzidp = icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER);
2084 tzidc = icalparameter_get_tzid(tzidp);
2090 /* First see if there's a timezone attached to the data structure itself */
2091 if (icaltime_is_utc(t)) {
2092 z = icaltimezone_get_utc_timezone();
2095 z = icaltime_get_timezone(t);
2098 /* If not, try to determine the tzid from the parameter using attached zones */
2099 if ((!z) && (tzidc)) {
2100 z = icalcomponent_get_timezone(top_level_cal, tzidc);
2103 /* Still no good? Try our internal database */
2104 if ((!z) && (tzidc)) {
2105 z = icaltimezone_get_builtin_timezone_from_tzid(tzidc);
2109 /* We have a valid timezone. Good. Now we need to attach it. */
2111 zone_already_attached = 0;
2112 for (i=0; i<5; ++i) {
2113 if (z == attached_zones[i]) {
2114 /* We've already got this one, no need to attach another. */
2115 ++zone_already_attached;
2118 if ((!zone_already_attached) && (num_zones_attached < 5)) {
2119 /* This is a new one, so attach it. */
2120 attached_zones[num_zones_attached++] = z;
2123 icalproperty_set_parameter(p, icalparameter_new_tzid(icaltimezone_get_tzid(z))
2129 /* Encapsulate any timezones we need */
2130 if (num_zones_attached > 0) for (i=0; i<num_zones_attached; ++i) {
2132 zc = icalcomponent_new_clone(icaltimezone_get_component(attached_zones[i]));
2133 icalcomponent_add_component(encaps, zc);
2136 /* Here we go: encapsulate the VEVENT into the VCALENDAR. We now no longer
2137 * are responsible for "the_request"'s memory -- it will be freed
2138 * when we free "encaps".
2140 icalcomponent_add_component(encaps, the_request);
2143 serialized_request = icalcomponent_as_ical_string_r(encaps);
2144 icalcomponent_free(encaps); /* Don't need this anymore. */
2145 if (serialized_request == NULL) return;
2147 reqsize = strlen(serialized_request) + SIZ;
2148 request_message_text = malloc(reqsize);
2149 if (request_message_text != NULL) {
2150 snprintf(request_message_text, reqsize,
2151 "Content-type: text/calendar\r\n\r\n%s\r\n",
2155 msg = CtdlMakeMessage(
2157 NULL, /* No single recipient here */
2158 NULL, /* No single recipient here */
2164 summary_string, /* Use summary for subject */
2166 request_message_text,
2171 valid = validate_recipients(attendees_string, NULL, 0);
2172 CtdlSubmitMsg(msg, valid, "", QP_EADDR);
2174 free_recipients(valid);
2177 free(serialized_request);
2182 * When a calendar object is being saved, determine whether it's a VEVENT
2183 * and the user saving it is the organizer. If so, send out invitations
2184 * to any listed attendees.
2186 * This function is recursive. The caller can simply supply the same object
2187 * as both arguments. When it recurses it will alter the second argument
2188 * while holding on to the top level object. This allows us to go back and
2189 * grab things like time zones which might be attached.
2192 void ical_saving_vevent(icalcomponent *top_level_cal, icalcomponent *cal) {
2194 icalproperty *organizer = NULL;
2195 char organizer_string[SIZ];
2197 syslog(LOG_DEBUG, "calendar: ical_saving_vevent() has been called");
2199 /* Don't send out invitations unless the client wants us to. */
2200 if (CIT_ICAL->server_generated_invitations == 0) {
2204 /* Don't send out invitations if we've been asked not to. */
2205 if (CIT_ICAL->avoid_sending_invitations > 0) {
2209 strcpy(organizer_string, "");
2211 * The VEVENT subcomponent is the one we're interested in.
2212 * Send out invitations if, and only if, this user is the Organizer.
2214 if (icalcomponent_isa(cal) == ICAL_VEVENT_COMPONENT) {
2215 organizer = icalcomponent_get_first_property(cal, ICAL_ORGANIZER_PROPERTY);
2216 if (organizer != NULL) {
2217 if (icalproperty_get_organizer(organizer)) {
2218 strcpy(organizer_string,
2219 icalproperty_get_organizer(organizer));
2222 if (!strncasecmp(organizer_string, "MAILTO:", 7)) {
2223 strcpy(organizer_string, &organizer_string[7]);
2224 striplt(organizer_string);
2226 * If the user saving the event is listed as the
2227 * organizer, then send out invitations.
2229 if (CtdlIsMe(organizer_string, sizeof organizer_string)) {
2230 ical_send_out_invitations(top_level_cal, cal);
2235 /* If the component has subcomponents, recurse through them. */
2236 for (c = icalcomponent_get_first_component(cal, ICAL_ANY_COMPONENT);
2238 c = icalcomponent_get_next_component(cal, ICAL_ANY_COMPONENT)) {
2239 /* Recursively process subcomponent */
2240 ical_saving_vevent(top_level_cal, c);
2247 * Back end for ical_obj_beforesave()
2248 * This hunts for the UID of the calendar event (becomes Citadel msg EUID),
2249 * the summary of the event (becomes message subject),
2250 * and the start time (becomes message date/time).
2252 void ical_obj_beforesave_backend(char *name, char *filename, char *partnum,
2253 char *disp, void *content, char *cbtype, char *cbcharset, size_t length,
2254 char *encoding, char *cbid, void *cbuserdata)
2257 icalcomponent *cal, *nested_event, *nested_todo, *whole_cal;
2259 char new_uid[256] = "";
2260 struct CtdlMessage *msg = (struct CtdlMessage *) cbuserdata;
2264 /* We're only interested in calendar data. */
2265 if ( (strcasecmp(cbtype, "text/calendar"))
2266 && (strcasecmp(cbtype, "application/ics")) ) {
2270 /* Hunt for the UID and drop it in
2271 * the "user data" pointer for the MIME parser. When
2272 * ical_obj_beforesave() sees it there, it'll set the Exclusive msgid
2275 whole_cal = icalcomponent_new_from_string(content);
2278 if (icalcomponent_isa(cal) == ICAL_VCALENDAR_COMPONENT) {
2279 nested_event = icalcomponent_get_first_component(
2280 cal, ICAL_VEVENT_COMPONENT);
2281 if (nested_event != NULL) {
2285 nested_todo = icalcomponent_get_first_component(
2286 cal, ICAL_VTODO_COMPONENT);
2287 if (nested_todo != NULL) {
2295 /* Set the message EUID to the iCalendar UID */
2297 p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
2299 /* If there's no uid we must generate one */
2300 generate_uuid(new_uid);
2301 icalcomponent_add_property(cal, icalproperty_new_uid(new_uid));
2302 p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
2305 pch = icalproperty_get_comment(p);
2306 if (!IsEmptyStr(pch)) {
2307 CM_SetField(msg, eExclusiveID, pch, strlen(pch));
2308 syslog(LOG_DEBUG, "calendar: saving calendar UID <%s>", pch);
2312 /* Set the message subject to the iCalendar summary */
2314 p = ical_ctdl_get_subprop(cal, ICAL_SUMMARY_PROPERTY);
2316 pch = icalproperty_get_comment(p);
2317 if (!IsEmptyStr(pch)) {
2320 subj = rfc2047encode(pch, strlen(pch));
2321 CM_SetAsField(msg, eMsgSubject, &subj, strlen(subj));
2325 /* Set the message date/time to the iCalendar start time */
2327 p = ical_ctdl_get_subprop(cal, ICAL_DTSTART_PROPERTY);
2330 idtstart = icaltime_as_timet(icalproperty_get_dtstart(p));
2332 CM_SetFieldLONG(msg, eTimestamp, idtstart);
2337 icalcomponent_free(cal);
2338 if (whole_cal != cal) {
2339 icalcomponent_free(whole_cal);
2346 * See if we need to prevent the object from being saved (we don't allow
2347 * MIME types other than text/calendar in "calendar" or "tasks" rooms).
2349 * If the message is being saved, we also set various message header fields
2350 * using data found in the iCalendar object.
2352 int ical_obj_beforesave(struct CtdlMessage *msg, recptypes *recp)
2354 /* First determine if this is a calendar or tasks room */
2355 if ( (CC->room.QRdefaultview != VIEW_CALENDAR)
2356 && (CC->room.QRdefaultview != VIEW_TASKS)
2358 return(0); /* Not an iCalendar-centric room */
2361 /* It must be an RFC822 message! */
2362 if (msg->cm_format_type != 4) {
2363 syslog(LOG_DEBUG, "calendar: rejecting non-RFC822 message");
2364 return(1); /* You tried to save a non-RFC822 message! */
2367 if (CM_IsEmpty(msg, eMesageText)) {
2368 return(1); /* You tried to save a null message! */
2371 /* Do all of our lovely back-end parsing */
2372 mime_parser(CM_RANGE(msg, eMesageText),
2373 *ical_obj_beforesave_backend,
2384 * Things we need to do after saving a calendar event.
2386 void ical_obj_aftersave_backend(char *name, char *filename, char *partnum,
2387 char *disp, void *content, char *cbtype, char *cbcharset, size_t length,
2388 char *encoding, char *cbid, void *cbuserdata)
2392 /* We're only interested in calendar items here. */
2393 if ( (strcasecmp(cbtype, "text/calendar"))
2394 && (strcasecmp(cbtype, "application/ics")) ) {
2398 /* Hunt for the UID and drop it in
2399 * the "user data" pointer for the MIME parser. When
2400 * ical_obj_beforesave() sees it there, it'll set the Exclusive msgid
2403 if ( (!strcasecmp(cbtype, "text/calendar"))
2404 || (!strcasecmp(cbtype, "application/ics")) ) {
2405 cal = icalcomponent_new_from_string(content);
2407 ical_saving_vevent(cal, cal);
2408 icalcomponent_free(cal);
2415 * Things we need to do after saving a calendar event.
2416 * (This will start back end tasks such as automatic generation of invitations,
2417 * if such actions are appropriate.)
2419 int ical_obj_aftersave(struct CtdlMessage *msg, recptypes *recp)
2421 char roomname[ROOMNAMELEN];
2424 * If this isn't the Calendar> room, no further action is necessary.
2427 /* First determine if this is our room */
2428 CtdlMailboxName(roomname, sizeof roomname, &CC->user, USERCALENDARROOM);
2429 if (strcasecmp(roomname, CC->room.QRname)) {
2430 return(0); /* Not the Calendar room -- don't do anything. */
2433 /* It must be an RFC822 message! */
2434 if (msg->cm_format_type != 4) return(1);
2436 /* Reject null messages */
2437 if (CM_IsEmpty(msg, eMesageText)) return(1);
2439 /* Now recurse through it looking for our icalendar data */
2440 mime_parser(CM_RANGE(msg, eMesageText),
2441 *ical_obj_aftersave_backend,
2451 void ical_session_startup(void) {
2452 CIT_ICAL = malloc(sizeof(struct cit_ical));
2453 memset(CIT_ICAL, 0, sizeof(struct cit_ical));
2457 void ical_session_shutdown(void) {
2463 * Back end for ical_fixed_output()
2465 void ical_fixed_output_backend(icalcomponent *cal, int recursion_level) {
2471 p = icalcomponent_get_first_property(cal, ICAL_SUMMARY_PROPERTY);
2473 cprintf("%s\n", (const char *)icalproperty_get_comment(p));
2476 p = icalcomponent_get_first_property(cal, ICAL_LOCATION_PROPERTY);
2478 cprintf("%s\n", (const char *)icalproperty_get_comment(p));
2481 p = icalcomponent_get_first_property(cal, ICAL_DESCRIPTION_PROPERTY);
2483 cprintf("%s\n", (const char *)icalproperty_get_comment(p));
2486 /* If the component has attendees, iterate through them. */
2487 for (p = icalcomponent_get_first_property(cal, ICAL_ATTENDEE_PROPERTY); (p != NULL); p = icalcomponent_get_next_property(cal, ICAL_ATTENDEE_PROPERTY)) {
2488 ch = icalproperty_get_attendee(p);
2490 !strncasecmp(ch, "MAILTO:", 7)) {
2492 /* screen name or email address */
2493 safestrncpy(buf, ch + 7, sizeof(buf));
2495 cprintf("%s ", buf);
2500 /* If the component has subcomponents, recurse through them. */
2501 for (c = icalcomponent_get_first_component(cal, ICAL_ANY_COMPONENT);
2503 c = icalcomponent_get_next_component(cal, ICAL_ANY_COMPONENT)) {
2504 /* Recursively process subcomponent */
2505 ical_fixed_output_backend(c, recursion_level+1);
2511 * Function to output iCalendar data as plain text. Nobody uses MSG0
2512 * anymore, so really this is just so we expose the vCard data to the full
2515 void ical_fixed_output(char *ptr, int len) {
2519 stringy_cal = malloc(len + 1);
2520 safestrncpy(stringy_cal, ptr, len + 1);
2521 cal = icalcomponent_new_from_string(stringy_cal);
2528 ical_fixed_output_backend(cal, 0);
2530 /* Free the memory we obtained from libical's constructor */
2531 icalcomponent_free(cal);
2535 void serv_calendar_destroy(void) {
2536 icaltimezone_free_builtin_timezones();
2541 * Register this module with the Citadel server.
2543 CTDL_MODULE_INIT(calendar)
2548 /* Tell libical to return errors instead of aborting if it gets bad data */
2550 #ifdef LIBICAL_ICAL_EXPORT // cheap and sleazy way to detect libical >=2.0
2551 icalerror_set_errors_are_fatal(0);
2553 icalerror_errors_are_fatal = 0;
2556 /* Use our own application prefix in tzid's generated from system tzdata */
2557 icaltimezone_set_tzid_prefix("/citadel.org/");
2559 /* Initialize our hook functions */
2560 CtdlRegisterMessageHook(ical_obj_beforesave, EVT_BEFORESAVE);
2561 CtdlRegisterMessageHook(ical_obj_aftersave, EVT_AFTERSAVE);
2562 CtdlRegisterSessionHook(ical_CtdlCreateRoom, EVT_LOGIN, PRIO_LOGIN + 1);
2563 CtdlRegisterProtoHook(cmd_ical, "ICAL", "Citadel iCalendar commands");
2564 CtdlRegisterSessionHook(ical_session_startup, EVT_START, PRIO_START + 1);
2565 CtdlRegisterSessionHook(ical_session_shutdown, EVT_STOP, PRIO_STOP + 80);
2566 CtdlRegisterFixedOutputHook("text/calendar", ical_fixed_output);
2567 CtdlRegisterFixedOutputHook("application/ics", ical_fixed_output);
2568 CtdlRegisterCleanupHook(serv_calendar_destroy);
2571 /* return our module name for the log */