]> code.citadel.org Git - citadel.git/blob - citadel/serv_calendar.c
* More work on conflict detects
[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 #ifdef HAVE_ICAL_H
33 #include <ical.h>
34 #endif
35
36
37 #ifdef HAVE_ICAL_H
38
39 struct ical_respond_data {
40         char desired_partnum[SIZ];
41         icalcomponent *cal;
42 };
43
44
45
46
47
48 /*
49  * Write our config to disk
50  */
51 void ical_write_to_cal(struct usersupp *u, icalcomponent *cal) {
52         char temp[PATH_MAX];
53         FILE *fp;
54         char *ser;
55
56         strcpy(temp, tmpnam(NULL));
57         ser = icalcomponent_as_ical_string(cal);
58         if (ser == NULL) return;
59
60         /* Make a temp file out of it */
61         fp = fopen(temp, "w");
62         if (fp == NULL) return;
63         fwrite(ser, strlen(ser), 1, fp);
64         fclose(fp);
65
66         /* This handy API function does all the work for us.
67          * NOTE: normally we would want to set that last argument to 1, to
68          * force the system to delete the user's old vCard.  But it doesn't
69          * have to, because the vcard_upload_beforesave() hook above
70          * is going to notice what we're trying to do, and delete the old vCard.
71          */
72         CtdlWriteObject(USERCALENDARROOM,       /* which room */
73                         "text/calendar",        /* MIME type */
74                         temp,                   /* temp file */
75                         u,                      /* which user */
76                         0,                      /* not binary */
77                         0,              /* don't delete others of this type */
78                         0);                     /* no flags */
79
80         unlink(temp);
81 }
82
83
84 /*
85  * Add a calendar object to the user's calendar
86  */
87 void ical_add(icalcomponent *cal, int recursion_level) {
88         icalcomponent *c;
89
90         /*
91          * The VEVENT subcomponent is the one we're interested in saving.
92          */
93         if (icalcomponent_isa(cal) == ICAL_VEVENT_COMPONENT) {
94         
95                 ical_write_to_cal(&CC->usersupp, cal);
96
97         }
98
99         /* If the component has subcomponents, recurse through them. */
100         for (c = icalcomponent_get_first_component(cal, ICAL_ANY_COMPONENT);
101             (c != 0);
102             c = icalcomponent_get_next_component(cal, ICAL_ANY_COMPONENT)) {
103                 /* Recursively process subcomponent */
104                 ical_add(c, recursion_level+1);
105         }
106
107 }
108
109
110
111 /*
112  * Callback function for mime parser that hunts for calendar content types
113  * and turns them into calendar objects
114  */
115 void ical_locate_part(char *name, char *filename, char *partnum, char *disp,
116                 void *content, char *cbtype, size_t length, char *encoding,
117                 void *cbuserdata) {
118
119         struct ical_respond_data *ird = NULL;
120
121         ird = (struct ical_respond_data *) cbuserdata;
122         if (ird->cal != NULL) {
123                 icalcomponent_free(ird->cal);
124                 ird->cal = NULL;
125         }
126         if (strcasecmp(partnum, ird->desired_partnum)) return;
127         ird->cal = icalcomponent_new_from_string(content);
128 }
129
130
131 /*
132  * Respond to a meeting request.
133  */
134 void ical_respond(long msgnum, char *partnum, char *action) {
135         struct CtdlMessage *msg;
136         struct ical_respond_data ird;
137
138         if (
139            (strcasecmp(action, "accept"))
140            && (strcasecmp(action, "decline"))
141         ) {
142                 cprintf("%d Action must be 'accept' or 'decline'\n",
143                         ERROR + ILLEGAL_VALUE
144                 );
145                 return;
146         }
147
148         msg = CtdlFetchMessage(msgnum);
149         if (msg == NULL) {
150                 cprintf("%d Message %ld not found.\n",
151                         ERROR+ILLEGAL_VALUE,
152                         (long)msgnum
153                 );
154                 return;
155         }
156
157         memset(&ird, 0, sizeof ird);
158         strcpy(ird.desired_partnum, partnum);
159         mime_parser(msg->cm_fields['M'],
160                 NULL,
161                 *ical_locate_part,              /* callback function */
162                 NULL, NULL,
163                 (void *) &ird,                  /* user data */
164                 0
165         );
166
167         CtdlFreeMessage(msg);
168
169         if (ird.cal != NULL) {
170                 /* Save this in the user's calendar if necessary */
171                 if (!strcasecmp(action, "accept")) {
172                         ical_add(ird.cal, 0);
173                 }
174
175                 /* Send a reply if necessary */
176                 /* FIXME ... do this */
177
178                 /* Delete the message from the inbox */
179                 /* FIXME ... do this */
180
181                 icalcomponent_free(ird.cal);
182                 ird.cal = NULL;
183                 cprintf("%d ok\n", CIT_OK);
184                 return;
185         }
186         else {
187                 cprintf("%d No calendar object found\n", ERROR);
188                 return;
189         }
190
191         /* should never get here */
192 }
193
194
195 /*
196  * Search for a property in both the top level and in a VEVENT subcomponent
197  */
198 icalproperty *ical_ctdl_get_subprop(
199                 icalcomponent *cal,
200                 icalproperty_kind which_prop
201 ) {
202         icalproperty *p;
203         icalcomponent *c;
204
205         p = icalcomponent_get_first_property(cal, which_prop);
206         if (p == NULL) {
207                 c = icalcomponent_get_first_component(cal,
208                                                         ICAL_VEVENT_COMPONENT);
209                 if (c != NULL) {
210                         p = icalcomponent_get_first_property(c, which_prop);
211                 }
212         }
213         return p;
214 }
215
216
217 /*
218  * Helper function for ical_ctdl_is_overlap() to simplify the code when
219  * comparing year/month/day.  The number doesn't have to be meaningful, only
220  * consistent and unique for the supplied y/m/d combination.
221  */
222 inline int itymd(struct icaltimetype t) {
223         return (
224                 (t.year * 416)
225                 + (t.month * 32)
226                 + (t.day)
227         );
228 }
229
230 /*
231  * Check to see if two events overlap.  Returns nonzero if they do.
232  */
233 int ical_ctdl_is_overlap(
234                         struct icaltimetype t1start,
235                         struct icaltimetype t1end,
236                         struct icaltimetype t2start,
237                         struct icaltimetype t2end
238 ) {
239
240         if (icaltime_is_null_time(t1start)) return(0);
241         if (icaltime_is_null_time(t2start)) return(0);
242
243         /* First, check for all-day events */
244         if (t1start.is_date) {
245                 if (!icaltime_compare_date_only(t1start, t2start)) {
246                         return(1);
247                 }
248                 if (!icaltime_is_null_time(t2end)) {
249                         if (!icaltime_compare_date_only(t1start, t2end)) {
250                                 return(1);
251                         }
252                 }
253         }
254
255         if (t2start.is_date) {
256                 if (!icaltime_compare_date_only(t2start, t1start)) {
257                         return(1);
258                 }
259                 if (!icaltime_is_null_time(t1end)) {
260                         if (!icaltime_compare_date_only(t2start, t1end)) {
261                                 return(1);
262                         }
263                 }
264         }
265
266         /* Now check for overlaps using date *and* time. */
267
268         /* First, bail out if either event 1 or event 2 is missing end time. */
269         if (icaltime_is_null_time(t1end)) return(0);
270         if (icaltime_is_null_time(t2end)) return(0);
271
272         /* If event 1 ends before event 2 starts, we're in the clear. */
273         if (icaltime_compare(t1end, t2start) <= 0) return(0);
274
275         /* If event 2 ends before event 1 starts, we're also ok. */
276         if (icaltime_compare(t2end, t1start) <= 0) return(0);
277
278         /* Otherwise, they overlap. */
279         return(1);
280 }
281
282
283
284 /*
285  * Backend for ical_hunt_for_conflicts()
286  */
287 void vcard_hunt_for_conflicts_backend(long msgnum, void *data) {
288         icalcomponent *cal;
289         struct CtdlMessage *msg;
290         struct ical_respond_data ird;
291         struct icaltimetype t1start, t1end, t2start, t2end;
292         icalproperty *p;
293
294         cal = (icalcomponent *)data;
295
296         msg = CtdlFetchMessage(msgnum);
297         if (msg == NULL) return;
298         memset(&ird, 0, sizeof ird);
299         strcpy(ird.desired_partnum, "1");       /* hopefully it's always 1 */
300         mime_parser(msg->cm_fields['M'],
301                 NULL,
302                 *ical_locate_part,              /* callback function */
303                 NULL, NULL,
304                 (void *) &ird,                  /* user data */
305                 0
306         );
307         CtdlFreeMessage(msg);
308
309         if (ird.cal == NULL) return;
310
311         t1start = icaltime_null_time();
312         t1end = icaltime_null_time();
313         t2start = icaltime_null_time();
314         t1end = icaltime_null_time();
315
316         /* Now compare cal to ird.cal */
317         p = ical_ctdl_get_subprop(ird.cal, ICAL_DTSTART_PROPERTY);
318         if (p == NULL) return;
319         if (p != NULL) t2start = icalproperty_get_dtstart(p);
320         
321         p = ical_ctdl_get_subprop(ird.cal, ICAL_DTEND_PROPERTY);
322         if (p != NULL) t2end = icalproperty_get_dtend(p);
323
324         p = ical_ctdl_get_subprop(cal, ICAL_DTSTART_PROPERTY);
325         if (p == NULL) return;
326         if (p != NULL) t1start = icalproperty_get_dtstart(p);
327         
328         p = ical_ctdl_get_subprop(cal, ICAL_DTEND_PROPERTY);
329         if (p != NULL) t1end = icalproperty_get_dtend(p);
330         
331         icalcomponent_free(ird.cal);
332
333         if (ical_ctdl_is_overlap(t1start, t1end, t2start, t2end)) {
334                 cprintf("%ld|FIXME put more here\n",
335                         msgnum
336                 );
337         }
338 }
339
340
341
342 /* 
343  * Phase 2 of "hunt for conflicts" operation.
344  * At this point we have a calendar object which represents the VEVENT that
345  * we're considering adding to the calendar.  Now hunt through the user's
346  * calendar room, and output zero or more existing VEVENTs which conflict
347  * with this one.
348  */
349 void ical_hunt_for_conflicts(icalcomponent *cal) {
350         char hold_rm[ROOMNAMELEN];
351
352         strcpy(hold_rm, CC->quickroom.QRname);  /* save current room */
353
354         if (getroom(&CC->quickroom, USERCALENDARROOM) != 0) {
355                 getroom(&CC->quickroom, hold_rm);
356                 cprintf("%d You do not have a calendar.\n", ERROR);
357                 return;
358         }
359
360         cprintf("%d Conflicting events:\n", LISTING_FOLLOWS);
361
362         CtdlForEachMessage(MSGS_ALL, 0, "text/calendar",
363                 NULL,
364                 vcard_hunt_for_conflicts_backend,
365                 (void *) cal
366         );
367
368         cprintf("000\n");
369         getroom(&CC->quickroom, hold_rm);       /* return to saved room */
370
371 }
372
373
374
375 /*
376  * Hunt for conflicts (Phase 1 -- retrieve the object and call Phase 2)
377  */
378 void ical_conflicts(long msgnum, char *partnum) {
379         struct CtdlMessage *msg;
380         struct ical_respond_data ird;
381
382         msg = CtdlFetchMessage(msgnum);
383         if (msg == NULL) {
384                 cprintf("%d Message %ld not found.\n",
385                         ERROR+ILLEGAL_VALUE,
386                         (long)msgnum
387                 );
388                 return;
389         }
390
391         memset(&ird, 0, sizeof ird);
392         strcpy(ird.desired_partnum, partnum);
393         mime_parser(msg->cm_fields['M'],
394                 NULL,
395                 *ical_locate_part,              /* callback function */
396                 NULL, NULL,
397                 (void *) &ird,                  /* user data */
398                 0
399         );
400
401         CtdlFreeMessage(msg);
402
403         if (ird.cal != NULL) {
404                 ical_hunt_for_conflicts(ird.cal);
405                 icalcomponent_free(ird.cal);
406                 return;
407         }
408         else {
409                 cprintf("%d No calendar object found\n", ERROR);
410                 return;
411         }
412
413         /* should never get here */
414 }
415
416
417
418
419 /*
420  * All Citadel calendar commands from the client come through here.
421  */
422 void cmd_ical(char *argbuf)
423 {
424         char subcmd[SIZ];
425         long msgnum;
426         char partnum[SIZ];
427         char action[SIZ];
428
429         if (CtdlAccessCheck(ac_logged_in)) return;
430
431         extract(subcmd, argbuf, 0);
432
433         if (!strcmp(subcmd, "test")) {
434                 cprintf("%d This server supports calendaring\n", CIT_OK);
435                 return;
436         }
437
438         else if (!strcmp(subcmd, "respond")) {
439                 msgnum = extract_long(argbuf, 1);
440                 extract(partnum, argbuf, 2);
441                 extract(action, argbuf, 3);
442                 ical_respond(msgnum, partnum, action);
443         }
444
445         else if (!strcmp(subcmd, "conflicts")) {
446                 msgnum = extract_long(argbuf, 1);
447                 extract(partnum, argbuf, 2);
448                 ical_conflicts(msgnum, partnum);
449         }
450
451         else {
452                 cprintf("%d Invalid subcommand\n", ERROR+CMD_NOT_SUPPORTED);
453                 return;
454         }
455
456         /* should never get here */
457 }
458
459 #endif /* HAVE_ICAL_H */
460
461
462 /*
463  * We don't know if the calendar room exists so we just create it at login
464  */
465 void ical_create_room(void)
466 {
467         struct quickroom qr;
468         struct visit vbuf;
469
470         /* Create the calendar room if it doesn't already exist */
471         create_room(USERCALENDARROOM, 4, "", 0, 1, 0);
472
473         /* Set expiration policy to manual; otherwise objects will be lost! */
474         if (lgetroom(&qr, USERCALENDARROOM)) {
475                 lprintf(3, "Couldn't get the user calendar room!\n");
476                 return;
477         }
478         qr.QRep.expire_mode = EXPIRE_MANUAL;
479         lputroom(&qr);
480
481         /* Set the view to a calendar view */
482         CtdlGetRelationship(&vbuf, &CC->usersupp, &qr);
483         vbuf.v_view = 3;        /* 3 = calendar */
484         CtdlSetRelationship(&vbuf, &CC->usersupp, &qr);
485
486         /* Create the tasks list room if it doesn't already exist */
487         create_room(USERTASKSROOM, 4, "", 0, 1, 0);
488
489         /* Set expiration policy to manual; otherwise objects will be lost! */
490         if (lgetroom(&qr, USERTASKSROOM)) {
491                 lprintf(3, "Couldn't get the user calendar room!\n");
492                 return;
493         }
494         qr.QRep.expire_mode = EXPIRE_MANUAL;
495         lputroom(&qr);
496
497         /* Set the view to a task list view */
498         CtdlGetRelationship(&vbuf, &CC->usersupp, &qr);
499         vbuf.v_view = 4;        /* 4 = tasks */
500         CtdlSetRelationship(&vbuf, &CC->usersupp, &qr);
501
502         return;
503 }
504
505
506 /* See if we need to prevent the object from being saved */
507 int ical_obj_beforesave(struct CtdlMessage *msg)
508 {
509         char roomname[ROOMNAMELEN];
510         char *p;
511         int a;
512         
513         /*
514          * Only messages with content-type text/calendar or text/x-calendar
515          * may be saved to Calendar>.  If the message is bound for
516          * Calendar> but doesn't have this content-type, throw an error
517          * so that the message may not be posted.
518          */
519
520         /* First determine if this is our room */
521         MailboxName(roomname, sizeof roomname, &CC->usersupp, USERCALENDARROOM);
522         if (strncmp(roomname, msg->cm_fields['O'], ROOMNAMELEN))
523                 return 0;       /* It's not us... */
524
525         /* Then determine content-type of the message */
526         
527         /* It must be an RFC822 message! */
528         /* FIXME: Not handling MIME multipart messages; implement with IMIP */
529         if (msg->cm_format_type != 4)
530                 return 1;       /* You tried to save a non-RFC822 message! */
531         
532         /* Find the Content-Type: header */
533         p = msg->cm_fields['M'];
534         a = strlen(p);
535         while (--a > 0) {
536                 if (!strncasecmp(p, "Content-Type: ", 14)) {    /* Found it */
537                         if (!strncasecmp(p + 14, "text/x-calendar", 15) ||
538                             !strncasecmp(p + 14, "text/calendar", 13))
539                                 return 0;
540                         else
541                                 return 1;
542                 }
543                 p++;
544         }
545         
546         /* Oops!  No Content-Type in this message!  How'd that happen? */
547         lprintf(7, "RFC822 message with no Content-Type header!\n");
548         return 1;
549 }
550
551
552
553 /* Register this module with the Citadel server. */
554 char *Dynamic_Module_Init(void)
555 {
556         CtdlRegisterMessageHook(ical_obj_beforesave, EVT_BEFORESAVE);
557 #ifdef HAVE_ICAL_H
558         CtdlRegisterSessionHook(ical_create_room, EVT_LOGIN);
559         CtdlRegisterProtoHook(cmd_ical, "ICAL", "Citadel iCal commands");
560 #endif
561         return "$Id$";
562 }