]> code.citadel.org Git - citadel.git/blob - citadel/serv_calendar.c
* Finished: when saving an object of type text/calendar to the Calendar> room,
[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 our config to disk
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  * Callback function for mime parser that hunts for calendar content types
105  * and turns them into calendar objects
106  */
107 void ical_locate_part(char *name, char *filename, char *partnum, char *disp,
108                 void *content, char *cbtype, size_t length, char *encoding,
109                 void *cbuserdata) {
110
111         struct ical_respond_data *ird = NULL;
112
113         ird = (struct ical_respond_data *) cbuserdata;
114         if (ird->cal != NULL) {
115                 icalcomponent_free(ird->cal);
116                 ird->cal = NULL;
117         }
118         if (strcasecmp(partnum, ird->desired_partnum)) return;
119         ird->cal = icalcomponent_new_from_string(content);
120 }
121
122
123 /*
124  * Respond to a meeting request.
125  */
126 void ical_respond(long msgnum, char *partnum, char *action) {
127         struct CtdlMessage *msg;
128         struct ical_respond_data ird;
129
130         if (
131            (strcasecmp(action, "accept"))
132            && (strcasecmp(action, "decline"))
133         ) {
134                 cprintf("%d Action must be 'accept' or 'decline'\n",
135                         ERROR + ILLEGAL_VALUE
136                 );
137                 return;
138         }
139
140         msg = CtdlFetchMessage(msgnum);
141         if (msg == NULL) {
142                 cprintf("%d Message %ld not found.\n",
143                         ERROR+ILLEGAL_VALUE,
144                         (long)msgnum
145                 );
146                 return;
147         }
148
149         memset(&ird, 0, sizeof ird);
150         strcpy(ird.desired_partnum, partnum);
151         mime_parser(msg->cm_fields['M'],
152                 NULL,
153                 *ical_locate_part,              /* callback function */
154                 NULL, NULL,
155                 (void *) &ird,                  /* user data */
156                 0
157         );
158
159         CtdlFreeMessage(msg);
160
161         if (ird.cal != NULL) {
162                 /* Save this in the user's calendar if necessary */
163                 if (!strcasecmp(action, "accept")) {
164                         ical_add(ird.cal, 0);
165                 }
166
167                 /* Send a reply if necessary */
168                 /* FIXME ... do this */
169
170                 /* Delete the message from the inbox */
171                 /* FIXME ... do this */
172
173                 icalcomponent_free(ird.cal);
174                 ird.cal = NULL;
175                 cprintf("%d ok\n", CIT_OK);
176                 return;
177         }
178         else {
179                 cprintf("%d No calendar object found\n", ERROR);
180                 return;
181         }
182
183         /* should never get here */
184 }
185
186
187 /*
188  * Search for a property in both the top level and in a VEVENT subcomponent
189  */
190 icalproperty *ical_ctdl_get_subprop(
191                 icalcomponent *cal,
192                 icalproperty_kind which_prop
193 ) {
194         icalproperty *p;
195         icalcomponent *c;
196
197         p = icalcomponent_get_first_property(cal, which_prop);
198         if (p == NULL) {
199                 c = icalcomponent_get_first_component(cal,
200                                                         ICAL_VEVENT_COMPONENT);
201                 if (c != NULL) {
202                         p = icalcomponent_get_first_property(c, which_prop);
203                 }
204         }
205         return p;
206 }
207
208
209 /*
210  * Check to see if two events overlap.  Returns nonzero if they do.
211  */
212 int ical_ctdl_is_overlap(
213                         struct icaltimetype t1start,
214                         struct icaltimetype t1end,
215                         struct icaltimetype t2start,
216                         struct icaltimetype t2end
217 ) {
218
219         if (icaltime_is_null_time(t1start)) return(0);
220         if (icaltime_is_null_time(t2start)) return(0);
221
222         /* First, check for all-day events */
223         if (t1start.is_date) {
224                 if (!icaltime_compare_date_only(t1start, t2start)) {
225                         return(1);
226                 }
227                 if (!icaltime_is_null_time(t2end)) {
228                         if (!icaltime_compare_date_only(t1start, t2end)) {
229                                 return(1);
230                         }
231                 }
232         }
233
234         if (t2start.is_date) {
235                 if (!icaltime_compare_date_only(t2start, t1start)) {
236                         return(1);
237                 }
238                 if (!icaltime_is_null_time(t1end)) {
239                         if (!icaltime_compare_date_only(t2start, t1end)) {
240                                 return(1);
241                         }
242                 }
243         }
244
245         /* Now check for overlaps using date *and* time. */
246
247         /* First, bail out if either event 1 or event 2 is missing end time. */
248         if (icaltime_is_null_time(t1end)) return(0);
249         if (icaltime_is_null_time(t2end)) return(0);
250
251         /* If event 1 ends before event 2 starts, we're in the clear. */
252         if (icaltime_compare(t1end, t2start) <= 0) return(0);
253
254         /* If event 2 ends before event 1 starts, we're also ok. */
255         if (icaltime_compare(t2end, t1start) <= 0) return(0);
256
257         /* Otherwise, they overlap. */
258         return(1);
259 }
260
261
262
263 /*
264  * Backend for ical_hunt_for_conflicts()
265  */
266 void ical_hunt_for_conflicts_backend(long msgnum, void *data) {
267         icalcomponent *cal;
268         struct CtdlMessage *msg;
269         struct ical_respond_data ird;
270         struct icaltimetype t1start, t1end, t2start, t2end;
271         icalproperty *p;
272         char conflict_event_uid[SIZ];
273         char conflict_event_summary[SIZ];
274
275         cal = (icalcomponent *)data;
276         strcpy(conflict_event_uid, "");
277         strcpy(conflict_event_summary, "");
278
279         msg = CtdlFetchMessage(msgnum);
280         if (msg == NULL) return;
281         memset(&ird, 0, sizeof ird);
282         strcpy(ird.desired_partnum, "1");       /* hopefully it's always 1 */
283         mime_parser(msg->cm_fields['M'],
284                 NULL,
285                 *ical_locate_part,              /* callback function */
286                 NULL, NULL,
287                 (void *) &ird,                  /* user data */
288                 0
289         );
290         CtdlFreeMessage(msg);
291
292         if (ird.cal == NULL) return;
293
294         t1start = icaltime_null_time();
295         t1end = icaltime_null_time();
296         t2start = icaltime_null_time();
297         t1end = icaltime_null_time();
298
299         /* Now compare cal to ird.cal */
300         p = ical_ctdl_get_subprop(ird.cal, ICAL_DTSTART_PROPERTY);
301         if (p == NULL) return;
302         if (p != NULL) t2start = icalproperty_get_dtstart(p);
303         
304         p = ical_ctdl_get_subprop(ird.cal, ICAL_DTEND_PROPERTY);
305         if (p != NULL) t2end = icalproperty_get_dtend(p);
306
307         p = ical_ctdl_get_subprop(cal, ICAL_DTSTART_PROPERTY);
308         if (p == NULL) return;
309         if (p != NULL) t1start = icalproperty_get_dtstart(p);
310         
311         p = ical_ctdl_get_subprop(cal, ICAL_DTEND_PROPERTY);
312         if (p != NULL) t1end = icalproperty_get_dtend(p);
313         
314         p = ical_ctdl_get_subprop(ird.cal, ICAL_UID_PROPERTY);
315         if (p != NULL) {
316                 strcpy(conflict_event_uid, icalproperty_get_comment(p));
317         }
318         p = ical_ctdl_get_subprop(ird.cal, ICAL_SUMMARY_PROPERTY);
319         if (p != NULL) {
320                 strcpy(conflict_event_summary, icalproperty_get_comment(p));
321         }
322
323
324         icalcomponent_free(ird.cal);
325
326         if (ical_ctdl_is_overlap(t1start, t1end, t2start, t2end)) {
327                 cprintf("%ld||%s|%s|\n",
328                         msgnum,
329                         conflict_event_uid,
330                         conflict_event_summary
331                 );
332         }
333 }
334
335
336
337 /* 
338  * Phase 2 of "hunt for conflicts" operation.
339  * At this point we have a calendar object which represents the VEVENT that
340  * we're considering adding to the calendar.  Now hunt through the user's
341  * calendar room, and output zero or more existing VEVENTs which conflict
342  * with this one.
343  */
344 void ical_hunt_for_conflicts(icalcomponent *cal) {
345         char hold_rm[ROOMNAMELEN];
346
347         strcpy(hold_rm, CC->quickroom.QRname);  /* save current room */
348
349         if (getroom(&CC->quickroom, USERCALENDARROOM) != 0) {
350                 getroom(&CC->quickroom, hold_rm);
351                 cprintf("%d You do not have a calendar.\n", ERROR);
352                 return;
353         }
354
355         cprintf("%d Conflicting events:\n", LISTING_FOLLOWS);
356
357         CtdlForEachMessage(MSGS_ALL, 0, "text/calendar",
358                 NULL,
359                 ical_hunt_for_conflicts_backend,
360                 (void *) cal
361         );
362
363         cprintf("000\n");
364         getroom(&CC->quickroom, hold_rm);       /* return to saved room */
365
366 }
367
368
369
370 /*
371  * Hunt for conflicts (Phase 1 -- retrieve the object and call Phase 2)
372  */
373 void ical_conflicts(long msgnum, char *partnum) {
374         struct CtdlMessage *msg;
375         struct ical_respond_data ird;
376
377         msg = CtdlFetchMessage(msgnum);
378         if (msg == NULL) {
379                 cprintf("%d Message %ld not found.\n",
380                         ERROR+ILLEGAL_VALUE,
381                         (long)msgnum
382                 );
383                 return;
384         }
385
386         memset(&ird, 0, sizeof ird);
387         strcpy(ird.desired_partnum, partnum);
388         mime_parser(msg->cm_fields['M'],
389                 NULL,
390                 *ical_locate_part,              /* callback function */
391                 NULL, NULL,
392                 (void *) &ird,                  /* user data */
393                 0
394         );
395
396         CtdlFreeMessage(msg);
397
398         if (ird.cal != NULL) {
399                 ical_hunt_for_conflicts(ird.cal);
400                 icalcomponent_free(ird.cal);
401                 return;
402         }
403         else {
404                 cprintf("%d No calendar object found\n", ERROR);
405                 return;
406         }
407
408         /* should never get here */
409 }
410
411
412
413
414 /*
415  * All Citadel calendar commands from the client come through here.
416  */
417 void cmd_ical(char *argbuf)
418 {
419         char subcmd[SIZ];
420         long msgnum;
421         char partnum[SIZ];
422         char action[SIZ];
423
424         if (CtdlAccessCheck(ac_logged_in)) return;
425
426         extract(subcmd, argbuf, 0);
427
428         if (!strcmp(subcmd, "test")) {
429                 cprintf("%d This server supports calendaring\n", CIT_OK);
430                 return;
431         }
432
433         else if (!strcmp(subcmd, "respond")) {
434                 msgnum = extract_long(argbuf, 1);
435                 extract(partnum, argbuf, 2);
436                 extract(action, argbuf, 3);
437                 ical_respond(msgnum, partnum, action);
438         }
439
440         else if (!strcmp(subcmd, "conflicts")) {
441                 msgnum = extract_long(argbuf, 1);
442                 extract(partnum, argbuf, 2);
443                 ical_conflicts(msgnum, partnum);
444         }
445
446         else {
447                 cprintf("%d Invalid subcommand\n", ERROR+CMD_NOT_SUPPORTED);
448                 return;
449         }
450
451         /* should never get here */
452 }
453
454
455
456 /*
457  * We don't know if the calendar room exists so we just create it at login
458  */
459 void ical_create_room(void)
460 {
461         struct quickroom qr;
462         struct visit vbuf;
463
464         /* Create the calendar room if it doesn't already exist */
465         create_room(USERCALENDARROOM, 4, "", 0, 1, 0);
466
467         /* Set expiration policy to manual; otherwise objects will be lost! */
468         if (lgetroom(&qr, USERCALENDARROOM)) {
469                 lprintf(3, "Couldn't get the user calendar room!\n");
470                 return;
471         }
472         qr.QRep.expire_mode = EXPIRE_MANUAL;
473         lputroom(&qr);
474
475         /* Set the view to a calendar view */
476         CtdlGetRelationship(&vbuf, &CC->usersupp, &qr);
477         vbuf.v_view = 3;        /* 3 = calendar */
478         CtdlSetRelationship(&vbuf, &CC->usersupp, &qr);
479
480         /* Create the tasks list room if it doesn't already exist */
481         create_room(USERTASKSROOM, 4, "", 0, 1, 0);
482
483         /* Set expiration policy to manual; otherwise objects will be lost! */
484         if (lgetroom(&qr, USERTASKSROOM)) {
485                 lprintf(3, "Couldn't get the user calendar room!\n");
486                 return;
487         }
488         qr.QRep.expire_mode = EXPIRE_MANUAL;
489         lputroom(&qr);
490
491         /* Set the view to a task list view */
492         CtdlGetRelationship(&vbuf, &CC->usersupp, &qr);
493         vbuf.v_view = 4;        /* 4 = tasks */
494         CtdlSetRelationship(&vbuf, &CC->usersupp, &qr);
495
496         return;
497 }
498
499
500
501 /*
502  * Back end for ical_obj_beforesave()
503  * This hunts for the UID of the calendar event.
504  */
505 void ical_ctdl_set_extended_msgid(char *name, char *filename, char *partnum,
506                 char *disp, void *content, char *cbtype, size_t length,
507                 char *encoding, void *cbuserdata)
508 {
509         icalcomponent *cal;
510         icalproperty *p;
511
512         /* If this is a text/calendar object, hunt for the UID and drop it in
513          * the "user data" pointer for the MIME parser.  When
514          * ical_obj_beforesave() sees it there, it'll set the Extended msgid
515          * to that string.
516          */
517         if (!strcasecmp(cbtype, "text/calendar")) {
518                 cal = icalcomponent_new_from_string(content);
519                 if (cal != NULL) {
520                         p = ical_ctdl_get_subprop(cal, ICAL_UID_PROPERTY);
521                         if (p != NULL) {
522                                 strcpy((char *)cbuserdata,
523                                         icalproperty_get_comment(p)
524                                 );
525                         }
526                         icalcomponent_free(cal);
527                 }
528         }
529 }
530
531
532
533
534
535 /*
536  * See if we need to prevent the object from being saved (we don't allow
537  * MIME types other than text/calendar in the Calendar> room).  Also, when
538  * saving an event to the calendar, set the message's Citadel extended message
539  * ID to the UID of the object.  This causes our replication checker to
540  * automatically delete any existing instances of the same object.  (Isn't
541  * that cool?)
542  */
543 int ical_obj_beforesave(struct CtdlMessage *msg)
544 {
545         char roomname[ROOMNAMELEN];
546         char *p;
547         int a;
548         char eidbuf[SIZ];
549
550         /*
551          * Only messages with content-type text/calendar
552          * may be saved to Calendar>.  If the message is bound for
553          * Calendar> but doesn't have this content-type, throw an error
554          * so that the message may not be posted.
555          */
556
557         /* First determine if this is our room */
558         MailboxName(roomname, sizeof roomname, &CC->usersupp, USERCALENDARROOM);
559         if (strcasecmp(roomname, CC->quickroom.QRname)) {
560                 return 0;       /* It's not the Calendar room. */
561         }
562
563         /* Then determine content-type of the message */
564         
565         /* It must be an RFC822 message! */
566         /* FIXME: Not handling MIME multipart messages; implement with IMIP */
567         if (msg->cm_format_type != 4)
568                 return 1;       /* You tried to save a non-RFC822 message! */
569         
570         /* Find the Content-Type: header */
571         p = msg->cm_fields['M'];
572         a = strlen(p);
573         while (--a > 0) {
574                 if (!strncasecmp(p, "Content-Type: ", 14)) {    /* Found it */
575                         if (!strncasecmp(p + 14, "text/calendar", 13)) {
576                                 strcpy(eidbuf, "");
577                                 mime_parser(msg->cm_fields['M'],
578                                         NULL,
579                                         *ical_ctdl_set_extended_msgid,
580                                         NULL, NULL,
581                                         (void *)eidbuf,
582                                         0
583                                 );
584                                 if (strlen(eidbuf) > 0) {
585                                         if (msg->cm_fields['E'] != NULL) {
586                                                 phree(msg->cm_fields['E']);
587                                         }
588                                         msg->cm_fields['E'] = strdoop(eidbuf);
589                                 }
590                                 return 0;
591                         }
592                         else {
593                                 return 1;
594                         }
595                 }
596                 p++;
597         }
598         
599         /* Oops!  No Content-Type in this message!  How'd that happen? */
600         lprintf(7, "RFC822 message with no Content-Type header!\n");
601         return 1;
602 }
603
604
605 #endif  /* HAVE_ICAL_H */
606
607 /*
608  * Register this module with the Citadel server.
609  */
610 char *Dynamic_Module_Init(void)
611 {
612 #ifdef HAVE_ICAL_H
613         CtdlRegisterMessageHook(ical_obj_beforesave, EVT_BEFORESAVE);
614         CtdlRegisterSessionHook(ical_create_room, EVT_LOGIN);
615         CtdlRegisterProtoHook(cmd_ical, "ICAL", "Citadel iCal commands");
616 #endif
617         return "$Id$";
618 }