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