End support for libical <2.0
[citadel.git] / citadel / server / modules / calendar / serv_calendar.c
1 // This module implements iCalendar object processing and the Calendar>
2 // room on a Citadel server.  It handles iCalendar objects using the
3 // iTIP protocol.  See RFCs 2445 and 2446.
4 //
5 // Copyright (c) 1987-2024 by the citadel.org team
6 //
7 // This program is open source software.  Use, duplication, or disclosure
8 // are subject to the terms of the GNU General Public License version 3.
9
10 #define PRODID "-//Citadel//NONSGML Citadel Calendar//EN"
11
12 #include "../../ctdl_module.h"
13 #include <libical/ical.h>
14 #include "../../msgbase.h"
15 #include "../../internet_addressing.h"
16 #include "serv_calendar.h"
17 #include "../../room_ops.h"
18 #include "../../euidindex.h"
19 #include "../../default_timezone.h"
20 #include "../../config.h"
21
22 struct ical_respond_data {
23         char desired_partnum[SIZ];
24         icalcomponent *cal;
25 };
26
27
28 // Utility function to create a new VCALENDAR component with some of the
29 // required fields already set the way we like them.
30 icalcomponent *icalcomponent_new_citadel_vcalendar(void) {
31         icalcomponent *encaps;
32
33         encaps = icalcomponent_new_vcalendar();
34         if (encaps == NULL) {
35                 syslog(LOG_ERR, "calendar: could not allocate component");
36                 return NULL;
37         }
38
39         // Set the Product ID
40         icalcomponent_add_property(encaps, icalproperty_new_prodid(PRODID));
41
42         // Set the Version Number
43         icalcomponent_add_property(encaps, icalproperty_new_version("2.0"));
44
45         return(encaps);
46 }
47
48
49 // Utility function to encapsulate a subcomponent into a full VCALENDAR
50 icalcomponent *ical_encapsulate_subcomponent(icalcomponent *subcomp) {
51         icalcomponent *encaps;
52
53         // If we're already looking at a full VCALENDAR component, don't bother ... just return itself.
54         if (icalcomponent_isa(subcomp) == ICAL_VCALENDAR_COMPONENT) {
55                 return subcomp;
56         }
57
58         // Encapsulate the VEVENT component into a complete VCALENDAR
59         encaps = icalcomponent_new_citadel_vcalendar();
60         if (encaps == NULL) return NULL;
61
62         // Encapsulate the subcomponent inside
63         icalcomponent_add_component(encaps, subcomp);
64
65         // Return the object we just created.
66         return(encaps);
67 }
68
69
70 // Write a calendar object into the specified user's calendar room.
71 // If the supplied user is NULL, this function writes the calendar object
72 // to the currently selected room.
73 void ical_write_to_cal(struct ctdluser *u, icalcomponent *cal) {
74         char *ser = NULL;
75         long serlen;
76         icalcomponent *encaps = NULL;
77         struct CtdlMessage *msg = NULL;
78         icalcomponent *tmp=NULL;
79
80         if (cal == NULL) return;
81
82         // If the supplied object is a subcomponent, encapsulate it in
83         // a full VCALENDAR component, and save that instead.
84         if (icalcomponent_isa(cal) != ICAL_VCALENDAR_COMPONENT) {
85                 tmp = icalcomponent_new_clone(cal);
86                 encaps = ical_encapsulate_subcomponent(tmp);
87                 ical_write_to_cal(u, encaps);
88                 icalcomponent_free(tmp);
89                 icalcomponent_free(encaps);
90                 return;
91         }
92
93         ser = icalcomponent_as_ical_string_r(cal);
94         if (ser == NULL) return;
95
96         serlen = strlen(ser);
97
98         // If the caller supplied a user, write to that user's default calendar room
99         if (u) {
100                 CtdlWriteObject(                // This handy API function does all the work for us.
101                         USERCALENDARROOM,       // which room
102                         "text/calendar",        // MIME type
103                         ser,                    // data
104                         serlen + 1,             // length
105                         u,                      // which user
106                         0,                      // not binary
107                         0                       // no flags
108                 );
109         }
110
111         // If the caller did not supply a user, write to the currently selected room
112         if (!u) {
113                 struct CitContext *CCC = CC;
114                 StrBuf *MsgBody;
115
116                 msg = malloc(sizeof(struct CtdlMessage));
117                 memset(msg, 0, sizeof(struct CtdlMessage));
118                 msg->cm_magic = CTDLMESSAGE_MAGIC;
119                 msg->cm_anon_type = MES_NORMAL;
120                 msg->cm_format_type = 4;
121                 CM_SetField(msg, eAuthor, CCC->user.fullname);
122                 CM_SetField(msg, eOriginalRoom, CCC->room.QRname);
123
124                 MsgBody = NewStrBufPlain(NULL, serlen + 100);
125                 StrBufAppendBufPlain(MsgBody, HKEY("Content-type: text/calendar\r\n\r\n"), 0);
126                 StrBufAppendBufPlain(MsgBody, ser, serlen, 0);
127
128                 CM_SetAsFieldSB(msg, eMesageText, &MsgBody);
129         
130                 // Now write the data
131                 CtdlSubmitMsg(msg, NULL, "");
132                 CM_Free(msg);
133         }
134
135         // In either case, now we can free the serialized calendar object
136         free(ser);
137 }
138
139
140 // Send a reply to a meeting invitation.
141 //
142 // 'request' is the invitation to reply to.
143 // 'action' is the string "accept" or "decline" or "tentative".
144 void ical_send_a_reply(icalcomponent *request, char *action) {
145         icalcomponent *the_reply = NULL;
146         icalcomponent *vevent = NULL;
147         icalproperty *attendee = NULL;
148         char attendee_string[SIZ];
149         icalproperty *organizer = NULL;
150         char organizer_string[SIZ];
151         icalproperty *summary = NULL;
152         char summary_string[SIZ];
153         icalproperty *me_attend = NULL;
154         struct recptypes *recp = NULL;
155         icalparameter *partstat = NULL;
156         char *serialized_reply = NULL;
157         char *reply_message_text = NULL;
158         const char *ch;
159         struct CtdlMessage *msg = NULL;
160         struct recptypes *valid = NULL;
161
162         *organizer_string = '\0';
163         strcpy(summary_string, "Calendar item");
164
165         if (request == NULL) {
166                 syslog(LOG_ERR, "calendar: trying to reply to NULL event");
167                 return;
168         }
169
170         the_reply = icalcomponent_new_clone(request);
171         if (the_reply == NULL) {
172                 syslog(LOG_ERR, "calendar: cannot clone request");
173                 return;
174         }
175
176         // Change the method from REQUEST to REPLY
177         icalcomponent_set_method(the_reply, ICAL_METHOD_REPLY);
178
179         vevent = icalcomponent_get_first_component(the_reply, ICAL_VEVENT_COMPONENT);
180         if (vevent != NULL) {
181                 // Hunt for attendees, removing ones that aren't us.
182                 // (Actually, remove them all, cloning our own one so we can
183                 // re-insert it later)
184                 while (attendee = icalcomponent_get_first_property(vevent,
185                     ICAL_ATTENDEE_PROPERTY), (attendee != NULL)
186                 ) {
187                         ch = icalproperty_get_attendee(attendee);
188                         if ((ch != NULL) && !strncasecmp(ch, "MAILTO:", 7)) {
189                                 safestrncpy(attendee_string, ch + 7, sizeof (attendee_string));
190                                 string_trim(attendee_string);
191                                 recp = validate_recipients(attendee_string, NULL, 0);
192                                 if (recp != NULL) {
193                                         if (!strcasecmp(recp->recp_local, CC->user.fullname)) {
194                                                 if (me_attend) icalproperty_free(me_attend);
195                                                 me_attend = icalproperty_new_clone(attendee);
196                                         }
197                                         free_recipients(recp);
198                                 }
199                         }
200
201                         // Remove it...
202                         icalcomponent_remove_property(vevent, attendee);
203                         icalproperty_free(attendee);
204                 }
205
206                 // We found our own address in the attendee list.
207                 if (me_attend) {
208                         // Change the partstat from NEEDS-ACTION to ACCEPT or DECLINE
209                         icalproperty_remove_parameter_by_kind(me_attend, ICAL_PARTSTAT_PARAMETER);
210
211                         if (!strcasecmp(action, "accept")) {
212                                 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_ACCEPTED);
213                         }
214                         else if (!strcasecmp(action, "decline")) {
215                                 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_DECLINED);
216                         }
217                         else if (!strcasecmp(action, "tentative")) {
218                                 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_TENTATIVE);
219                         }
220
221                         if (partstat) icalproperty_add_parameter(me_attend, partstat);
222
223                         // Now insert it back into the vevent.
224                         icalcomponent_add_property(vevent, me_attend);
225                 }
226
227                 // Figure out who to send this thing to
228                 organizer = icalcomponent_get_first_property(vevent, ICAL_ORGANIZER_PROPERTY);
229                 if (organizer != NULL) {
230                         if (icalproperty_get_organizer(organizer)) {
231                                 strcpy(organizer_string,
232                                         icalproperty_get_organizer(organizer) );
233                         }
234                 }
235                 if (!strncasecmp(organizer_string, "MAILTO:", 7)) {
236                         strcpy(organizer_string, &organizer_string[7]);
237                         string_trim(organizer_string);
238                 }
239                 else {
240                         strcpy(organizer_string, "");
241                 }
242
243                 // Extract the summary string -- we'll use it as the message subject for the reply
244                 summary = icalcomponent_get_first_property(vevent, ICAL_SUMMARY_PROPERTY);
245                 if (summary != NULL) {
246                         if (icalproperty_get_summary(summary)) {
247                                 strcpy(summary_string,
248                                         icalproperty_get_summary(summary) );
249                         }
250                 }
251         }
252
253         // Now generate the reply message and send it out.
254         serialized_reply = icalcomponent_as_ical_string_r(the_reply);
255         icalcomponent_free(the_reply);  // don't need this anymore
256         if (serialized_reply == NULL) return;
257
258         reply_message_text = malloc(strlen(serialized_reply) + SIZ);
259         if (reply_message_text != NULL) {
260                 sprintf(reply_message_text,
261                         "Content-type: text/calendar; charset=\"utf-8\"\r\n\r\n%s\r\n",
262                         serialized_reply
263                 );
264
265                 msg = CtdlMakeMessage(&CC->user,
266                         organizer_string,       // to
267                         "",                     // cc
268                         CC->room.QRname,
269                         0,
270                         FMT_RFC822,
271                         "",
272                         "",
273                         summary_string,         // Use the event SUMMARY as the message subject
274                         NULL,
275                         reply_message_text,
276                         NULL
277                 );
278         
279                 if (msg != NULL) {
280                         valid = validate_recipients(organizer_string, NULL, 0);
281                         CtdlSubmitMsg(msg, valid, "");
282                         CM_Free(msg);
283                         free_recipients(valid);
284                 }
285         }
286         free(serialized_reply);
287 }
288
289
290 // Callback function for mime parser that hunts for calendar content types
291 // and turns them into calendar objects.  If something is found, it is placed
292 // in ird->cal, and the caller now owns that memory and is responsible for freeing it.
293 void ical_locate_part(char *name, char *filename, char *partnum, char *disp,
294                 void *content, char *cbtype, char *cbcharset, size_t length, char *encoding,
295                 char *cbid, void *cbuserdata) {
296
297         struct ical_respond_data *ird = NULL;
298
299         ird = (struct ical_respond_data *) cbuserdata;
300
301         // desired_partnum can be set to "_HUNT_" to have it just look for
302         // the first part with a content type of text/calendar.  Otherwise
303         // we have to only process the right one.
304         if (strcasecmp(ird->desired_partnum, "_HUNT_")) {
305                 if (strcasecmp(partnum, ird->desired_partnum)) {
306                         return;
307                 }
308         }
309
310         if (  (strcasecmp(cbtype, "text/calendar"))
311            && (strcasecmp(cbtype, "application/ics")) ) {
312                 return;
313         }
314
315         if (ird->cal != NULL) {
316                 icalcomponent_free(ird->cal);
317                 ird->cal = NULL;
318         }
319
320         ird->cal = icalcomponent_new_from_string(content);
321 }
322
323
324 // Respond to a meeting request.
325 void ical_respond(long msgnum, char *partnum, char *action) {
326         struct CtdlMessage *msg = NULL;
327         struct ical_respond_data ird;
328
329         if (
330            (strcasecmp(action, "accept"))
331            && (strcasecmp(action, "decline"))
332         ) {
333                 cprintf("%d Action must be 'accept' or 'decline'\n", ERROR + ILLEGAL_VALUE);
334                 return;
335         }
336
337         msg = CtdlFetchMessage(msgnum, 1);
338         if (msg == NULL) {
339                 cprintf("%d Message %ld not found.\n", ERROR + ILLEGAL_VALUE, (long)msgnum);
340                 return;
341         }
342
343         memset(&ird, 0, sizeof ird);
344         strcpy(ird.desired_partnum, partnum);
345         mime_parser(CM_RANGE(msg, eMesageText),
346                 *ical_locate_part,              // callback function
347                 NULL,
348                 NULL,
349                 (void *) &ird,                  // user data
350                 0
351         );
352
353         // We're done with the incoming message, because we now have a * calendar object in memory.
354         CM_Free(msg);
355
356         // Here is the real meat of this function.  Handle the event.
357         if (ird.cal != NULL) {
358                 // Save this in the user's calendar if necessary
359                 if (!strcasecmp(action, "accept")) {
360                         ical_write_to_cal(&CC->user, ird.cal);
361                 }
362
363                 // Send a reply if necessary
364                 if (icalcomponent_get_method(ird.cal) == ICAL_METHOD_REQUEST) {
365                         ical_send_a_reply(ird.cal, action);
366                 }
367
368                 // We used to delete the invitation after handling it.
369                 // We don't do that anymore, but here is the code that handled it:
370                 // CtdlDeleteMessages(CC->room.QRname, &msgnum, 1, "");
371
372                 // Free the memory we allocated and return a response.
373                 icalcomponent_free(ird.cal);
374                 ird.cal = NULL;
375                 cprintf("%d ok\n", CIT_OK);
376                 return;
377         }
378         else {
379                 cprintf("%d No calendar object found\n", ERROR + ROOM_NOT_FOUND);
380                 return;
381         }
382
383         // should never get here
384 }
385
386
387 /*
388  * Figure out the UID of the calendar event being referred to in a
389  * REPLY object.  This function is recursive.
390  */
391 void ical_learn_uid_of_reply(char *uidbuf, icalcomponent *cal) {
392         icalcomponent *subcomponent;
393         icalproperty *p;
394
395         /* If this object is a REPLY, then extract the UID. */
396         if (icalcomponent_isa(cal) == ICAL_VEVENT_COMPONENT) {
397                 p = icalcomponent_get_first_property(cal, ICAL_UID_PROPERTY);
398                 if (p != NULL) {
399                         strcpy(uidbuf, icalproperty_get_comment(p));
400                 }
401         }
402
403         /* Otherwise, recurse through any VEVENT subcomponents.  We do NOT want the
404          * UID of the reply; we want the UID of the invitation being replied to.
405          */
406         for (subcomponent = icalcomponent_get_first_component(cal, ICAL_VEVENT_COMPONENT);
407             subcomponent != NULL;
408             subcomponent = icalcomponent_get_next_component(cal, ICAL_VEVENT_COMPONENT) ) {
409                 ical_learn_uid_of_reply(uidbuf, subcomponent);
410         }
411 }
412
413
414 /*
415  * ical_update_my_calendar_with_reply() refers to this callback function; when we
416  * locate the message containing the calendar event we're replying to, this function
417  * gets called.  It basically just sticks the message number in a supplied buffer.
418  */
419 void ical_hunt_for_event_to_update(long msgnum, void *data) {
420         long *msgnumptr;
421
422         msgnumptr = (long *) data;
423         *msgnumptr = msgnum;
424 }
425
426
427 struct original_event_container {
428         icalcomponent *c;
429 };
430
431 /*
432  * Callback function for mime parser that hunts for calendar content types
433  * and turns them into calendar objects (called by ical_update_my_calendar_with_reply()
434  * to fetch the object being updated)
435  */
436 void ical_locate_original_event(char *name, char *filename, char *partnum, char *disp,
437                 void *content, char *cbtype, char *cbcharset, size_t length, char *encoding,
438                 char *cbid, void *cbuserdata) {
439
440         struct original_event_container *oec = NULL;
441
442         if (  (strcasecmp(cbtype, "text/calendar"))
443            && (strcasecmp(cbtype, "application/ics")) ) {
444                 return;
445         }
446         oec = (struct original_event_container *) cbuserdata;
447         if (oec->c != NULL) {
448                 icalcomponent_free(oec->c);
449         }
450         oec->c = icalcomponent_new_from_string(content);
451 }
452
453
454 /*
455  * Merge updated attendee information from a REPLY into an existing event.
456  */
457 void ical_merge_attendee_reply(icalcomponent *event, icalcomponent *reply) {
458         icalcomponent *c;
459         icalproperty *e_attendee, *r_attendee;
460
461         /* First things first.  If we're not looking at a VEVENT component,
462          * recurse through subcomponents until we find one.
463          */
464         if (icalcomponent_isa(event) != ICAL_VEVENT_COMPONENT) {
465                 for (c = icalcomponent_get_first_component(event, ICAL_VEVENT_COMPONENT);
466                     c != NULL;
467                     c = icalcomponent_get_next_component(event, ICAL_VEVENT_COMPONENT) ) {
468                         ical_merge_attendee_reply(c, reply);
469                 }
470                 return;
471         }
472
473         /* Now do the same thing with the reply.
474          */
475         if (icalcomponent_isa(reply) != ICAL_VEVENT_COMPONENT) {
476                 for (c = icalcomponent_get_first_component(reply, ICAL_VEVENT_COMPONENT);
477                     c != NULL;
478                     c = icalcomponent_get_next_component(reply, ICAL_VEVENT_COMPONENT) ) {
479                         ical_merge_attendee_reply(event, c);
480                 }
481                 return;
482         }
483
484         /* Clone the reply, because we're going to rip its guts out. */
485         reply = icalcomponent_new_clone(reply);
486
487         /* At this point we're looking at the correct subcomponents.
488          * Iterate through the attendees looking for a match.
489          */
490 STARTOVER:
491         for (e_attendee = icalcomponent_get_first_property(event, ICAL_ATTENDEE_PROPERTY);
492             e_attendee != NULL;
493             e_attendee = icalcomponent_get_next_property(event, ICAL_ATTENDEE_PROPERTY)) {
494
495                 for (r_attendee = icalcomponent_get_first_property(reply, ICAL_ATTENDEE_PROPERTY);
496                     r_attendee != NULL;
497                     r_attendee = icalcomponent_get_next_property(reply, ICAL_ATTENDEE_PROPERTY)) {
498
499                         /* Check to see if these two attendees match...
500                          */
501                         const char *e, *r;
502                         e = icalproperty_get_attendee(e_attendee);
503                         r = icalproperty_get_attendee(r_attendee);
504
505                         if ((e != NULL) && 
506                             (r != NULL) && 
507                             !strcasecmp(e, r)) {
508                                 /* ...and if they do, remove the attendee from the event
509                                  * and replace it with the attendee from the reply.  (The
510                                  * reply's copy will have the same address, but an updated
511                                  * status.)
512                                  */
513                                 icalcomponent_remove_property(event, e_attendee);
514                                 icalproperty_free(e_attendee);
515                                 icalcomponent_remove_property(reply, r_attendee);
516                                 icalcomponent_add_property(event, r_attendee);
517
518                                 /* Since we diddled both sets of attendees, we have to start
519                                  * the iteration over again.  This will not create an infinite
520                                  * loop because we removed the attendee from the reply.  (That's
521                                  * why we cloned the reply, and that's what we mean by "ripping
522                                  * its guts out.")
523                                  */
524                                 goto STARTOVER;
525                         }
526         
527                 }
528         }
529
530         /* Free the *clone* of the reply. */
531         icalcomponent_free(reply);
532 }
533
534
535 /*
536  * Handle an incoming RSVP (object with method==ICAL_METHOD_REPLY) for a
537  * calendar event.  The object has already been deserialized for us; all
538  * we have to do here is hunt for the event in our calendar, merge in the
539  * updated attendee status, and save it again.
540  *
541  * This function returns 0 on success, 1 if the event was not found in the
542  * user's calendar, or 2 if an internal error occurred.
543  */
544 int ical_update_my_calendar_with_reply(icalcomponent *cal) {
545         char uid[SIZ];
546         char hold_rm[ROOMNAMELEN];
547         long msgnum_being_replaced = 0;
548         struct CtdlMessage *msg = NULL;
549         struct original_event_container oec;
550         icalcomponent *original_event;
551         char *serialized_event = NULL;
552         char roomname[ROOMNAMELEN];
553         char *message_text = NULL;
554
555         /* Figure out just what event it is we're dealing with */
556         strcpy(uid, "--==<< InVaLiD uId >>==--");
557         ical_learn_uid_of_reply(uid, cal);
558         syslog(LOG_DEBUG, "calendar: UID of event being replied to is <%s>", uid);
559
560         strcpy(hold_rm, CC->room.QRname);       /* save current room */
561
562         if (CtdlGetRoom(&CC->room, USERCALENDARROOM) != 0) {
563                 CtdlGetRoom(&CC->room, hold_rm);
564                 syslog(LOG_ERR, "calendar: cannot get user calendar room");
565                 return(2);
566         }
567
568         /*
569          * Look in the EUID index for a message with
570          * the Citadel EUID set to the value we're looking for.  Since
571          * Citadel always sets the message EUID to the iCalendar UID of
572          * the event, this will work.
573          */
574         msgnum_being_replaced = CtdlLocateMessageByEuid(uid, &CC->room);
575
576         CtdlGetRoom(&CC->room, hold_rm);        /* return to saved room */
577
578         syslog(LOG_DEBUG, "calendar: msgnum_being_replaced == %ld", msgnum_being_replaced);
579         if (msgnum_being_replaced == 0) {
580                 return(1);                      /* no calendar event found */
581         }
582
583         /* Now we know the ID of the message containing the event being updated.
584          * We don't actually have to delete it; that'll get taken care of by the
585          * server when we save another event with the same UID.  This just gives
586          * us the ability to load the event into memory so we can diddle the
587          * attendees.
588          */
589         msg = CtdlFetchMessage(msgnum_being_replaced, 1);
590         if (msg == NULL) {
591                 return(2);                      /* internal error */
592         }
593         oec.c = NULL;
594         mime_parser(CM_RANGE(msg, eMesageText),
595                     *ical_locate_original_event,        /* callback function */
596                     NULL, NULL,
597                     &oec,                               /* user data */
598                     0
599         );
600         CM_Free(msg);
601
602         original_event = oec.c;
603         if (original_event == NULL) {
604                 syslog(LOG_ERR, "calendar: original_component is NULL");
605                 return(2);
606         }
607
608         /* Merge the attendee's updated status into the event */
609         ical_merge_attendee_reply(original_event, cal);
610
611         /* Serialize it */
612         serialized_event = icalcomponent_as_ical_string_r(original_event);
613         icalcomponent_free(original_event);     /* Don't need this anymore. */
614         if (serialized_event == NULL) return(2);
615
616         CtdlMailboxName(roomname, sizeof roomname, &CC->user, USERCALENDARROOM);
617
618         message_text = malloc(strlen(serialized_event) + SIZ);
619         if (message_text != NULL) {
620                 sprintf(message_text,
621                         "Content-type: text/calendar; charset=\"utf-8\"\r\n\r\n%s\r\n",
622                         serialized_event
623                 );
624
625                 msg = CtdlMakeMessage(&CC->user,
626                         "",                     /* No recipient */
627                         "",                     /* No recipient */
628                         roomname,
629                         0, FMT_RFC822,
630                         "",
631                         "",
632                         "",             /* no subject */
633                         NULL,
634                         message_text,
635                         NULL);
636         
637                 if (msg != NULL) {
638                         CIT_ICAL->avoid_sending_invitations = 1;
639                         CtdlSubmitMsg(msg, NULL, roomname);
640                         CM_Free(msg);
641                         CIT_ICAL->avoid_sending_invitations = 0;
642                 }
643         }
644         free(serialized_event);
645         return(0);
646 }
647
648
649 /*
650  * Handle an incoming RSVP for an event.  (This is the server subcommand part; it
651  * simply extracts the calendar object from the message, deserializes it, and
652  * passes it up to ical_update_my_calendar_with_reply() for processing.
653  */
654 void ical_handle_rsvp(long msgnum, char *partnum, char *action) {
655         struct CtdlMessage *msg = NULL;
656         struct ical_respond_data ird;
657         int ret;
658
659         if (
660            (strcasecmp(action, "update"))
661            && (strcasecmp(action, "ignore"))
662         ) {
663                 cprintf("%d Action must be 'update' or 'ignore'\n",
664                         ERROR + ILLEGAL_VALUE
665                 );
666                 return;
667         }
668
669         msg = CtdlFetchMessage(msgnum, 1);
670         if (msg == NULL) {
671                 cprintf("%d Message %ld not found.\n",
672                         ERROR + ILLEGAL_VALUE,
673                         (long)msgnum
674                 );
675                 return;
676         }
677
678         memset(&ird, 0, sizeof ird);
679         strcpy(ird.desired_partnum, partnum);
680         mime_parser(CM_RANGE(msg, eMesageText),
681                     *ical_locate_part,          /* callback function */
682                     NULL, NULL,
683                     (void *) &ird,                      /* user data */
684                     0
685                 );
686
687         /* We're done with the incoming message, because we now have a
688          * calendar object in memory.
689          */
690         CM_Free(msg);
691
692         /*
693          * Here is the real meat of this function.  Handle the event.
694          */
695         if (ird.cal != NULL) {
696                 /* Update the user's calendar if necessary */
697                 if (!strcasecmp(action, "update")) {
698                         ret = ical_update_my_calendar_with_reply(ird.cal);
699                         if (ret == 0) {
700                                 cprintf("%d Your calendar has been updated with this reply.\n",
701                                         CIT_OK);
702                         }
703                         else if (ret == 1) {
704                                 cprintf("%d This event does not exist in your calendar.\n",
705                                         ERROR + FILE_NOT_FOUND);
706                         }
707                         else {
708                                 cprintf("%d An internal error occurred.\n",
709                                         ERROR + INTERNAL_ERROR);
710                         }
711                 }
712                 else {
713                         cprintf("%d This reply has been ignored.\n", CIT_OK);
714                 }
715
716                 /* Now that we've processed this message, we don't need it
717                  * anymore.  So delete it.  (Don't do this anymore.)
718                 CtdlDeleteMessages(CC->room.QRname, &msgnum, 1, "");
719                  */
720
721                 /* Free the memory we allocated and return a response. */
722                 icalcomponent_free(ird.cal);
723                 ird.cal = NULL;
724                 return;
725         }
726         else {
727                 cprintf("%d No calendar object found\n", ERROR + ROOM_NOT_FOUND);
728                 return;
729         }
730
731         /* should never get here */
732 }
733
734
735 /*
736  * Search for a property in both the top level and in a VEVENT subcomponent
737  */
738 icalproperty *ical_ctdl_get_subprop(
739                 icalcomponent *cal,
740                 icalproperty_kind which_prop
741 ) {
742         icalproperty *p;
743         icalcomponent *c;
744
745         p = icalcomponent_get_first_property(cal, which_prop);
746         if (p == NULL) {
747                 c = icalcomponent_get_first_component(cal,
748                                                         ICAL_VEVENT_COMPONENT);
749                 if (c != NULL) {
750                         p = icalcomponent_get_first_property(c, which_prop);
751                 }
752         }
753         return p;
754 }
755
756
757 /*
758  * Check to see if two events overlap.  Returns nonzero if they do.
759  * (This function is used in both Citadel and WebCit.  If you change it in
760  * one place, change it in the other.  Better yet, put it in a library.)
761  */
762 int ical_ctdl_is_overlap(
763                         struct icaltimetype t1start,
764                         struct icaltimetype t1end,
765                         struct icaltimetype t2start,
766                         struct icaltimetype t2end
767 ) {
768         if (icaltime_is_null_time(t1start)) return(0);
769         if (icaltime_is_null_time(t2start)) return(0);
770
771         /* if either event lacks end time, assume end = start */
772         if (icaltime_is_null_time(t1end))
773                 memcpy(&t1end, &t1start, sizeof(struct icaltimetype));
774         else {
775                 if (t1end.is_date && icaltime_compare(t1start, t1end)) {
776                         /*
777                          * the end date is non-inclusive so adjust it by one
778                          * day because our test is inclusive, note that a day is
779                          * not too much because we are talking about all day
780                          * events
781                          * if start = end we assume that nevertheless the whole
782                          * day is meant
783                          */
784                         icaltime_adjust(&t1end, -1, 0, 0, 0);   
785                 }
786         }
787
788         if (icaltime_is_null_time(t2end))
789                 memcpy(&t2end, &t2start, sizeof(struct icaltimetype));
790         else {
791                 if (t2end.is_date && icaltime_compare(t2start, t2end)) {
792                         icaltime_adjust(&t2end, -1, 0, 0, 0);   
793                 }
794         }
795
796         /* First, check for all-day events */
797         if (t1start.is_date || t2start.is_date) {
798                 /* If event 1 ends before event 2 starts, we're in the clear. */
799                 if (icaltime_compare_date_only(t1end, t2start) < 0) return(0);
800
801                 /* If event 2 ends before event 1 starts, we're also ok. */
802                 if (icaltime_compare_date_only(t2end, t1start) < 0) return(0);
803
804                 return(1);
805         }
806
807         /* syslog(LOG_DEBUG, "Comparing t1start %d:%d t1end %d:%d t2start %d:%d t2end %d:%d",
808                 t1start.hour, t1start.minute, t1end.hour, t1end.minute,
809                 t2start.hour, t2start.minute, t2end.hour, t2end.minute);
810         */
811
812         /* Now check for overlaps using date *and* time. */
813
814         /* If event 1 ends before event 2 starts, we're in the clear. */
815         if (icaltime_compare(t1end, t2start) <= 0) return(0);
816         /* syslog(LOG_DEBUG, "calendar: first passed"); */
817
818         /* If event 2 ends before event 1 starts, we're also ok. */
819         if (icaltime_compare(t2end, t1start) <= 0) return(0);
820         /* syslog(LOG_DEBUG, "calendar: second passed"); */
821
822         /* Otherwise, they overlap. */
823         return(1);
824 }
825
826
827 /* 
828  * Phase 6 of "hunt for conflicts"
829  * called by ical_conflicts_phase5()
830  *
831  * Now both the proposed and existing events have been boiled down to start and end times.
832  * Check for overlap and output any conflicts.
833  *
834  * Returns nonzero if a conflict was reported.  This allows the caller to stop iterating.
835  */
836 int ical_conflicts_phase6(struct icaltimetype t1start,
837                         struct icaltimetype t1end,
838                         struct icaltimetype t2start,
839                         struct icaltimetype t2end,
840                         long existing_msgnum,
841                         char *conflict_event_uid,
842                         char *conflict_event_summary,
843                         char *compare_uid)
844 {
845         int conflict_reported = 0;
846
847         /* debugging cruft *
848         time_t tt;
849         tt = icaltime_as_timet_with_zone(t1start, t1start.zone);
850         syslog(LOG_DEBUG, "PROPOSED START: %s", ctime(&tt));
851         tt = icaltime_as_timet_with_zone(t1end, t1end.zone);
852         syslog(LOG_DEBUG, "  PROPOSED END: %s", ctime(&tt));
853         tt = icaltime_as_timet_with_zone(t2start, t2start.zone);
854         syslog(LOG_DEBUG, "EXISTING START: %s", ctime(&tt));
855         tt = icaltime_as_timet_with_zone(t2end, t2end.zone);
856         syslog(LOG_DEBUG, "  EXISTING END: %s", ctime(&tt));
857         * debugging cruft */
858
859         /* compare and output */
860
861         if (ical_ctdl_is_overlap(t1start, t1end, t2start, t2end)) {
862                 cprintf("%ld||%s|%s|%d|\n",
863                         existing_msgnum,
864                         conflict_event_uid,
865                         conflict_event_summary,
866                         (       (!IsEmptyStr(compare_uid)
867                                 &&(!strcasecmp(compare_uid,
868                                 conflict_event_uid))) ? 1 : 0
869                                 )
870                         );
871                 conflict_reported = 1;
872         }
873
874         return(conflict_reported);
875 }
876
877
878 /*
879  * Phase 5 of "hunt for conflicts"
880  * Called by ical_conflicts_phase4()
881  *
882  * We have the proposed event boiled down to start and end times.
883  * Now check it against an existing event. 
884  */
885 void ical_conflicts_phase5(struct icaltimetype t1start,
886                         struct icaltimetype t1end,
887                         icalcomponent *existing_event,
888                         long existing_msgnum,
889                         char *compare_uid)
890 {
891         char conflict_event_uid[SIZ];
892         char conflict_event_summary[SIZ];
893         struct icaltimetype t2start, t2end;
894         icalproperty *p;
895
896         /* recur variables */
897         icalproperty *rrule = NULL;
898         struct icalrecurrencetype recur;
899         icalrecur_iterator *ritr = NULL;
900         struct icaldurationtype dur;
901         int num_recur = 0;
902
903         /* initialization */
904         strcpy(conflict_event_uid, "");
905         strcpy(conflict_event_summary, "");
906         t2start = icaltime_null_time();
907         t2end = icaltime_null_time();
908
909         /* existing event stuff */
910         p = ical_ctdl_get_subprop(existing_event, ICAL_DTSTART_PROPERTY);
911         if (p == NULL) return;
912         if (p != NULL) t2start = icalproperty_get_dtstart(p);
913         if (icaltime_is_utc(t2start)) {
914                 t2start.zone = icaltimezone_get_utc_timezone();
915         }
916         else {
917                 t2start.zone = icalcomponent_get_timezone(existing_event,
918                         icalparameter_get_tzid(
919                                 icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER)
920                         )
921                 );
922                 if (!t2start.zone) {
923                         t2start.zone = get_default_icaltimezone();
924                 }
925         }
926
927         p = ical_ctdl_get_subprop(existing_event, ICAL_DTEND_PROPERTY);
928         if (p != NULL) {
929                 t2end = icalproperty_get_dtend(p);
930
931                 if (icaltime_is_utc(t2end)) {
932                         t2end.zone = icaltimezone_get_utc_timezone();
933                 }
934                 else {
935                         t2end.zone = icalcomponent_get_timezone(existing_event,
936                                 icalparameter_get_tzid(
937                                         icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER)
938                                 )
939                         );
940                         if (!t2end.zone) {
941                                 t2end.zone = get_default_icaltimezone();
942                         }
943                 }
944                 dur = icaltime_subtract(t2end, t2start);
945         }
946         else {
947                 memset (&dur, 0, sizeof(struct icaldurationtype));
948         }
949
950         rrule = ical_ctdl_get_subprop(existing_event, ICAL_RRULE_PROPERTY);
951         if (rrule) {
952                 recur = icalproperty_get_rrule(rrule);
953                 ritr = icalrecur_iterator_new(recur, t2start);
954         }
955
956         do {
957                 p = ical_ctdl_get_subprop(existing_event, ICAL_UID_PROPERTY);
958                 if (p != NULL) {
959                         strcpy(conflict_event_uid, icalproperty_get_comment(p));
960                 }
961         
962                 p = ical_ctdl_get_subprop(existing_event, ICAL_SUMMARY_PROPERTY);
963                 if (p != NULL) {
964                         strcpy(conflict_event_summary, icalproperty_get_comment(p));
965                 }
966         
967                 if (ical_conflicts_phase6(t1start, t1end, t2start, t2end,
968                    existing_msgnum, conflict_event_uid, conflict_event_summary, compare_uid))
969                 {
970                         num_recur = MAX_RECUR + 1;      /* force it out of scope, no need to continue */
971                 }
972
973                 if (rrule) {
974                         t2start = icalrecur_iterator_next(ritr);
975                         if (!icaltime_is_null_time(t2end)) {
976                                 const icaltimezone *hold_zone = t2end.zone;
977                                 t2end = icaltime_add(t2start, dur);
978                                 t2end.zone = hold_zone;
979                         }
980                         ++num_recur;
981                 }
982
983                 if (icaltime_compare(t2start, t1end) < 0) {
984                         num_recur = MAX_RECUR + 1;      /* force it out of scope */
985                 }
986
987         } while ( (rrule) && (!icaltime_is_null_time(t2start)) && (num_recur < MAX_RECUR) );
988         icalrecur_iterator_free(ritr);
989 }
990
991
992 /*
993  * Phase 4 of "hunt for conflicts"
994  * Called by ical_hunt_for_conflicts_backend()
995  *
996  * At this point we've got it boiled down to two icalcomponent events in memory.
997  * If they conflict, output something to the client.
998  */
999 void ical_conflicts_phase4(icalcomponent *proposed_event,
1000                 icalcomponent *existing_event,
1001                 long existing_msgnum)
1002 {
1003         struct icaltimetype t1start, t1end;
1004         icalproperty *p;
1005         char compare_uid[SIZ];
1006
1007         /* recur variables */
1008         icalproperty *rrule = NULL;
1009         struct icalrecurrencetype recur;
1010         icalrecur_iterator *ritr = NULL;
1011         struct icaldurationtype dur;
1012         int num_recur = 0;
1013
1014         /* initialization */
1015         t1end = icaltime_null_time();
1016         *compare_uid = '\0';
1017
1018         /* proposed event stuff */
1019
1020         p = ical_ctdl_get_subprop(proposed_event, ICAL_DTSTART_PROPERTY);
1021         if (p == NULL)
1022                 return;
1023         else
1024                 t1start = icalproperty_get_dtstart(p);
1025
1026         if (icaltime_is_utc(t1start)) {
1027                 t1start.zone = icaltimezone_get_utc_timezone();
1028         }
1029         else {
1030                 t1start.zone = icalcomponent_get_timezone(proposed_event,
1031                         icalparameter_get_tzid(
1032                                 icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER)
1033                         )
1034                 );
1035                 if (!t1start.zone) {
1036                         t1start.zone = get_default_icaltimezone();
1037                 }
1038         }
1039         
1040         p = ical_ctdl_get_subprop(proposed_event, ICAL_DTEND_PROPERTY);
1041         if (p != NULL) {
1042                 t1end = icalproperty_get_dtend(p);
1043
1044                 if (icaltime_is_utc(t1end)) {
1045                         t1end.zone = icaltimezone_get_utc_timezone();
1046                 }
1047                 else {
1048                         t1end.zone = icalcomponent_get_timezone(proposed_event,
1049                                 icalparameter_get_tzid(
1050                                         icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER)
1051                                 )
1052                         );
1053                         if (!t1end.zone) {
1054                                 t1end.zone = get_default_icaltimezone();
1055                         }
1056                 }
1057
1058                 dur = icaltime_subtract(t1end, t1start);
1059         }
1060         else {
1061                 memset (&dur, 0, sizeof(struct icaldurationtype));
1062         }
1063
1064         rrule = ical_ctdl_get_subprop(proposed_event, ICAL_RRULE_PROPERTY);
1065         if (rrule) {
1066                 recur = icalproperty_get_rrule(rrule);
1067                 ritr = icalrecur_iterator_new(recur, t1start);
1068         }
1069
1070         p = ical_ctdl_get_subprop(proposed_event, ICAL_UID_PROPERTY);
1071         if (p != NULL) {
1072                 strcpy(compare_uid, icalproperty_get_comment(p));
1073         }
1074
1075         do {
1076                 ical_conflicts_phase5(t1start, t1end, existing_event, existing_msgnum, compare_uid);
1077
1078                 if (rrule) {
1079                         t1start = icalrecur_iterator_next(ritr);
1080                         if (!icaltime_is_null_time(t1end)) {
1081                                 const icaltimezone *hold_zone = t1end.zone;
1082                                 t1end = icaltime_add(t1start, dur);
1083                                 t1end.zone = hold_zone;
1084                         }
1085                         ++num_recur;
1086                 }
1087
1088         } while ( (rrule) && (!icaltime_is_null_time(t1start)) && (num_recur < MAX_RECUR) );
1089         icalrecur_iterator_free(ritr);
1090 }
1091
1092
1093 /*
1094  * Phase 3 of "hunt for conflicts"
1095  * Called by ical_hunt_for_conflicts()
1096  */
1097 void ical_hunt_for_conflicts_backend(long msgnum, void *data) {
1098         icalcomponent *proposed_event;
1099         struct CtdlMessage *msg = NULL;
1100         struct ical_respond_data ird;
1101
1102         proposed_event = (icalcomponent *)data;
1103
1104         msg = CtdlFetchMessage(msgnum, 1);
1105         if (msg == NULL) return;
1106         memset(&ird, 0, sizeof ird);
1107         strcpy(ird.desired_partnum, "_HUNT_");
1108         mime_parser(CM_RANGE(msg, eMesageText),
1109                     *ical_locate_part,          /* callback function */
1110                     NULL, NULL,
1111                     (void *) &ird,                      /* user data */
1112                     0
1113         );
1114         CM_Free(msg);
1115
1116         if (ird.cal == NULL) return;
1117
1118         ical_conflicts_phase4(proposed_event, ird.cal, msgnum);
1119         icalcomponent_free(ird.cal);
1120 }
1121
1122
1123 /* 
1124  * Phase 2 of "hunt for conflicts" operation.
1125  * At this point we have a calendar object which represents the VEVENT that
1126  * is proposed for addition to the calendar.  Now hunt through the user's
1127  * calendar room, and output zero or more existing VEVENTs which conflict
1128  * with this one.
1129  */
1130 void ical_hunt_for_conflicts(icalcomponent *cal) {
1131         char hold_rm[ROOMNAMELEN];
1132
1133         strcpy(hold_rm, CC->room.QRname);       /* save current room */
1134
1135         if (CtdlGetRoom(&CC->room, USERCALENDARROOM) != 0) {
1136                 CtdlGetRoom(&CC->room, hold_rm);
1137                 cprintf("%d You do not have a calendar.\n", ERROR + ROOM_NOT_FOUND);
1138                 return;
1139         }
1140
1141         cprintf("%d Conflicting events:\n", LISTING_FOLLOWS);
1142
1143         CtdlForEachMessage(MSGS_ALL, 0, NULL,
1144                 NULL,
1145                 NULL,
1146                 ical_hunt_for_conflicts_backend,
1147                 (void *) cal
1148         );
1149
1150         cprintf("000\n");
1151         CtdlGetRoom(&CC->room, hold_rm);        /* return to saved room */
1152
1153 }
1154
1155
1156 /*
1157  * Hunt for conflicts (Phase 1 -- retrieve the object and call Phase 2)
1158  */
1159 void ical_conflicts(long msgnum, char *partnum) {
1160         struct CtdlMessage *msg = NULL;
1161         struct ical_respond_data ird;
1162
1163         msg = CtdlFetchMessage(msgnum, 1);
1164         if (msg == NULL) {
1165                 cprintf("%d Message %ld not found\n",
1166                         ERROR + ILLEGAL_VALUE,
1167                         (long)msgnum
1168                 );
1169                 return;
1170         }
1171
1172         memset(&ird, 0, sizeof ird);
1173         strcpy(ird.desired_partnum, partnum);
1174         mime_parser(CM_RANGE(msg, eMesageText),
1175                     *ical_locate_part,          /* callback function */
1176                     NULL, NULL,
1177                     (void *) &ird,                      /* user data */
1178                     0
1179                 );
1180
1181         CM_Free(msg);
1182
1183         if (ird.cal != NULL) {
1184                 ical_hunt_for_conflicts(ird.cal);
1185                 icalcomponent_free(ird.cal);
1186                 return;
1187         }
1188
1189         cprintf("%d No calendar object found\n", ERROR + ROOM_NOT_FOUND);
1190 }
1191
1192
1193 /*
1194  * Look for busy time in a VEVENT and add it to the supplied VFREEBUSY.
1195  *
1196  * fb                   The VFREEBUSY component to which we are appending
1197  * top_level_cal        The top-level VCALENDAR component which contains a VEVENT to be added
1198  */
1199 void ical_add_to_freebusy(icalcomponent *fb, icalcomponent *top_level_cal) {
1200         icalcomponent *cal;
1201         icalproperty *p;
1202         icalvalue *v;
1203         struct icalperiodtype this_event_period = icalperiodtype_null_period();
1204         icaltimetype dtstart;
1205         icaltimetype dtend;
1206
1207         /* recur variables */
1208         icalproperty *rrule = NULL;
1209         struct icalrecurrencetype recur;
1210         icalrecur_iterator *ritr = NULL;
1211         struct icaldurationtype dur;
1212         int num_recur = 0;
1213
1214         if (!top_level_cal) return;
1215
1216         /* Find the VEVENT component containing an event */
1217         cal = icalcomponent_get_first_component(top_level_cal, ICAL_VEVENT_COMPONENT);
1218         if (!cal) return;
1219
1220         /* If this event is not opaque, the user isn't publishing it as
1221          * busy time, so don't bother doing anything else.
1222          */
1223         p = icalcomponent_get_first_property(cal, ICAL_TRANSP_PROPERTY);
1224         if (p != NULL) {
1225                 v = icalproperty_get_value(p);
1226                 if (v != NULL) {
1227                         if (icalvalue_get_transp(v) != ICAL_TRANSP_OPAQUE) {
1228                                 return;
1229                         }
1230                 }
1231         }
1232
1233         /*
1234          * Now begin calculating the event start and end times.
1235          */
1236         p = icalcomponent_get_first_property(cal, ICAL_DTSTART_PROPERTY);
1237         if (!p) return;
1238         dtstart = icalproperty_get_dtstart(p);
1239
1240         if (icaltime_is_utc(dtstart)) {
1241                 dtstart.zone = icaltimezone_get_utc_timezone();
1242         }
1243         else {
1244                 dtstart.zone = icalcomponent_get_timezone(top_level_cal,
1245                         icalparameter_get_tzid(
1246                                 icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER)
1247                         )
1248                 );
1249                 if (!dtstart.zone) {
1250                         dtstart.zone = get_default_icaltimezone();
1251                 }
1252         }
1253
1254         dtend = icalcomponent_get_dtend(cal);
1255         if (!icaltime_is_null_time(dtend)) {
1256                 dur = icaltime_subtract(dtend, dtstart);
1257         }
1258         else {
1259                 memset (&dur, 0, sizeof(struct icaldurationtype));
1260         }
1261
1262         /* Is a recurrence specified?  If so, get ready to process it... */
1263         rrule = ical_ctdl_get_subprop(cal, ICAL_RRULE_PROPERTY);
1264         if (rrule) {
1265                 recur = icalproperty_get_rrule(rrule);
1266                 ritr = icalrecur_iterator_new(recur, dtstart);
1267         }
1268
1269         do {
1270                 /* Convert the DTSTART and DTEND properties to an icalperiod. */
1271                 this_event_period.start = dtstart;
1272         
1273                 if (!icaltime_is_null_time(dtend)) {
1274                         this_event_period.end = dtend;
1275                 }
1276
1277                 /* Convert the timestamps to UTC.  It's ok to do this because we've already expanded
1278                  * recurrences and this data is never going to get used again.
1279                  */
1280                 this_event_period.start = icaltime_convert_to_zone(
1281                         this_event_period.start,
1282                         icaltimezone_get_utc_timezone()
1283                 );
1284                 this_event_period.end = icaltime_convert_to_zone(
1285                         this_event_period.end,
1286                         icaltimezone_get_utc_timezone()
1287                 );
1288         
1289                 /* Now add it. */
1290                 icalcomponent_add_property(fb, icalproperty_new_freebusy(this_event_period));
1291
1292                 /* Make sure the DTSTART property of the freebusy *list* is set to
1293                  * the DTSTART property of the *earliest event*.
1294                  */
1295                 p = icalcomponent_get_first_property(fb, ICAL_DTSTART_PROPERTY);
1296                 if (p == NULL) {
1297                         icalcomponent_set_dtstart(fb, this_event_period.start);
1298                 }
1299                 else {
1300                         if (icaltime_compare(this_event_period.start, icalcomponent_get_dtstart(fb)) < 0) {
1301                                 icalcomponent_set_dtstart(fb, this_event_period.start);
1302                         }
1303                 }
1304         
1305                 /* Make sure the DTEND property of the freebusy *list* is set to
1306                  * the DTEND property of the *latest event*.
1307                  */
1308                 p = icalcomponent_get_first_property(fb, ICAL_DTEND_PROPERTY);
1309                 if (p == NULL) {
1310                         icalcomponent_set_dtend(fb, this_event_period.end);
1311                 }
1312                 else {
1313                         if (icaltime_compare(this_event_period.end, icalcomponent_get_dtend(fb)) > 0) {
1314                                 icalcomponent_set_dtend(fb, this_event_period.end);
1315                         }
1316                 }
1317
1318                 if (rrule) {
1319                         dtstart = icalrecur_iterator_next(ritr);
1320                         if (!icaltime_is_null_time(dtend)) {
1321                                 dtend = icaltime_add(dtstart, dur);
1322                                 dtend.zone = dtstart.zone;
1323                         }
1324                         ++num_recur;
1325                 }
1326
1327         } while ( (rrule) && (!icaltime_is_null_time(dtstart)) && (num_recur < MAX_RECUR) ) ;
1328         icalrecur_iterator_free(ritr);
1329 }
1330
1331
1332 /*
1333  * Backend for ical_freebusy()
1334  *
1335  * This function simply loads the messages in the user's calendar room,
1336  * which contain VEVENTs, then strips them of all non-freebusy data, and
1337  * adds them to the supplied VCALENDAR.
1338  *
1339  */
1340 void ical_freebusy_backend(long msgnum, void *data) {
1341         icalcomponent *fb;
1342         struct CtdlMessage *msg = NULL;
1343         struct ical_respond_data ird;
1344
1345         fb = (icalcomponent *)data;             /* User-supplied data will be the VFREEBUSY component */
1346
1347         msg = CtdlFetchMessage(msgnum, 1);
1348         if (msg == NULL) return;
1349         memset(&ird, 0, sizeof ird);
1350         strcpy(ird.desired_partnum, "_HUNT_");
1351         mime_parser(CM_RANGE(msg, eMesageText),
1352                     *ical_locate_part,          /* callback function */
1353                     NULL, NULL,
1354                     (void *) &ird,                      /* user data */
1355                     0
1356                 );
1357         CM_Free(msg);
1358
1359         if (ird.cal) {
1360                 ical_add_to_freebusy(fb, ird.cal);              /* Add VEVENT times to VFREEBUSY */
1361                 icalcomponent_free(ird.cal);
1362         }
1363 }
1364
1365
1366 /*
1367  * Grab another user's free/busy times
1368  */
1369 void ical_freebusy(char *who) {
1370         struct ctdluser usbuf;
1371         char calendar_room_name[ROOMNAMELEN];
1372         char hold_rm[ROOMNAMELEN];
1373         char *serialized_request = NULL;
1374         icalcomponent *encaps = NULL;
1375         icalcomponent *fb = NULL;
1376         int found_user = (-1);
1377         struct recptypes *recp = NULL;
1378         char buf[256];
1379         char host[256];
1380         char type[256];
1381         int i = 0;
1382         int config_lines = 0;
1383
1384         /* First try an exact match. */
1385         found_user = CtdlGetUser(&usbuf, who);
1386
1387         /* If not found, try it as an unqualified email address. */
1388         if (found_user != 0) {
1389                 strcpy(buf, who);
1390                 recp = validate_recipients(buf, NULL, 0);
1391                 syslog(LOG_DEBUG, "calendar: trying <%s>", buf);
1392                 if (recp != NULL) {
1393                         if (recp->num_local == 1) {
1394                                 found_user = CtdlGetUser(&usbuf, recp->recp_local);
1395                         }
1396                         free_recipients(recp);
1397                 }
1398         }
1399
1400         /* If still not found, try it as an address qualified with the
1401          * primary FQDN of this Citadel node.
1402          */
1403         if (found_user != 0) {
1404                 snprintf(buf, sizeof buf, "%s@%s", who, CtdlGetConfigStr("c_fqdn"));
1405                 syslog(LOG_DEBUG, "calendar: trying <%s>", buf);
1406                 recp = validate_recipients(buf, NULL, 0);
1407                 if (recp != NULL) {
1408                         if (recp->num_local == 1) {
1409                                 found_user = CtdlGetUser(&usbuf, recp->recp_local);
1410                         }
1411                         free_recipients(recp);
1412                 }
1413         }
1414
1415         /* Still not found?  Try qualifying it with every domain we
1416          * might have addresses in.
1417          */
1418         if (found_user != 0) {
1419                 config_lines = num_tokens(inetcfg, '\n');
1420                 for (i=0; ((i < config_lines) && (found_user != 0)); ++i) {
1421                         extract_token(buf, inetcfg, i, '\n', sizeof buf);
1422                         extract_token(host, buf, 0, '|', sizeof host);
1423                         extract_token(type, buf, 1, '|', sizeof type);
1424
1425                         if ( (!strcasecmp(type, "localhost"))
1426                            || (!strcasecmp(type, "directory")) ) {
1427                                 snprintf(buf, sizeof buf, "%s@%s", who, host);
1428                                 syslog(LOG_DEBUG, "calendar: trying <%s>", buf);
1429                                 recp = validate_recipients(buf, NULL, 0);
1430                                 if (recp != NULL) {
1431                                         if (recp->num_local == 1) {
1432                                                 found_user = CtdlGetUser(&usbuf, recp->recp_local);
1433                                         }
1434                                         free_recipients(recp);
1435                                 }
1436                         }
1437                 }
1438         }
1439
1440         if (found_user != 0) {
1441                 cprintf("%d No such user.\n", ERROR + NO_SUCH_USER);
1442                 return;
1443         }
1444
1445         CtdlMailboxName(calendar_room_name, sizeof calendar_room_name,
1446                 &usbuf, USERCALENDARROOM);
1447
1448         strcpy(hold_rm, CC->room.QRname);       /* save current room */
1449
1450         if (CtdlGetRoom(&CC->room, calendar_room_name) != 0) {
1451                 cprintf("%d Cannot open calendar\n", ERROR + ROOM_NOT_FOUND);
1452                 CtdlGetRoom(&CC->room, hold_rm);
1453                 return;
1454         }
1455
1456         /* Create a VFREEBUSY subcomponent */
1457         syslog(LOG_DEBUG, "calendar: creating VFREEBUSY component");
1458         fb = icalcomponent_new_vfreebusy();
1459         if (fb == NULL) {
1460                 cprintf("%d Internal error: cannot allocate memory.\n", ERROR + INTERNAL_ERROR);
1461                 CtdlGetRoom(&CC->room, hold_rm);
1462                 return;
1463         }
1464
1465         /* Set the method to PUBLISH */
1466         icalcomponent_set_method(fb, ICAL_METHOD_PUBLISH);
1467
1468         /* Set the DTSTAMP to right now. */
1469         icalcomponent_set_dtstamp(fb, icaltime_from_timet_with_zone(time(NULL), 0, icaltimezone_get_utc_timezone()));
1470
1471         /* Add the user's email address as ORGANIZER */
1472         sprintf(buf, "MAILTO:%s", who);
1473         if (strchr(buf, '@') == NULL) {
1474                 strcat(buf, "@");
1475                 strcat(buf, CtdlGetConfigStr("c_fqdn"));
1476         }
1477         for (i=0; buf[i]; ++i) {
1478                 if (buf[i]==' ') buf[i] = '_';
1479         }
1480         icalcomponent_add_property(fb, icalproperty_new_organizer(buf));
1481
1482         /* Add busy time from events */
1483         syslog(LOG_DEBUG, "calendar: adding busy time from events");
1484         CtdlForEachMessage(MSGS_ALL, 0, NULL, NULL, NULL, ical_freebusy_backend, (void *)fb );
1485
1486         /* If values for DTSTART and DTEND are still not present, set them
1487          * to yesterday and tomorrow as default values.
1488          */
1489         if (icalcomponent_get_first_property(fb, ICAL_DTSTART_PROPERTY) == NULL) {
1490                 icalcomponent_set_dtstart(fb, icaltime_from_timet_with_zone(time(NULL)-86400L, 0, icaltimezone_get_utc_timezone()));
1491         }
1492         if (icalcomponent_get_first_property(fb, ICAL_DTEND_PROPERTY) == NULL) {
1493                 icalcomponent_set_dtend(fb, icaltime_from_timet_with_zone(time(NULL)+86400L, 0, icaltimezone_get_utc_timezone()));
1494         }
1495
1496         /* Put the freebusy component into the calendar component */
1497         syslog(LOG_DEBUG, "calendar: encapsulating");
1498         encaps = ical_encapsulate_subcomponent(fb);
1499         if (encaps == NULL) {
1500                 icalcomponent_free(fb);
1501                 cprintf("%d Internal error: cannot allocate memory.\n",
1502                         ERROR + INTERNAL_ERROR);
1503                 CtdlGetRoom(&CC->room, hold_rm);
1504                 return;
1505         }
1506
1507         /* Set the method to PUBLISH */
1508         syslog(LOG_DEBUG, "calendar: setting method");
1509         icalcomponent_set_method(encaps, ICAL_METHOD_PUBLISH);
1510
1511         /* Serialize it */
1512         syslog(LOG_DEBUG, "calendar: serializing");
1513         serialized_request = icalcomponent_as_ical_string_r(encaps);
1514         icalcomponent_free(encaps);     /* Don't need this anymore. */
1515
1516         cprintf("%d Free/busy for %s\n", LISTING_FOLLOWS, usbuf.fullname);
1517         if (serialized_request != NULL) {
1518                 client_write(serialized_request, strlen(serialized_request));
1519                 free(serialized_request);
1520         }
1521         cprintf("\n000\n");
1522
1523         /* Go back to the room from which we came... */
1524         CtdlGetRoom(&CC->room, hold_rm);
1525 }
1526
1527
1528 /*
1529  * Backend for ical_getics()
1530  * 
1531  * This is a ForEachMessage() callback function that searches the current room
1532  * for calendar events and adds them each into one big calendar component.
1533  */
1534 void ical_getics_backend(long msgnum, void *data) {
1535         icalcomponent *encaps, *c;
1536         struct CtdlMessage *msg = NULL;
1537         struct ical_respond_data ird;
1538
1539         encaps = (icalcomponent *)data;
1540         if (encaps == NULL) return;
1541
1542         /* Look for the calendar event... */
1543
1544         msg = CtdlFetchMessage(msgnum, 1);
1545         if (msg == NULL) return;
1546         memset(&ird, 0, sizeof ird);
1547         strcpy(ird.desired_partnum, "_HUNT_");
1548         mime_parser(CM_RANGE(msg, eMesageText),
1549                     *ical_locate_part,          /* callback function */
1550                     NULL, NULL,
1551                     (void *) &ird,                      /* user data */
1552                     0
1553         );
1554         CM_Free(msg);
1555
1556         if (ird.cal == NULL) return;
1557
1558         /* Here we go: put the VEVENT into the VCALENDAR.  We now no longer
1559          * are responsible for "the_request"'s memory -- it will be freed
1560          * when we free "encaps".
1561          */
1562
1563         /* If the top-level component is *not* a VCALENDAR, we can drop it right
1564          * in.  This will almost never happen.
1565          */
1566         if (icalcomponent_isa(ird.cal) != ICAL_VCALENDAR_COMPONENT) {
1567                 icalcomponent_add_component(encaps, ird.cal);
1568         }
1569         /*
1570          * In the more likely event that we're looking at a VCALENDAR with the VEVENT
1571          * and other components encapsulated inside, we have to extract them.
1572          */
1573         else {
1574                 for (c = icalcomponent_get_first_component(ird.cal, ICAL_ANY_COMPONENT);
1575                     (c != NULL);
1576                     c = icalcomponent_get_next_component(ird.cal, ICAL_ANY_COMPONENT)) {
1577
1578                         /* For VTIMEZONE components, suppress duplicates of the same tzid */
1579
1580                         if (icalcomponent_isa(c) == ICAL_VTIMEZONE_COMPONENT) {
1581                                 icalproperty *p = icalcomponent_get_first_property(c, ICAL_TZID_PROPERTY);
1582                                 if (p) {
1583                                         const char *tzid = icalproperty_get_tzid(p);
1584                                         if (!icalcomponent_get_timezone(encaps, tzid)) {
1585                                                 icalcomponent_add_component(encaps,
1586                                                                         icalcomponent_new_clone(c));
1587                                         }
1588                                 }
1589                         }
1590
1591                         /* All other types of components can go in verbatim */
1592                         else {
1593                                 icalcomponent_add_component(encaps, icalcomponent_new_clone(c));
1594                         }
1595                 }
1596                 icalcomponent_free(ird.cal);
1597         }
1598 }
1599
1600
1601 /*
1602  * Retrieve all of the calendar items in the current room, and output them
1603  * as a single icalendar object.
1604  */
1605 void ical_getics(void)
1606 {
1607         icalcomponent *encaps = NULL;
1608         char *ser = NULL;
1609
1610         if ( (CC->room.QRdefaultview != VIEW_CALENDAR)
1611            &&(CC->room.QRdefaultview != VIEW_TASKS) ) {
1612                 cprintf("%d Not a calendar room\n", ERROR+NOT_HERE);
1613                 return;         /* Not an iCalendar-centric room */
1614         }
1615
1616         encaps = icalcomponent_new_vcalendar();
1617         if (encaps == NULL) {
1618                 syslog(LOG_ERR, "calendar: could not allocate component!");
1619                 cprintf("%d Could not allocate memory\n", ERROR+INTERNAL_ERROR);
1620                 return;
1621         }
1622
1623         cprintf("%d one big calendar\n", LISTING_FOLLOWS);
1624
1625         /* Set the Product ID */
1626         icalcomponent_add_property(encaps, icalproperty_new_prodid(PRODID));
1627
1628         /* Set the Version Number */
1629         icalcomponent_add_property(encaps, icalproperty_new_version("2.0"));
1630
1631         /* Set the method to PUBLISH */
1632         icalcomponent_set_method(encaps, ICAL_METHOD_PUBLISH);
1633
1634         /* Now go through the room encapsulating all calendar items. */
1635         CtdlForEachMessage(MSGS_ALL, 0, NULL,
1636                 NULL,
1637                 NULL,
1638                 ical_getics_backend,
1639                 (void *) encaps
1640         );
1641
1642         ser = icalcomponent_as_ical_string_r(encaps);
1643         icalcomponent_free(encaps);                     /* Don't need this anymore. */
1644         client_write(ser, strlen(ser));
1645         free(ser);
1646         cprintf("\n000\n");
1647 }
1648
1649
1650 /*
1651  * Helper callback function for ical_putics() to discover which TZID's we need.
1652  * Simply put the tzid name string into a hash table.  After the callbacks are
1653  * done we'll go through them and attach the ones that we have.
1654  */
1655 void ical_putics_grabtzids(icalparameter *param, void *data)
1656 {
1657         const char *tzid = icalparameter_get_tzid(param);
1658         HashList *keys = (HashList *) data;
1659         
1660         if ( (keys) && (tzid) && (!IsEmptyStr(tzid)) ) {
1661                 Put(keys, tzid, strlen(tzid), strdup(tzid), NULL);
1662         }
1663 }
1664
1665
1666 /*
1667  * Delete all of the calendar items in the current room, and replace them
1668  * with calendar items from a client-supplied data stream.
1669  */
1670 void ical_putics(void)
1671 {
1672         char *calstream = NULL;
1673         icalcomponent *cal;
1674         icalcomponent *c;
1675         icalcomponent *encaps = NULL;
1676         HashList *tzidlist = NULL;
1677         HashPos *HashPos;
1678         void *Value;
1679         const char *Key;
1680         long len;
1681
1682         /* Only allow this operation if we're in a room containing a calendar or tasks view */
1683         if ( (CC->room.QRdefaultview != VIEW_CALENDAR)
1684            &&(CC->room.QRdefaultview != VIEW_TASKS) ) {
1685                 cprintf("%d Not a calendar room\n", ERROR+NOT_HERE);
1686                 return;
1687         }
1688
1689         /* Only allow this operation if we have permission to overwrite the existing calendar */
1690         if (!CtdlDoIHavePermissionToDeleteMessagesFromThisRoom()) {
1691                 cprintf("%d Permission denied.\n", ERROR+HIGHER_ACCESS_REQUIRED);
1692                 return;
1693         }
1694
1695         cprintf("%d Transmit data now\n", SEND_LISTING);
1696         calstream = CtdlReadMessageBody(HKEY("000"), CtdlGetConfigLong("c_maxmsglen"), NULL, 0);
1697         if (calstream == NULL) {
1698                 return;
1699         }
1700
1701         cal = icalcomponent_new_from_string(calstream);
1702         free(calstream);
1703
1704         /* We got our data stream -- now do something with it. */
1705
1706         /* Delete the existing messages in the room, because we are overwriting
1707          * the entire calendar with an entire new (or updated) calendar.
1708          * (Careful: this opens an S_ROOMS critical section!)
1709          */
1710         CtdlDeleteMessages(CC->room.QRname, NULL, 0, "");
1711
1712         /* If the top-level component is *not* a VCALENDAR, we can drop it right
1713          * in.  This will almost never happen.
1714          */
1715         if (icalcomponent_isa(cal) != ICAL_VCALENDAR_COMPONENT) {
1716                 ical_write_to_cal(NULL, cal);
1717         }
1718         /*
1719          * In the more likely event that we're looking at a VCALENDAR with the VEVENT
1720          * and other components encapsulated inside, we have to extract them.
1721          */
1722         else {
1723                 for (c = icalcomponent_get_first_component(cal, ICAL_ANY_COMPONENT);
1724                     (c != NULL);
1725                     c = icalcomponent_get_next_component(cal, ICAL_ANY_COMPONENT)) {
1726
1727                         /* Non-VTIMEZONE components each get written as individual messages.
1728                          * But we also need to attach the relevant VTIMEZONE components to them.
1729                          */
1730                         if ( (icalcomponent_isa(c) != ICAL_VTIMEZONE_COMPONENT)
1731                            && (encaps = icalcomponent_new_vcalendar()) ) {
1732                                 icalcomponent_add_property(encaps, icalproperty_new_prodid(PRODID));
1733                                 icalcomponent_add_property(encaps, icalproperty_new_version("2.0"));
1734                                 icalcomponent_set_method(encaps, ICAL_METHOD_PUBLISH);
1735
1736                                 /* Attach any needed timezones here */
1737                                 tzidlist = NewHash(1, NULL);
1738                                 if (tzidlist) {
1739                                         icalcomponent_foreach_tzid(c, ical_putics_grabtzids, tzidlist);
1740                                 }
1741                                 HashPos = GetNewHashPos(tzidlist, 0);
1742
1743                                 while (GetNextHashPos(tzidlist, HashPos, &len, &Key, &Value)) {
1744                                         syslog(LOG_DEBUG, "calendar: attaching timezone '%s'", (char*) Value);
1745                                         icaltimezone *t = NULL;
1746
1747                                         /* First look for a timezone attached to the original calendar */
1748                                         t = icalcomponent_get_timezone(cal, Value);
1749
1750                                         /* Try built-in tzdata if the right one wasn't attached */
1751                                         if (!t) {
1752                                                 t = icaltimezone_get_builtin_timezone(Value);
1753                                         }
1754
1755                                         /* I've got a valid timezone to attach. */
1756                                         if (t) {
1757                                                 icalcomponent_add_component(encaps,
1758                                                         icalcomponent_new_clone(
1759                                                                 icaltimezone_get_component(t)
1760                                                         )
1761                                                 );
1762                                         }
1763
1764                                 }
1765                                 DeleteHashPos(&HashPos);
1766                                 DeleteHash(&tzidlist);
1767
1768                                 /* Now attach the component itself (usually a VEVENT or VTODO) */
1769                                 icalcomponent_add_component(encaps, icalcomponent_new_clone(c));
1770
1771                                 /* Write it to the message store */
1772                                 ical_write_to_cal(NULL, encaps);
1773                                 icalcomponent_free(encaps);
1774                         }
1775                 }
1776         }
1777
1778         icalcomponent_free(cal);
1779 }
1780
1781
1782 /*
1783  * All Citadel calendar commands from the client come through here.
1784  */
1785 void cmd_ical(char *argbuf)
1786 {
1787         char subcmd[64];
1788         long msgnum;
1789         char partnum[256];
1790         char action[256];
1791         char who[256];
1792
1793         extract_token(subcmd, argbuf, 0, '|', sizeof subcmd);
1794
1795         /* Allow "test" and "freebusy" subcommands without logging in. */
1796
1797         if (!strcasecmp(subcmd, "test")) {
1798                 cprintf("%d This server supports calendaring\n", CIT_OK);
1799                 return;
1800         }
1801
1802         if (!strcasecmp(subcmd, "freebusy")) {
1803                 extract_token(who, argbuf, 1, '|', sizeof who);
1804                 ical_freebusy(who);
1805                 return;
1806         }
1807
1808         if (!strcasecmp(subcmd, "sgi")) {
1809                 CIT_ICAL->server_generated_invitations = (extract_int(argbuf, 1) ? 1 : 0) ;
1810                 cprintf("%d %d\n", CIT_OK, CIT_ICAL->server_generated_invitations);
1811                 return;
1812         }
1813
1814         if (CtdlAccessCheck(ac_logged_in)) return;
1815
1816         if (!strcasecmp(subcmd, "respond")) {
1817                 msgnum = extract_long(argbuf, 1);
1818                 extract_token(partnum, argbuf, 2, '|', sizeof partnum);
1819                 extract_token(action, argbuf, 3, '|', sizeof action);
1820                 ical_respond(msgnum, partnum, action);
1821                 return;
1822         }
1823
1824         if (!strcasecmp(subcmd, "handle_rsvp")) {
1825                 msgnum = extract_long(argbuf, 1);
1826                 extract_token(partnum, argbuf, 2, '|', sizeof partnum);
1827                 extract_token(action, argbuf, 3, '|', sizeof action);
1828                 ical_handle_rsvp(msgnum, partnum, action);
1829                 return;
1830         }
1831
1832         if (!strcasecmp(subcmd, "conflicts")) {
1833                 msgnum = extract_long(argbuf, 1);
1834                 extract_token(partnum, argbuf, 2, '|', sizeof partnum);
1835                 ical_conflicts(msgnum, partnum);
1836                 return;
1837         }
1838
1839         if (!strcasecmp(subcmd, "getics")) {
1840                 ical_getics();
1841                 return;
1842         }
1843
1844         if (!strcasecmp(subcmd, "putics")) {
1845                 ical_putics();
1846                 return;
1847         }
1848
1849         cprintf("%d Invalid subcommand\n", ERROR + CMD_NOT_SUPPORTED);
1850 }
1851
1852
1853 /*
1854  * We don't know if the calendar room exists so we just create it at login
1855  */
1856 void ical_CtdlCreateRoom(void)
1857 {
1858         struct ctdlroom qr;
1859         struct visit vbuf;
1860
1861         /* Create the calendar room if it doesn't already exist */
1862         CtdlCreateRoom(USERCALENDARROOM, 4, "", 0, 1, 0, VIEW_CALENDAR);
1863
1864         /* Set expiration policy to manual; otherwise objects will be lost! */
1865         if (CtdlGetRoomLock(&qr, USERCALENDARROOM)) {
1866                 syslog(LOG_ERR, "calendar: couldn't get the user calendar room");
1867                 return;
1868         }
1869         qr.QRep.expire_mode = EXPIRE_MANUAL;
1870         qr.QRdefaultview = VIEW_CALENDAR;       /* 3 = calendar view */
1871         CtdlPutRoomLock(&qr);
1872
1873         /* Set the view to a calendar view */
1874         CtdlGetRelationship(&vbuf, &CC->user, &qr);
1875         vbuf.v_view = VIEW_CALENDAR;
1876         CtdlSetRelationship(&vbuf, &CC->user, &qr);
1877
1878         /* Create the tasks list room if it doesn't already exist */
1879         CtdlCreateRoom(USERTASKSROOM, 4, "", 0, 1, 0, VIEW_TASKS);
1880
1881         /* Set expiration policy to manual; otherwise objects will be lost! */
1882         if (CtdlGetRoomLock(&qr, USERTASKSROOM)) {
1883                 syslog(LOG_ERR, "calendar: couldn't get the user calendar room!");
1884                 return;
1885         }
1886         qr.QRep.expire_mode = EXPIRE_MANUAL;
1887         qr.QRdefaultview = VIEW_TASKS;
1888         CtdlPutRoomLock(&qr);
1889
1890         /* Set the view to a task list view */
1891         CtdlGetRelationship(&vbuf, &CC->user, &qr);
1892         vbuf.v_view = VIEW_TASKS;
1893         CtdlSetRelationship(&vbuf, &CC->user, &qr);
1894
1895         /* Create the notes room if it doesn't already exist */
1896         CtdlCreateRoom(USERNOTESROOM, 4, "", 0, 1, 0, VIEW_NOTES);
1897
1898         /* Set expiration policy to manual; otherwise objects will be lost! */
1899         if (CtdlGetRoomLock(&qr, USERNOTESROOM)) {
1900                 syslog(LOG_ERR, "calendar: couldn't get the user calendar room!");
1901                 return;
1902         }
1903         qr.QRep.expire_mode = EXPIRE_MANUAL;
1904         qr.QRdefaultview = VIEW_NOTES;
1905         CtdlPutRoomLock(&qr);
1906
1907         /* Set the view to a notes view */
1908         CtdlGetRelationship(&vbuf, &CC->user, &qr);
1909         vbuf.v_view = VIEW_NOTES;
1910         CtdlSetRelationship(&vbuf, &CC->user, &qr);
1911
1912         return;
1913 }
1914
1915
1916 /*
1917  * ical_send_out_invitations() is called by ical_saving_vevent() when it finds a VEVENT.
1918  *
1919  * top_level_cal is the highest available level calendar object.
1920  * cal is the subcomponent containing the VEVENT.
1921  *
1922  * Note: if you change the encapsulation code here, change it in WebCit's ical_encapsulate_subcomponent()
1923  */
1924 void ical_send_out_invitations(icalcomponent *top_level_cal, icalcomponent *cal) {
1925         icalcomponent *the_request = NULL;
1926         char *serialized_request = NULL;
1927         icalcomponent *encaps = NULL;
1928         char *request_message_text = NULL;
1929         struct CtdlMessage *msg = NULL;
1930         struct recptypes *valid = NULL;
1931         char attendees_string[SIZ];
1932         int num_attendees = 0;
1933         char this_attendee[256];
1934         icalproperty *attendee = NULL;
1935         char summary_string[SIZ];
1936         icalproperty *summary = NULL;
1937         size_t reqsize;
1938         icalproperty *p;
1939         struct icaltimetype t;
1940         const icaltimezone *attached_zones[5] = { NULL, NULL, NULL, NULL, NULL };
1941         int i;
1942         const icaltimezone *z;
1943         int num_zones_attached = 0;
1944         int zone_already_attached;
1945         icalparameter *tzidp = NULL;
1946         const char *tzidc = NULL;
1947
1948         if (cal == NULL) {
1949                 syslog(LOG_ERR, "calendar: trying to reply to NULL event?");
1950                 return;
1951         }
1952
1953         /* If this is a VCALENDAR component, look for a VEVENT subcomponent. */
1954         if (icalcomponent_isa(cal) == ICAL_VCALENDAR_COMPONENT) {
1955                 ical_send_out_invitations(top_level_cal,
1956                         icalcomponent_get_first_component(
1957                                 cal, ICAL_VEVENT_COMPONENT
1958                         )
1959                 );
1960                 return;
1961         }
1962
1963         /* Clone the event */
1964         the_request = icalcomponent_new_clone(cal);
1965         if (the_request == NULL) {
1966                 syslog(LOG_ERR, "calendar: cannot clone calendar object");
1967                 return;
1968         }
1969
1970         /* Extract the summary string -- we'll use it as the
1971          * message subject for the request
1972          */
1973         strcpy(summary_string, "Meeting request");
1974         summary = icalcomponent_get_first_property(the_request, ICAL_SUMMARY_PROPERTY);
1975         if (summary != NULL) {
1976                 if (icalproperty_get_summary(summary)) {
1977                         strcpy(summary_string,
1978                                 icalproperty_get_summary(summary) );
1979                 }
1980         }
1981
1982         /* Determine who the recipients of this message are (the attendees) */
1983         strcpy(attendees_string, "");
1984         for (attendee = icalcomponent_get_first_property(the_request, ICAL_ATTENDEE_PROPERTY); attendee != NULL; attendee = icalcomponent_get_next_property(the_request, ICAL_ATTENDEE_PROPERTY)) {
1985                 const char *ch = icalproperty_get_attendee(attendee);
1986                 if ((ch != NULL) && !strncasecmp(ch, "MAILTO:", 7)) {
1987                         safestrncpy(this_attendee, ch + 7, sizeof(this_attendee));
1988                         
1989                         if (!CtdlIsMe(this_attendee, sizeof this_attendee)) {   /* don't send an invitation to myself! */
1990                                 snprintf(&attendees_string[strlen(attendees_string)],
1991                                          sizeof(attendees_string) - strlen(attendees_string),
1992                                          "%s, ",
1993                                          this_attendee
1994                                         );
1995                                 ++num_attendees;
1996                         }
1997                 }
1998         }
1999
2000         syslog(LOG_DEBUG, "calendar: <%d> attendees: <%s>", num_attendees, attendees_string);
2001
2002         /* If there are no attendees, there are no invitations to send, so...
2003          * don't bother putting one together!  Punch out, Maverick!
2004          */
2005         if (num_attendees == 0) {
2006                 icalcomponent_free(the_request);
2007                 return;
2008         }
2009
2010         /* Encapsulate the VEVENT component into a complete VCALENDAR */
2011         encaps = icalcomponent_new_vcalendar();
2012         if (encaps == NULL) {
2013                 syslog(LOG_ERR, "calendar: could not allocate component!");
2014                 icalcomponent_free(the_request);
2015                 return;
2016         }
2017
2018         /* Set the Product ID */
2019         icalcomponent_add_property(encaps, icalproperty_new_prodid(PRODID));
2020
2021         /* Set the Version Number */
2022         icalcomponent_add_property(encaps, icalproperty_new_version("2.0"));
2023
2024         /* Set the method to REQUEST */
2025         icalcomponent_set_method(encaps, ICAL_METHOD_REQUEST);
2026
2027         /* Look for properties containing timezone parameters, to see if we need to attach VTIMEZONEs */
2028         for (p = icalcomponent_get_first_property(the_request, ICAL_ANY_PROPERTY);
2029              p != NULL;
2030              p = icalcomponent_get_next_property(the_request, ICAL_ANY_PROPERTY))
2031         {
2032                 if ( (icalproperty_isa(p) == ICAL_COMPLETED_PROPERTY)
2033                   || (icalproperty_isa(p) == ICAL_CREATED_PROPERTY)
2034                   || (icalproperty_isa(p) == ICAL_DATEMAX_PROPERTY)
2035                   || (icalproperty_isa(p) == ICAL_DATEMIN_PROPERTY)
2036                   || (icalproperty_isa(p) == ICAL_DTEND_PROPERTY)
2037                   || (icalproperty_isa(p) == ICAL_DTSTAMP_PROPERTY)
2038                   || (icalproperty_isa(p) == ICAL_DTSTART_PROPERTY)
2039                   || (icalproperty_isa(p) == ICAL_DUE_PROPERTY)
2040                   || (icalproperty_isa(p) == ICAL_EXDATE_PROPERTY)
2041                   || (icalproperty_isa(p) == ICAL_LASTMODIFIED_PROPERTY)
2042                   || (icalproperty_isa(p) == ICAL_MAXDATE_PROPERTY)
2043                   || (icalproperty_isa(p) == ICAL_MINDATE_PROPERTY)
2044                   || (icalproperty_isa(p) == ICAL_RECURRENCEID_PROPERTY)
2045                 ) {
2046                         t = icalproperty_get_dtstart(p);        // it's safe to use dtstart for all of them
2047
2048                         /* Determine the tzid in order for some of the conditions below to work */
2049                         tzidp = icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER);
2050                         if (tzidp) {
2051                                 tzidc = icalparameter_get_tzid(tzidp);
2052                         }
2053                         else {
2054                                 tzidc = NULL;
2055                         }
2056
2057                         /* First see if there's a timezone attached to the data structure itself */
2058                         if (icaltime_is_utc(t)) {
2059                                 z = icaltimezone_get_utc_timezone();
2060                         }
2061                         else {
2062                                 z = icaltime_get_timezone(t);
2063                         }
2064
2065                         /* If not, try to determine the tzid from the parameter using attached zones */
2066                         if ((!z) && (tzidc)) {
2067                                 z = icalcomponent_get_timezone(top_level_cal, tzidc);
2068                         }
2069
2070                         /* Still no good?  Try our internal database */
2071                         if ((!z) && (tzidc)) {
2072                                 z = icaltimezone_get_builtin_timezone_from_tzid(tzidc);
2073                         }
2074
2075                         if (z) {
2076                                 /* We have a valid timezone.  Good.  Now we need to attach it. */
2077
2078                                 zone_already_attached = 0;
2079                                 for (i=0; i<5; ++i) {
2080                                         if (z == attached_zones[i]) {
2081                                                 /* We've already got this one, no need to attach another. */
2082                                                 ++zone_already_attached;
2083                                         }
2084                                 }
2085                                 if ((!zone_already_attached) && (num_zones_attached < 5)) {
2086                                         /* This is a new one, so attach it. */
2087                                         attached_zones[num_zones_attached++] = z;
2088                                 }
2089
2090                                 icalproperty_set_parameter(p, icalparameter_new_tzid(icaltimezone_get_tzid(z))
2091                                 );
2092                         }
2093                 }
2094         }
2095
2096         /* Encapsulate any timezones we need */
2097         if (num_zones_attached > 0) for (i=0; i<num_zones_attached; ++i) {
2098                 icalcomponent *zc;
2099                 zc = icalcomponent_new_clone(icaltimezone_get_component(attached_zones[i]));
2100                 icalcomponent_add_component(encaps, zc);
2101         }
2102
2103         /* Here we go: encapsulate the VEVENT into the VCALENDAR.  We now no longer
2104          * are responsible for "the_request"'s memory -- it will be freed
2105          * when we free "encaps".
2106          */
2107         icalcomponent_add_component(encaps, the_request);
2108
2109         /* Serialize it */
2110         serialized_request = icalcomponent_as_ical_string_r(encaps);
2111         icalcomponent_free(encaps);     /* Don't need this anymore. */
2112         if (serialized_request == NULL) return;
2113
2114         reqsize = strlen(serialized_request) + SIZ;
2115         request_message_text = malloc(reqsize);
2116         if (request_message_text != NULL) {
2117                 snprintf(request_message_text, reqsize,
2118                         "Content-type: text/calendar\r\n\r\n%s\r\n",
2119                         serialized_request
2120                 );
2121
2122                 msg = CtdlMakeMessage(
2123                         &CC->user,
2124                         NULL,                   /* No single recipient here */
2125                         NULL,                   /* No single recipient here */
2126                         CC->room.QRname,
2127                         0,
2128                         FMT_RFC822,
2129                         NULL,
2130                         NULL,
2131                         summary_string,         /* Use summary for subject */
2132                         NULL,
2133                         request_message_text,
2134                         NULL
2135                 );
2136         
2137                 if (msg != NULL) {
2138                         valid = validate_recipients(attendees_string, NULL, 0);
2139                         CtdlSubmitMsg(msg, valid, "");
2140                         CM_Free(msg);
2141                         free_recipients(valid);
2142                 }
2143         }
2144         free(serialized_request);
2145 }
2146
2147
2148 /*
2149  * When a calendar object is being saved, determine whether it's a VEVENT
2150  * and the user saving it is the organizer.  If so, send out invitations
2151  * to any listed attendees.
2152  *
2153  * This function is recursive.  The caller can simply supply the same object
2154  * as both arguments.  When it recurses it will alter the second argument
2155  * while holding on to the top level object.  This allows us to go back and
2156  * grab things like time zones which might be attached.
2157  *
2158  */
2159 void ical_saving_vevent(icalcomponent *top_level_cal, icalcomponent *cal) {
2160         icalcomponent *c;
2161         icalproperty *organizer = NULL;
2162         char organizer_string[SIZ];
2163
2164         syslog(LOG_DEBUG, "calendar: ical_saving_vevent() has been called");
2165
2166         /* Don't send out invitations unless the client wants us to. */
2167         if (CIT_ICAL->server_generated_invitations == 0) {
2168                 return;
2169         }
2170
2171         /* Don't send out invitations if we've been asked not to. */
2172         if (CIT_ICAL->avoid_sending_invitations > 0) {
2173                 return;
2174         }
2175
2176         strcpy(organizer_string, "");
2177         /*
2178          * The VEVENT subcomponent is the one we're interested in.
2179          * Send out invitations if, and only if, this user is the Organizer.
2180          */
2181         if (icalcomponent_isa(cal) == ICAL_VEVENT_COMPONENT) {
2182                 organizer = icalcomponent_get_first_property(cal, ICAL_ORGANIZER_PROPERTY);
2183                 if (organizer != NULL) {
2184                         if (icalproperty_get_organizer(organizer)) {
2185                                 strcpy(organizer_string,
2186                                         icalproperty_get_organizer(organizer));
2187                         }
2188                 }
2189                 if (!strncasecmp(organizer_string, "MAILTO:", 7)) {
2190                         strcpy(organizer_string, &organizer_string[7]);
2191                         string_trim(organizer_string);
2192                         /*
2193                          * If the user saving the event is listed as the
2194                          * organizer, then send out invitations.
2195                          */
2196                         if (CtdlIsMe(organizer_string, sizeof organizer_string)) {
2197                                 ical_send_out_invitations(top_level_cal, cal);
2198                         }
2199                 }
2200         }
2201
2202         /* If the component has subcomponents, recurse through them. */
2203         for (c = icalcomponent_get_first_component(cal, ICAL_ANY_COMPONENT);
2204             (c != NULL);
2205             c = icalcomponent_get_next_component(cal, ICAL_ANY_COMPONENT)) {
2206                 /* Recursively process subcomponent */
2207                 ical_saving_vevent(top_level_cal, c);
2208         }
2209
2210 }
2211
2212
2213 /*
2214  * Back end for ical_obj_beforesave()
2215  * This hunts for the UID of the calendar event (becomes Citadel msg EUID),
2216  * the summary of the event (becomes message subject),
2217  * and the start time (becomes message date/time).
2218  */
2219 void ical_obj_beforesave_backend(char *name, char *filename, char *partnum,
2220                 char *disp, void *content, char *cbtype, char *cbcharset, size_t length,
2221                 char *encoding, char *cbid, void *cbuserdata)
2222 {
2223         const char* pch;
2224         icalcomponent *cal, *nested_event, *nested_todo, *whole_cal;
2225         icalproperty *p;
2226         char new_uid[256] = "";
2227         struct CtdlMessage *msg = (struct CtdlMessage *) cbuserdata;
2228
2229         if (!msg) return;
2230
2231         /* We're only interested in calendar data. */
2232         if (  (strcasecmp(cbtype, "text/calendar"))
2233            && (strcasecmp(cbtype, "application/ics")) ) {
2234                 return;
2235         }
2236
2237         /* Hunt for the UID and drop it in
2238          * the "user data" pointer for the MIME parser.  When
2239          * ical_obj_beforesave() sees it there, it'll set the Exclusive msgid
2240          * to that string.
2241          */
2242         whole_cal = icalcomponent_new_from_string(content);
2243         cal = whole_cal;
2244         if (cal != NULL) {
2245                 if (icalcomponent_isa(cal) == ICAL_VCALENDAR_COMPONENT) {
2246                         nested_event = icalcomponent_get_first_component(
2247                                 cal, ICAL_VEVENT_COMPONENT);
2248                         if (nested_event != NULL) {
2249                                 cal = nested_event;
2250                         }
2251                         else {
2252                                 nested_todo = icalcomponent_get_first_component(
2253                                         cal, ICAL_VTODO_COMPONENT);
2254                                 if (nested_todo != NULL) {
2255                                         cal = nested_todo;
2256                                 }
2257                         }
2258                 }
2259                 
2260                 if (cal != NULL) {
2261
2262                         /* Set the message EUID to the iCalendar UID */
2263
2264                         p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
2265                         if (p == NULL) {
2266                                 /* If there's no uid we must generate one */
2267                                 generate_uuid(new_uid);
2268                                 icalcomponent_add_property(cal, icalproperty_new_uid(new_uid));
2269                                 p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
2270                         }
2271                         if (p != NULL) {
2272                                 pch = icalproperty_get_comment(p);
2273                                 if (!IsEmptyStr(pch)) {
2274                                         CM_SetField(msg, eExclusiveID, pch);
2275                                         syslog(LOG_DEBUG, "calendar: saving calendar UID <%s>", pch);
2276                                 }
2277                         }
2278
2279                         /* Set the message subject to the iCalendar summary */
2280
2281                         p = ical_ctdl_get_subprop(cal, ICAL_SUMMARY_PROPERTY);
2282                         if (p != NULL) {
2283                                 pch = icalproperty_get_comment(p);
2284                                 if (!IsEmptyStr(pch)) {
2285                                         char *subj;
2286
2287                                         subj = rfc2047encode(pch, strlen(pch));
2288                                         CM_SetAsField(msg, eMsgSubject, &subj, strlen(subj));
2289                                 }
2290                         }
2291
2292                         /* Set the message date/time to the iCalendar start time */
2293
2294                         p = ical_ctdl_get_subprop(cal, ICAL_DTSTART_PROPERTY);
2295                         if (p != NULL) {
2296                                 time_t idtstart;
2297                                 idtstart = icaltime_as_timet(icalproperty_get_dtstart(p));
2298                                 if (idtstart > 0) {
2299                                         CM_SetFieldLONG(msg, eTimestamp, idtstart);
2300                                 }
2301                         }
2302
2303                 }
2304                 icalcomponent_free(cal);
2305                 if (whole_cal != cal) {
2306                         icalcomponent_free(whole_cal);
2307                 }
2308         }
2309 }
2310
2311
2312 /*
2313  * See if we need to prevent the object from being saved (we don't allow
2314  * MIME types other than text/calendar in "calendar" or "tasks" rooms).
2315  *
2316  * If the message is being saved, we also set various message header fields
2317  * using data found in the iCalendar object.
2318  */
2319 int ical_obj_beforesave(struct CtdlMessage *msg, struct recptypes *recp)
2320 {
2321         /* First determine if this is a calendar or tasks room */
2322         if (  (CC->room.QRdefaultview != VIEW_CALENDAR)
2323            && (CC->room.QRdefaultview != VIEW_TASKS)
2324         ) {
2325                 return(0);              /* Not an iCalendar-centric room */
2326         }
2327
2328         /* It must be an RFC822 message! */
2329         if (msg->cm_format_type != 4) {
2330                 syslog(LOG_DEBUG, "calendar: rejecting non-RFC822 message");
2331                 return(1);              /* You tried to save a non-RFC822 message! */
2332         }
2333
2334         if (CM_IsEmpty(msg, eMesageText)) {
2335                 return(1);              /* You tried to save a null message! */
2336         }
2337
2338         /* Do all of our lovely back-end parsing */
2339         mime_parser(CM_RANGE(msg, eMesageText),
2340                     *ical_obj_beforesave_backend,
2341                     NULL, NULL,
2342                     (void *)msg,
2343                     0
2344                 );
2345
2346         return(0);
2347 }
2348
2349
2350 /*
2351  * Things we need to do after saving a calendar event.
2352  */
2353 void ical_obj_aftersave_backend(char *name, char *filename, char *partnum,
2354                 char *disp, void *content, char *cbtype, char *cbcharset, size_t length,
2355                 char *encoding, char *cbid, void *cbuserdata)
2356 {
2357         icalcomponent *cal;
2358
2359         /* We're only interested in calendar items here. */
2360         if (  (strcasecmp(cbtype, "text/calendar"))
2361            && (strcasecmp(cbtype, "application/ics")) ) {
2362                 return;
2363         }
2364
2365         /* Hunt for the UID and drop it in
2366          * the "user data" pointer for the MIME parser.  When
2367          * ical_obj_beforesave() sees it there, it'll set the Exclusive msgid
2368          * to that string.
2369          */
2370         if (  (!strcasecmp(cbtype, "text/calendar"))
2371            || (!strcasecmp(cbtype, "application/ics")) ) {
2372                 cal = icalcomponent_new_from_string(content);
2373                 if (cal != NULL) {
2374                         ical_saving_vevent(cal, cal);
2375                         icalcomponent_free(cal);
2376                 }
2377         }
2378 }
2379
2380
2381 /* 
2382  * Things we need to do after saving a calendar event.
2383  * (This will start back end tasks such as automatic generation of invitations,
2384  * if such actions are appropriate.)
2385  */
2386 int ical_obj_aftersave(struct CtdlMessage *msg, struct recptypes *recp)
2387 {
2388         char roomname[ROOMNAMELEN];
2389
2390         /*
2391          * If this isn't the Calendar> room, no further action is necessary.
2392          */
2393
2394         /* First determine if this is our room */
2395         CtdlMailboxName(roomname, sizeof roomname, &CC->user, USERCALENDARROOM);
2396         if (strcasecmp(roomname, CC->room.QRname)) {
2397                 return(0);      /* Not the Calendar room -- don't do anything. */
2398         }
2399
2400         // It must be an RFC822 message!
2401         if (msg->cm_format_type != 4) return(1);
2402
2403         // Reject null messages
2404         if (CM_IsEmpty(msg, eMesageText)) return(1);
2405         
2406         // Now recurse through it looking for our icalendar data
2407         mime_parser(CM_RANGE(msg, eMesageText),
2408                     *ical_obj_aftersave_backend,
2409                     NULL, NULL,
2410                     NULL,
2411                     0
2412                 );
2413
2414         return(0);
2415 }
2416
2417
2418 void ical_session_startup(void) {
2419         CIT_ICAL = malloc(sizeof(struct cit_ical));
2420         memset(CIT_ICAL, 0, sizeof(struct cit_ical));
2421 }
2422
2423
2424 void ical_session_shutdown(void) {
2425         free(CIT_ICAL);
2426 }
2427
2428
2429 // Back end for ical_fixed_output()
2430 void ical_fixed_output_backend(icalcomponent *cal, int recursion_level) {
2431         icalcomponent *c;
2432         icalproperty *p;
2433         char buf[256];
2434         const char *ch;
2435
2436         p = icalcomponent_get_first_property(cal, ICAL_SUMMARY_PROPERTY);
2437         if (p != NULL) {
2438                 cprintf("%s\n", (const char *)icalproperty_get_comment(p));
2439         }
2440
2441         p = icalcomponent_get_first_property(cal, ICAL_LOCATION_PROPERTY);
2442         if (p != NULL) {
2443                 cprintf("%s\n", (const char *)icalproperty_get_comment(p));
2444         }
2445
2446         p = icalcomponent_get_first_property(cal, ICAL_DESCRIPTION_PROPERTY);
2447         if (p != NULL) {
2448                 cprintf("%s\n", (const char *)icalproperty_get_comment(p));
2449         }
2450
2451         // If the component has attendees, iterate through them.
2452         for (p = icalcomponent_get_first_property(cal, ICAL_ATTENDEE_PROPERTY); (p != NULL); p = icalcomponent_get_next_property(cal, ICAL_ATTENDEE_PROPERTY)) {
2453                 ch =  icalproperty_get_attendee(p);
2454                 if ((ch != NULL) && 
2455                     !strncasecmp(ch, "MAILTO:", 7)) {
2456
2457                         // screen name or email address
2458                         safestrncpy(buf, ch + 7, sizeof(buf));
2459                         string_trim(buf);
2460                         cprintf("%s ", buf);
2461                 }
2462                 cprintf("\n");
2463         }
2464
2465         // If the component has subcomponents, recurse through them.
2466         for (c = icalcomponent_get_first_component(cal, ICAL_ANY_COMPONENT);
2467             (c != 0);
2468             c = icalcomponent_get_next_component(cal, ICAL_ANY_COMPONENT)) {
2469                 // Recursively process subcomponent 
2470                 ical_fixed_output_backend(c, recursion_level+1);
2471         }
2472 }
2473
2474
2475 // Function to output iCalendar data as plain text.  Nobody uses MSG0
2476 // anymore, so really this is just so we expose the vCard data to the full
2477 // text indexer.
2478 void ical_fixed_output(char *ptr, int len) {
2479         icalcomponent *cal;
2480         char *stringy_cal;
2481
2482         stringy_cal = malloc(len + 1);
2483         safestrncpy(stringy_cal, ptr, len + 1);
2484         cal = icalcomponent_new_from_string(stringy_cal);
2485         free(stringy_cal);
2486
2487         if (cal == NULL) {
2488                 return;
2489         }
2490
2491         ical_fixed_output_backend(cal, 0);
2492
2493         // Free the memory we obtained from libical's constructor
2494         icalcomponent_free(cal);
2495 }
2496
2497
2498 // Initialization function, called from modules_init.c
2499 char *ctdl_module_init_calendar(void) {
2500         if (!threading) {
2501
2502                 // Tell libical to return errors instead of aborting if it gets bad data.
2503                 // If this library call is not found, you need to upgrade libical.
2504                 icalerror_set_errors_are_fatal(0);
2505
2506                 // Use our own application prefix in tzid's generated from system tzdata
2507                 icaltimezone_set_tzid_prefix("/citadel.org/");
2508
2509                 // Initialize our hook functions
2510                 CtdlRegisterMessageHook(ical_obj_beforesave, EVT_BEFORESAVE);
2511                 CtdlRegisterMessageHook(ical_obj_aftersave, EVT_AFTERSAVE);
2512                 CtdlRegisterSessionHook(ical_CtdlCreateRoom, EVT_LOGIN, PRIO_LOGIN + 1);
2513                 CtdlRegisterProtoHook(cmd_ical, "ICAL", "Citadel iCalendar commands");
2514                 CtdlRegisterSessionHook(ical_session_startup, EVT_START, PRIO_START + 1);
2515                 CtdlRegisterSessionHook(ical_session_shutdown, EVT_STOP, PRIO_STOP + 80);
2516                 CtdlRegisterFixedOutputHook("text/calendar", ical_fixed_output);
2517                 CtdlRegisterFixedOutputHook("application/ics", ical_fixed_output);
2518         }
2519
2520         // return our module name for the log
2521         return "calendar";
2522 }