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