]> code.citadel.org Git - citadel.git/blob - citadel/serv_calendar.c
* Reply to VEVENT invitations: generate reply by cloning the request,
[citadel.git] / citadel / serv_calendar.c
1 /* 
2  * $Id$ 
3  *
4  * This module implements iCalendar object processing and the Calendar>
5  * room on a Citadel/UX server.  It handles iCalendar objects using the
6  * iTIP protocol.  See RFCs 2445 and 2446.
7  *
8  */
9
10 #include "sysdep.h"
11 #include <unistd.h>
12 #include <sys/types.h>
13 #include <limits.h>
14 #include <stdio.h>
15 #include <string.h>
16 #ifdef HAVE_STRINGS_H
17 #include <strings.h>
18 #endif
19 #include "serv_calendar.h"
20 #include "citadel.h"
21 #include "server.h"
22 #include "citserver.h"
23 #include "sysdep_decls.h"
24 #include "support.h"
25 #include "config.h"
26 #include "dynloader.h"
27 #include "user_ops.h"
28 #include "room_ops.h"
29 #include "tools.h"
30 #include "msgbase.h"
31 #include "mime_parser.h"
32
33
34 #ifdef HAVE_ICAL_H
35
36 #include <ical.h>
37
38 struct ical_respond_data {
39         char desired_partnum[SIZ];
40         icalcomponent *cal;
41 };
42
43
44 /*
45  * Write a calendar object into the specified user's calendar room.
46  */
47 void ical_write_to_cal(struct usersupp *u, icalcomponent *cal) {
48         char temp[PATH_MAX];
49         FILE *fp;
50         char *ser;
51
52         strcpy(temp, tmpnam(NULL));
53         ser = icalcomponent_as_ical_string(cal);
54         if (ser == NULL) return;
55
56         /* Make a temp file out of it */
57         fp = fopen(temp, "w");
58         if (fp == NULL) return;
59         fwrite(ser, strlen(ser), 1, fp);
60         fclose(fp);
61
62         /* This handy API function does all the work for us.
63          */
64         CtdlWriteObject(USERCALENDARROOM,       /* which room */
65                         "text/calendar",        /* MIME type */
66                         temp,                   /* temp file */
67                         u,                      /* which user */
68                         0,                      /* not binary */
69                         0,              /* don't delete others of this type */
70                         0);                     /* no flags */
71
72         unlink(temp);
73 }
74
75
76 /*
77  * Add a calendar object to the user's calendar
78  */
79 void ical_add(icalcomponent *cal, int recursion_level) {
80         icalcomponent *c;
81
82         /*
83          * The VEVENT subcomponent is the one we're interested in saving.
84          */
85         if (icalcomponent_isa(cal) == ICAL_VEVENT_COMPONENT) {
86         
87                 ical_write_to_cal(&CC->usersupp, cal);
88
89         }
90
91         /* If the component has subcomponents, recurse through them. */
92         for (c = icalcomponent_get_first_component(cal, ICAL_ANY_COMPONENT);
93             (c != 0);
94             c = icalcomponent_get_next_component(cal, ICAL_ANY_COMPONENT)) {
95                 /* Recursively process subcomponent */
96                 ical_add(c, recursion_level+1);
97         }
98
99 }
100
101
102
103 /*
104  * Send a reply to a meeting invitation.
105  *
106  * 'request' is the invitation to reply to.
107  * 'action' is the string "accept" or "decline".
108  */
109 void ical_send_a_reply(icalcomponent *request, char *action) {
110         icalcomponent *the_reply = NULL;
111         icalcomponent *vevent = NULL;
112         icalproperty *attendee = NULL;
113         char attendee_string[SIZ];
114         icalproperty *organizer = NULL;
115         char organizer_string[SIZ];
116         icalproperty *me_attend = NULL;
117         struct recptypes *recp = NULL;
118         icalparameter *partstat = NULL;
119
120         strcpy(organizer_string, "");
121
122         if (request == NULL) {
123                 lprintf(3, "ERROR: trying to reply to NULL event?\n");
124                 return;
125         }
126
127         the_reply = icalcomponent_new_clone(request);
128         if (the_reply == NULL) {
129                 lprintf(3, "ERROR: cannot clone request\n");
130                 return;
131         }
132
133         /* Change the method from REQUEST to REPLY */
134         icalcomponent_set_method(the_reply, ICAL_METHOD_REPLY);
135
136         vevent = icalcomponent_get_first_component(the_reply,
137                                                         ICAL_VEVENT_COMPONENT);
138         if (vevent != NULL) {
139                 /* Hunt for attendees, removing ones that aren't us.
140                  * (Actually, remove them all, cloning our own one so we can
141                  * re-insert it later)
142                  */
143                 while (attendee = icalcomponent_get_first_property(vevent,
144                     ICAL_ATTENDEE_PROPERTY), (attendee != NULL)
145                 ) {
146                         if (icalproperty_get_attendee(attendee)) {
147                                 strcpy(attendee_string,
148                                         icalproperty_get_attendee(attendee) );
149                                 if (!strncasecmp(attendee_string, "MAILTO:",
150                                    7)) {
151                                         strcpy(attendee_string, &attendee_string[7]);
152                                         striplt(attendee_string);
153                                         recp = validate_recipients(attendee_string);
154                                         if (recp != NULL) {
155                                                 if (!strcasecmp(recp->recp_local, CC->usersupp.fullname)) {
156                                                         if (me_attend) icalproperty_free(me_attend);
157                                                         me_attend = icalproperty_new_clone(attendee);
158                                                 }
159                                                 phree(recp);
160                                         }
161                                 }
162                         }
163                         /* Remove it... */
164                         icalcomponent_remove_property(vevent, attendee);
165                 }
166
167                 /* We found our own address in the attendee list. */
168                 if (me_attend) {
169                         /* Change the partstat from NEEDS-ACTION to ACCEPT or DECLINE */
170                         icalproperty_remove_parameter(me_attend, ICAL_PARTSTAT_PARAMETER);
171
172                         if (!strcasecmp(action, "accept")) {
173                                 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_ACCEPTED);
174                         }
175                         else if (!strcasecmp(action, "decline")) {
176                                 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_DECLINED);
177                         }
178                         else if (!strcasecmp(action, "tentative")) {
179                                 partstat = icalparameter_new_partstat(ICAL_PARTSTAT_TENTATIVE);
180                         }
181
182                         if (partstat) icalproperty_add_parameter(me_attend, partstat);
183
184                         /* Now insert it back into the vevent. */
185                         icalcomponent_add_property(vevent, me_attend);
186                 }
187
188                 /* Figure out who to send this thing to */
189                 organizer = icalcomponent_get_first_property(vevent, ICAL_ORGANIZER_PROPERTY);
190                 if (organizer != NULL) {
191                         if (icalproperty_get_organizer(organizer)) {
192                                 strcpy(organizer_string,
193                                         icalproperty_get_organizer(organizer) );
194                         }
195                 }
196                 if (!strncasecmp(organizer, "MAILTO:", 7)) {
197                         strcpy(organizer_string, &organizer_string[7]);
198                         striplt(organizer_string);
199                 } else {
200                         strcpy(organizer_string, "");
201                 }
202         }
203
204         /********* FIXME **********  
205         All we have to do now is send the reply.  Generate it with:
206         icalcomponent_as_ical_string(the_reply)
207         ...and send it to 'organizer_string'
208         (I'm just too tired to do it now)
209          **********************************/
210
211         /* clean up */
212         icalcomponent_free(the_reply);
213 }
214
215
216
217 /*
218  * Callback function for mime parser that hunts for calendar content types
219  * and turns them into calendar objects
220  */
221 void ical_locate_part(char *name, char *filename, char *partnum, char *disp,
222                 void *content, char *cbtype, size_t length, char *encoding,
223                 void *cbuserdata) {
224
225         struct ical_respond_data *ird = NULL;
226
227         ird = (struct ical_respond_data *) cbuserdata;
228         if (ird->cal != NULL) {
229                 icalcomponent_free(ird->cal);
230                 ird->cal = NULL;
231         }
232         if (strcasecmp(partnum, ird->desired_partnum)) return;
233         ird->cal = icalcomponent_new_from_string(content);
234 }
235
236
237 /*
238  * Respond to a meeting request.
239  */
240 void ical_respond(long msgnum, char *partnum, char *action) {
241         struct CtdlMessage *msg;
242         struct ical_respond_data ird;
243
244         if (
245            (strcasecmp(action, "accept"))
246            && (strcasecmp(action, "decline"))
247         ) {
248                 cprintf("%d Action must be 'accept' or 'decline'\n",
249                         ERROR + ILLEGAL_VALUE
250                 );
251                 return;
252         }
253
254         msg = CtdlFetchMessage(msgnum);
255         if (msg == NULL) {
256                 cprintf("%d Message %ld not found.\n",
257                         ERROR+ILLEGAL_VALUE,
258                         (long)msgnum
259                 );
260                 return;
261         }
262
263         memset(&ird, 0, sizeof ird);
264         strcpy(ird.desired_partnum, partnum);
265         mime_parser(msg->cm_fields['M'],
266                 NULL,
267                 *ical_locate_part,              /* callback function */
268                 NULL, NULL,
269                 (void *) &ird,                  /* user data */
270                 0
271         );
272
273         /* We're done with the incoming message, because we now have a
274          * calendar object in memory.
275          */
276         CtdlFreeMessage(msg);
277
278         /*
279          * Here is the real meat of this function.  Handle the event.
280          */
281         if (ird.cal != NULL) {
282                 /* Save this in the user's calendar if necessary */
283                 if (!strcasecmp(action, "accept")) {
284                         ical_add(ird.cal, 0);
285                 }
286
287                 /* Send a reply if necessary */
288                 if (icalcomponent_get_method(ird.cal) == ICAL_METHOD_REQUEST) {
289                         ical_send_a_reply(ird.cal, action);
290                 }
291
292                 /* Delete the message from the inbox */
293                 /* FIXME ... do this */
294
295                 /* Free the memory we allocated and return a response. */
296                 icalcomponent_free(ird.cal);
297                 ird.cal = NULL;
298                 cprintf("%d ok\n", CIT_OK);
299                 return;
300         }
301         else {
302                 cprintf("%d No calendar object found\n", ERROR);
303                 return;
304         }
305
306         /* should never get here */
307 }
308
309
310 /*
311  * Search for a property in both the top level and in a VEVENT subcomponent
312  */
313 icalproperty *ical_ctdl_get_subprop(
314                 icalcomponent *cal,
315                 icalproperty_kind which_prop
316 ) {
317         icalproperty *p;
318         icalcomponent *c;
319
320         p = icalcomponent_get_first_property(cal, which_prop);
321         if (p == NULL) {
322                 c = icalcomponent_get_first_component(cal,
323                                                         ICAL_VEVENT_COMPONENT);
324                 if (c != NULL) {
325                         p = icalcomponent_get_first_property(c, which_prop);
326                 }
327         }
328         return p;
329 }
330
331
332 /*
333  * Check to see if two events overlap.  Returns nonzero if they do.
334  */
335 int ical_ctdl_is_overlap(
336                         struct icaltimetype t1start,
337                         struct icaltimetype t1end,
338                         struct icaltimetype t2start,
339                         struct icaltimetype t2end
340 ) {
341
342         if (icaltime_is_null_time(t1start)) return(0);
343         if (icaltime_is_null_time(t2start)) return(0);
344
345         /* First, check for all-day events */
346         if (t1start.is_date) {
347                 if (!icaltime_compare_date_only(t1start, t2start)) {
348                         return(1);
349                 }
350                 if (!icaltime_is_null_time(t2end)) {
351                         if (!icaltime_compare_date_only(t1start, t2end)) {
352                                 return(1);
353                         }
354                 }
355         }
356
357         if (t2start.is_date) {
358                 if (!icaltime_compare_date_only(t2start, t1start)) {
359                         return(1);
360                 }
361                 if (!icaltime_is_null_time(t1end)) {
362                         if (!icaltime_compare_date_only(t2start, t1end)) {
363                                 return(1);
364                         }
365                 }
366         }
367
368         /* Now check for overlaps using date *and* time. */
369
370         /* First, bail out if either event 1 or event 2 is missing end time. */
371         if (icaltime_is_null_time(t1end)) return(0);
372         if (icaltime_is_null_time(t2end)) return(0);
373
374         /* If event 1 ends before event 2 starts, we're in the clear. */
375         if (icaltime_compare(t1end, t2start) <= 0) return(0);
376
377         /* If event 2 ends before event 1 starts, we're also ok. */
378         if (icaltime_compare(t2end, t1start) <= 0) return(0);
379
380         /* Otherwise, they overlap. */
381         return(1);
382 }
383
384
385
386 /*
387  * Backend for ical_hunt_for_conflicts()
388  */
389 void ical_hunt_for_conflicts_backend(long msgnum, void *data) {
390         icalcomponent *cal;
391         struct CtdlMessage *msg;
392         struct ical_respond_data ird;
393         struct icaltimetype t1start, t1end, t2start, t2end;
394         icalproperty *p;
395         char conflict_event_uid[SIZ];
396         char conflict_event_summary[SIZ];
397         char compare_uid[SIZ];
398
399         cal = (icalcomponent *)data;
400         strcpy(compare_uid, "");
401         strcpy(conflict_event_uid, "");
402         strcpy(conflict_event_summary, "");
403
404         msg = CtdlFetchMessage(msgnum);
405         if (msg == NULL) return;
406         memset(&ird, 0, sizeof ird);
407         strcpy(ird.desired_partnum, "1");       /* hopefully it's always 1 */
408         mime_parser(msg->cm_fields['M'],
409                 NULL,
410                 *ical_locate_part,              /* callback function */
411                 NULL, NULL,
412                 (void *) &ird,                  /* user data */
413                 0
414         );
415         CtdlFreeMessage(msg);
416
417         if (ird.cal == NULL) return;
418
419         t1start = icaltime_null_time();
420         t1end = icaltime_null_time();
421         t2start = icaltime_null_time();
422         t1end = icaltime_null_time();
423
424         /* Now compare cal to ird.cal */
425         p = ical_ctdl_get_subprop(ird.cal, ICAL_DTSTART_PROPERTY);
426         if (p == NULL) return;
427         if (p != NULL) t2start = icalproperty_get_dtstart(p);
428         
429         p = ical_ctdl_get_subprop(ird.cal, ICAL_DTEND_PROPERTY);
430         if (p != NULL) t2end = icalproperty_get_dtend(p);
431
432         p = ical_ctdl_get_subprop(cal, ICAL_DTSTART_PROPERTY);
433         if (p == NULL) return;
434         if (p != NULL) t1start = icalproperty_get_dtstart(p);
435         
436         p = ical_ctdl_get_subprop(cal, ICAL_DTEND_PROPERTY);
437         if (p != NULL) t1end = icalproperty_get_dtend(p);
438         
439         p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
440         if (p != NULL) {
441                 strcpy(compare_uid, icalproperty_get_comment(p));
442         }
443
444         p = ical_ctdl_get_subprop(ird.cal, ICAL_UID_PROPERTY);
445         if (p != NULL) {
446                 strcpy(conflict_event_uid, icalproperty_get_comment(p));
447         }
448
449         p = ical_ctdl_get_subprop(ird.cal, ICAL_SUMMARY_PROPERTY);
450         if (p != NULL) {
451                 strcpy(conflict_event_summary, icalproperty_get_comment(p));
452         }
453
454
455         icalcomponent_free(ird.cal);
456
457         if (ical_ctdl_is_overlap(t1start, t1end, t2start, t2end)) {
458                 cprintf("%ld||%s|%s|%d|\n",
459                         msgnum,
460                         conflict_event_uid,
461                         conflict_event_summary,
462                         (       ((strlen(compare_uid)>0)
463                                 &&(!strcasecmp(compare_uid,
464                                 conflict_event_uid))) ? 1 : 0
465                         )
466                 );
467         }
468 }
469
470
471
472 /* 
473  * Phase 2 of "hunt for conflicts" operation.
474  * At this point we have a calendar object which represents the VEVENT that
475  * we're considering adding to the calendar.  Now hunt through the user's
476  * calendar room, and output zero or more existing VEVENTs which conflict
477  * with this one.
478  */
479 void ical_hunt_for_conflicts(icalcomponent *cal) {
480         char hold_rm[ROOMNAMELEN];
481
482         strcpy(hold_rm, CC->quickroom.QRname);  /* save current room */
483
484         if (getroom(&CC->quickroom, USERCALENDARROOM) != 0) {
485                 getroom(&CC->quickroom, hold_rm);
486                 cprintf("%d You do not have a calendar.\n", ERROR);
487                 return;
488         }
489
490         cprintf("%d Conflicting events:\n", LISTING_FOLLOWS);
491
492         CtdlForEachMessage(MSGS_ALL, 0, "text/calendar",
493                 NULL,
494                 ical_hunt_for_conflicts_backend,
495                 (void *) cal
496         );
497
498         cprintf("000\n");
499         getroom(&CC->quickroom, hold_rm);       /* return to saved room */
500
501 }
502
503
504
505 /*
506  * Hunt for conflicts (Phase 1 -- retrieve the object and call Phase 2)
507  */
508 void ical_conflicts(long msgnum, char *partnum) {
509         struct CtdlMessage *msg;
510         struct ical_respond_data ird;
511
512         msg = CtdlFetchMessage(msgnum);
513         if (msg == NULL) {
514                 cprintf("%d Message %ld not found.\n",
515                         ERROR+ILLEGAL_VALUE,
516                         (long)msgnum
517                 );
518                 return;
519         }
520
521         memset(&ird, 0, sizeof ird);
522         strcpy(ird.desired_partnum, partnum);
523         mime_parser(msg->cm_fields['M'],
524                 NULL,
525                 *ical_locate_part,              /* callback function */
526                 NULL, NULL,
527                 (void *) &ird,                  /* user data */
528                 0
529         );
530
531         CtdlFreeMessage(msg);
532
533         if (ird.cal != NULL) {
534                 ical_hunt_for_conflicts(ird.cal);
535                 icalcomponent_free(ird.cal);
536                 return;
537         }
538         else {
539                 cprintf("%d No calendar object found\n", ERROR);
540                 return;
541         }
542
543         /* should never get here */
544 }
545
546
547
548
549 /*
550  * All Citadel calendar commands from the client come through here.
551  */
552 void cmd_ical(char *argbuf)
553 {
554         char subcmd[SIZ];
555         long msgnum;
556         char partnum[SIZ];
557         char action[SIZ];
558
559         if (CtdlAccessCheck(ac_logged_in)) return;
560
561         extract(subcmd, argbuf, 0);
562
563         if (!strcmp(subcmd, "test")) {
564                 cprintf("%d This server supports calendaring\n", CIT_OK);
565                 return;
566         }
567
568         else if (!strcmp(subcmd, "respond")) {
569                 msgnum = extract_long(argbuf, 1);
570                 extract(partnum, argbuf, 2);
571                 extract(action, argbuf, 3);
572                 ical_respond(msgnum, partnum, action);
573         }
574
575         else if (!strcmp(subcmd, "conflicts")) {
576                 msgnum = extract_long(argbuf, 1);
577                 extract(partnum, argbuf, 2);
578                 ical_conflicts(msgnum, partnum);
579         }
580
581         else {
582                 cprintf("%d Invalid subcommand\n", ERROR+CMD_NOT_SUPPORTED);
583                 return;
584         }
585
586         /* should never get here */
587 }
588
589
590
591 /*
592  * We don't know if the calendar room exists so we just create it at login
593  */
594 void ical_create_room(void)
595 {
596         struct quickroom qr;
597         struct visit vbuf;
598
599         /* Create the calendar room if it doesn't already exist */
600         create_room(USERCALENDARROOM, 4, "", 0, 1, 0);
601
602         /* Set expiration policy to manual; otherwise objects will be lost! */
603         if (lgetroom(&qr, USERCALENDARROOM)) {
604                 lprintf(3, "Couldn't get the user calendar room!\n");
605                 return;
606         }
607         qr.QRep.expire_mode = EXPIRE_MANUAL;
608         lputroom(&qr);
609
610         /* Set the view to a calendar view */
611         CtdlGetRelationship(&vbuf, &CC->usersupp, &qr);
612         vbuf.v_view = 3;        /* 3 = calendar */
613         CtdlSetRelationship(&vbuf, &CC->usersupp, &qr);
614
615         /* Create the tasks list room if it doesn't already exist */
616         create_room(USERTASKSROOM, 4, "", 0, 1, 0);
617
618         /* Set expiration policy to manual; otherwise objects will be lost! */
619         if (lgetroom(&qr, USERTASKSROOM)) {
620                 lprintf(3, "Couldn't get the user calendar room!\n");
621                 return;
622         }
623         qr.QRep.expire_mode = EXPIRE_MANUAL;
624         lputroom(&qr);
625
626         /* Set the view to a task list view */
627         CtdlGetRelationship(&vbuf, &CC->usersupp, &qr);
628         vbuf.v_view = 4;        /* 4 = tasks */
629         CtdlSetRelationship(&vbuf, &CC->usersupp, &qr);
630
631         return;
632 }
633
634
635
636 /*
637  * Back end for ical_obj_beforesave()
638  * This hunts for the UID of the calendar event.
639  */
640 void ical_ctdl_set_extended_msgid(char *name, char *filename, char *partnum,
641                 char *disp, void *content, char *cbtype, size_t length,
642                 char *encoding, void *cbuserdata)
643 {
644         icalcomponent *cal;
645         icalproperty *p;
646
647         /* If this is a text/calendar object, hunt for the UID and drop it in
648          * the "user data" pointer for the MIME parser.  When
649          * ical_obj_beforesave() sees it there, it'll set the Extended msgid
650          * to that string.
651          */
652         if (!strcasecmp(cbtype, "text/calendar")) {
653                 cal = icalcomponent_new_from_string(content);
654                 if (cal != NULL) {
655                         p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
656                         if (p != NULL) {
657                                 strcpy((char *)cbuserdata,
658                                         icalproperty_get_comment(p)
659                                 );
660                         }
661                         icalcomponent_free(cal);
662                 }
663         }
664 }
665
666
667
668
669
670 /*
671  * See if we need to prevent the object from being saved (we don't allow
672  * MIME types other than text/calendar in the Calendar> room).  Also, when
673  * saving an event to the calendar, set the message's Citadel extended message
674  * ID to the UID of the object.  This causes our replication checker to
675  * automatically delete any existing instances of the same object.  (Isn't
676  * that cool?)
677  */
678 int ical_obj_beforesave(struct CtdlMessage *msg)
679 {
680         char roomname[ROOMNAMELEN];
681         char *p;
682         int a;
683         char eidbuf[SIZ];
684
685         /*
686          * Only messages with content-type text/calendar
687          * may be saved to Calendar>.  If the message is bound for
688          * Calendar> but doesn't have this content-type, throw an error
689          * so that the message may not be posted.
690          */
691
692         /* First determine if this is our room */
693         MailboxName(roomname, sizeof roomname, &CC->usersupp, USERCALENDARROOM);
694         if (strcasecmp(roomname, CC->quickroom.QRname)) {
695                 return 0;       /* It's not the Calendar room. */
696         }
697
698         /* Then determine content-type of the message */
699         
700         /* It must be an RFC822 message! */
701         /* FIXME: Not handling MIME multipart messages; implement with IMIP */
702         if (msg->cm_format_type != 4)
703                 return 1;       /* You tried to save a non-RFC822 message! */
704         
705         /* Find the Content-Type: header */
706         p = msg->cm_fields['M'];
707         a = strlen(p);
708         while (--a > 0) {
709                 if (!strncasecmp(p, "Content-Type: ", 14)) {    /* Found it */
710                         if (!strncasecmp(p + 14, "text/calendar", 13)) {
711                                 strcpy(eidbuf, "");
712                                 mime_parser(msg->cm_fields['M'],
713                                         NULL,
714                                         *ical_ctdl_set_extended_msgid,
715                                         NULL, NULL,
716                                         (void *)eidbuf,
717                                         0
718                                 );
719                                 if (strlen(eidbuf) > 0) {
720                                         if (msg->cm_fields['E'] != NULL) {
721                                                 phree(msg->cm_fields['E']);
722                                         }
723                                         msg->cm_fields['E'] = strdoop(eidbuf);
724                                 }
725                                 return 0;
726                         }
727                         else {
728                                 return 1;
729                         }
730                 }
731                 p++;
732         }
733         
734         /* Oops!  No Content-Type in this message!  How'd that happen? */
735         lprintf(7, "RFC822 message with no Content-Type header!\n");
736         return 1;
737 }
738
739
740 #endif  /* HAVE_ICAL_H */
741
742 /*
743  * Register this module with the Citadel server.
744  */
745 char *Dynamic_Module_Init(void)
746 {
747 #ifdef HAVE_ICAL_H
748         CtdlRegisterMessageHook(ical_obj_beforesave, EVT_BEFORESAVE);
749         CtdlRegisterSessionHook(ical_create_room, EVT_LOGIN);
750         CtdlRegisterProtoHook(cmd_ical, "ICAL", "Citadel iCal commands");
751 #endif
752         return "$Id$";
753 }