]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Appointments/SOGoAppointmentObject.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1026 d1b88da0-ebda-0310...
[scalable-opengroupware.org] / SoObjects / Appointments / SOGoAppointmentObject.m
1 /*
2   Copyright (C) 2004-2005 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
6   OGo is free software; you can redistribute it and/or modify it under
7   the terms of the GNU Lesser General Public License as published by the
8   Free Software Foundation; either version 2, or (at your option) any
9   later version.
10
11   OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12   WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14   License for more details.
15
16   You should have received a copy of the GNU Lesser General Public
17   License along with OGo; see the file COPYING.  If not, write to the
18   Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19   02111-1307, USA.
20 */
21
22 #import "SOGoAppointmentObject.h"
23
24 #import <NGCards/iCalCalendar.h>
25 #import <NGCards/iCalEvent.h>
26 #import <NGCards/iCalEventChanges.h>
27 #import <NGCards/iCalPerson.h>
28
29 #import <SOGo/AgenorUserManager.h>
30 #import <SOGo/SOGoObject.h>
31 #import <SOGo/SOGoPermissions.h>
32
33 #import "iCalEntityObject+Agenor.h"
34
35 #import "common.h"
36
37 #import "NSArray+Appointments.h"
38
39 @implementation SOGoAppointmentObject
40
41 - (NSString *) componentTag
42 {
43   return @"vevent";
44 }
45
46 /* iCal handling */
47 - (NSArray *) attendeeUIDsFromAppointment: (iCalEvent *) _apt
48 {
49   AgenorUserManager *um;
50   NSMutableArray *uids;
51   NSArray *attendees;
52   unsigned i, count;
53   NSString *email, *uid;
54   
55   if (![_apt isNotNull])
56     return nil;
57   
58   if ((attendees = [_apt attendees]) == nil)
59     return nil;
60   count = [attendees count];
61   uids = [NSMutableArray arrayWithCapacity:count + 1];
62   
63   um = [AgenorUserManager sharedUserManager];
64   
65   /* add organizer */
66   
67   email = [[_apt organizer] rfc822Email];
68   if ([email isNotNull]) {
69     uid = [um getUIDForEmail:email];
70     if ([uid isNotNull]) {
71       [uids addObject:uid];
72     }
73     else
74       [self logWithFormat:@"Note: got no uid for organizer: '%@'", email];
75   }
76
77   /* add attendees */
78   
79   for (i = 0; i < count; i++)
80     {
81       iCalPerson *person;
82     
83       person = [attendees objectAtIndex:i];
84       email  = [person rfc822Email];
85       if (![email isNotNull]) continue;
86     
87       uid = [um getUIDForEmail:email];
88       if (![uid isNotNull]) {
89         [self logWithFormat:@"Note: got no uid for email: '%@'", email];
90         continue;
91       }
92       if (![uids containsObject:uid])
93         [uids addObject:uid];
94     }
95
96   return uids;
97 }
98
99 /* folder management */
100
101 - (id)lookupHomeFolderForUID:(NSString *)_uid inContext:(id)_ctx {
102   // TODO: what does this do? lookup the home of the organizer?
103   return [[self container] lookupHomeFolderForUID:_uid inContext:_ctx];
104 }
105 - (NSArray *)lookupCalendarFoldersForUIDs:(NSArray *)_uids inContext:(id)_ctx {
106   return [[self container] lookupCalendarFoldersForUIDs:_uids inContext:_ctx];
107 }
108
109 /* store in all the other folders */
110
111 - (NSException *)saveContentString:(NSString *)_iCal inUIDs:(NSArray *)_uids {
112   NSEnumerator *e;
113   id folder;
114   NSException *allErrors = nil;
115   id ctx;
116
117   ctx = [[WOApplication application] context];
118   
119   e = [[self lookupCalendarFoldersForUIDs:_uids inContext:ctx]
120              objectEnumerator];
121   while ((folder = [e nextObject]) != nil) {
122     NSException           *error;
123     SOGoAppointmentObject *apt;
124     
125     if (![folder isNotNull]) /* no folder was found for given UID */
126       continue;
127
128     apt = [folder lookupName: [self nameInContainer] inContext:ctx
129                   acquire: NO];
130     if ([apt isKindOfClass: [NSException class]])
131       {
132         [self logWithFormat:@"Note: an exception occured finding '%@' in folder: %@",
133               [self nameInContainer], folder];
134         [self logWithFormat:@"the exception reason was: %@",
135               [(NSException *) apt reason]];
136         continue;
137       }
138
139     if (![apt isNotNull]) {
140       [self logWithFormat:@"Note: did not find '%@' in folder: %@",
141               [self nameInContainer], folder];
142       continue;
143     }
144     if ([apt isKindOfClass: [NSException class]]) {
145       [self logWithFormat:@"Exception: %@", [(NSException *) apt reason]];
146       continue;
147     }
148     
149     if ((error = [apt primarySaveContentString:_iCal]) != nil) {
150       [self logWithFormat:@"Note: failed to save iCal in folder: %@", folder];
151       // TODO: make compound
152       allErrors = error;
153     }
154   }
155
156   return allErrors;
157 }
158
159 - (NSException *)deleteInUIDs:(NSArray *)_uids {
160   NSEnumerator *e;
161   id folder;
162   NSException *allErrors = nil;
163   id ctx;
164   
165   ctx = [[WOApplication application] context];
166   
167   e = [[self lookupCalendarFoldersForUIDs:_uids inContext:ctx]
168              objectEnumerator];
169   while ((folder = [e nextObject])) {
170     NSException           *error;
171     SOGoAppointmentObject *apt;
172     
173     apt = [folder lookupName:[self nameInContainer] inContext:ctx
174                   acquire:NO];
175     if ([apt isKindOfClass: [NSException class]]) {
176       [self logWithFormat: @"%@", [(NSException *) apt reason]];
177       continue;
178     }
179     
180     if ((error = [apt primaryDelete]) != nil) {
181       [self logWithFormat:@"Note: failed to delete in folder: %@", folder];
182       // TODO: make compound
183       allErrors = error;
184     }
185   }
186   return allErrors;
187 }
188
189 /* "iCal multifolder saves" */
190
191 - (NSException *) saveContentString: (NSString *) _iCal
192                        baseSequence: (int) _v
193 {
194   /* 
195      Note: we need to delete in all participants folders and send iMIP messages
196            for all external accounts.
197      
198      Steps:
199      - fetch stored content
200      - parse old content
201      - check if sequence matches (or if 0=ignore)
202      - extract old attendee list + organizer (make unique)
203      - parse new content (ensure that sequence is increased!)
204      - extract new attendee list + organizer (make unique)
205      - make a diff => new, same, removed
206      - write to new, same
207      - delete in removed folders
208      - send iMIP mail for all folders not found
209   */
210   AgenorUserManager *um;
211   iCalCalendar *newCalendar;
212   iCalEvent *oldApt, *newApt;
213   iCalEventChanges *changes;
214   iCalPerson *organizer;
215   NSString *oldContent, *uid;
216   NSArray *uids, *props;
217   NSMutableArray *attendees, *storeUIDs, *removedUIDs;
218   NSException *storeError, *delError;
219   BOOL updateForcesReconsider;
220   
221   updateForcesReconsider = NO;
222
223   if ([_iCal length] == 0) {
224     return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
225                         reason:@"got no iCalendar content to store!"];
226   }
227
228   um = [AgenorUserManager sharedUserManager];
229
230   /* handle old content */
231   
232   oldContent = [self contentAsString]; /* if nil, this is a new appointment */
233   if ([oldContent length] == 0)
234     {
235     /* new appointment */
236       [self debugWithFormat:@"saving new appointment: %@", _iCal];
237       oldApt = nil;
238     }
239   else
240     oldApt = (iCalEvent *) [self component];
241   
242   /* compare sequence if requested */
243
244   if (_v != 0) {
245     // TODO
246   }
247   
248   
249   /* handle new content */
250   
251   newCalendar = [iCalCalendar parseSingleFromSource: _iCal];
252   newApt = (iCalEvent *) [newCalendar firstChildWithTag: [self componentTag]];
253   if (newApt == nil) {
254     return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
255                         reason:@"could not parse iCalendar content!"];
256   }
257   
258   /* diff */
259   
260   changes = [iCalEventChanges changesFromEvent: oldApt
261                               toEvent: newApt];
262
263   uids = [um getUIDsForICalPersons:[changes deletedAttendees]
264                     applyStrictMapping:NO];
265   removedUIDs = [NSMutableArray arrayWithArray:uids];
266
267   uids = [um getUIDsForICalPersons:[newApt attendees]
268                     applyStrictMapping:NO];
269   storeUIDs = [NSMutableArray arrayWithArray:uids];
270   props = [changes updatedProperties];
271
272   /* detect whether sequence has to be increased */
273   if ([changes hasChanges])
274     [newApt increaseSequence];
275
276   /* preserve organizer */
277
278   organizer = [newApt organizer];
279   uid = [um getUIDForICalPerson:organizer];
280   if (uid) {
281     if (![storeUIDs containsObject:uid])
282       [storeUIDs addObject:uid];
283     [removedUIDs removeObject:uid];
284   }
285
286   /* organizer might have changed completely */
287
288   if (oldApt && ([props containsObject: @"organizer"])) {
289     uid = [um getUIDForICalPerson:[oldApt organizer]];
290     if (uid) {
291       if (![storeUIDs containsObject:uid]) {
292         if (![removedUIDs containsObject:uid]) {
293           [removedUIDs addObject:uid];
294         }
295       }
296     }
297   }
298
299   [self debugWithFormat:@"UID ops:\n  store: %@\n  remove: %@",
300                         storeUIDs, removedUIDs];
301
302   /* if time did change, all participants have to re-decide ...
303    * ... exception from that rule: the organizer
304    */
305
306   if (oldApt != nil &&
307       ([props containsObject:@"startDate"] ||
308        [props containsObject:@"endDate"]   ||
309        [props containsObject:@"duration"]))
310   {
311     NSArray  *ps;
312     unsigned i, count;
313     
314     ps    = [newApt attendees];
315     count = [ps count];
316     for (i = 0; i < count; i++) {
317       iCalPerson *p;
318       
319       p = [ps objectAtIndex:i];
320       if (![p hasSameEmailAddress:organizer])
321         [p setParticipationStatus:iCalPersonPartStatNeedsAction];
322     }
323     _iCal = [[newApt parent] versitString];
324     updateForcesReconsider = YES;
325   }
326
327   /* perform storing */
328
329   storeError = [self saveContentString:_iCal inUIDs:storeUIDs];
330   delError = [self deleteInUIDs:removedUIDs];
331
332   // TODO: make compound
333   if (storeError != nil) return storeError;
334   if (delError   != nil) return delError;
335
336   /* email notifications */
337   if ([self sendEMailNotifications])
338     {
339       attendees = [NSMutableArray arrayWithArray: [changes insertedAttendees]];
340       [attendees removePerson: organizer];
341       [self sendEMailUsingTemplateNamed: @"Invitation"
342             forOldObject: nil
343             andNewObject: newApt
344             toAttendees: attendees];
345
346       if (updateForcesReconsider) {
347         attendees = [NSMutableArray arrayWithArray:[newApt attendees]];
348         [attendees removeObjectsInArray:[changes insertedAttendees]];
349         [attendees removePerson:organizer];
350         [self sendEMailUsingTemplateNamed: @"Update"
351               forOldObject: oldApt
352               andNewObject: newApt
353               toAttendees: attendees];
354       }
355
356       attendees = [NSMutableArray arrayWithArray:[changes deletedAttendees]];
357       [attendees removePerson: organizer];
358       if ([attendees count])
359         {
360           iCalEvent *canceledApt;
361     
362           canceledApt = [newApt copy];
363           [(iCalCalendar *) [canceledApt parent] setMethod: @"cancel"];
364           [self sendEMailUsingTemplateNamed: @"Removal"
365                 forOldObject: nil
366                 andNewObject: canceledApt
367                 toAttendees: attendees];
368           [canceledApt release];
369         }
370     }
371
372   return nil;
373 }
374
375 - (NSException *)deleteWithBaseSequence:(int)_v {
376   /* 
377      Note: We need to delete in all participants folders and send iMIP messages
378            for all external accounts.
379            Delete is basically identical to save with all attendees and the
380            organizer being deleted.
381
382      Steps:
383      - fetch stored content
384      - parse old content
385      - check if sequence matches (or if 0=ignore)
386      - extract old attendee list + organizer (make unique)
387      - delete in removed folders
388      - send iMIP mail for all folders not found
389   */
390   iCalEvent *apt;
391   NSArray *removedUIDs;
392   NSMutableArray *attendees;
393
394   /* load existing content */
395
396   apt = (iCalEvent *) [self component];
397   
398   /* compare sequence if requested */
399
400 //   if (_v != 0) {
401 //     // TODO
402 //   }
403   
404   removedUIDs = [self attendeeUIDsFromAppointment:apt];
405
406   if ([self sendEMailNotifications])
407     {
408       /* send notification email to attendees excluding organizer */
409       attendees = [NSMutableArray arrayWithArray:[apt attendees]];
410       [attendees removePerson:[apt organizer]];
411   
412       /* flag appointment as being canceled */
413       [(iCalCalendar *) [apt parent] setMethod: @"cancel"];
414       [apt increaseSequence];
415
416       /* remove all attendees to signal complete removal */
417       [apt removeAllAttendees];
418
419       /* send notification email */
420       [self sendEMailUsingTemplateNamed: @"Deletion"
421             forOldObject: nil
422             andNewObject: apt
423             toAttendees: attendees];
424     }
425
426   /* perform */
427
428   return [self deleteInUIDs:removedUIDs];
429 }
430
431 - (NSException *) saveContentString: (NSString *) _iCalString
432 {
433   return [self saveContentString: _iCalString baseSequence: 0];
434 }
435
436 - (NSException *) changeParticipationStatus: (NSString *) _status
437                                   inContext: (id) _ctx
438 {
439   iCalEvent *apt;
440   iCalPerson *p;
441   NSString *newContent;
442   NSException *ex;
443   NSString *myEMail;
444   
445   ex = nil;
446
447   // TODO: do we need to use SOGoAppointment? (prefer iCalEvent?)
448   apt = (iCalEvent *) [self component];
449
450   if (apt)
451     {
452       myEMail = [[_ctx activeUser] email];
453       p = [apt findParticipantWithEmail: myEMail];
454       if (p)
455         {
456   // TODO: send iMIP reply mails?
457   
458           [p setPartStat:_status];
459           newContent = [[apt parent] versitString];
460           if (newContent)
461             {
462               ex = [self saveContentString:newContent];
463               if (ex)
464                 // TODO: why is the exception wrapped?
465                 /* Server Error */
466                 ex = [NSException exceptionWithHTTPStatus: 500
467                                   reason: [ex reason]];
468             }
469           else
470             ex
471               = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
472                              reason: @"Could not generate iCalendar data ..."];
473         }
474       else
475         ex = [NSException exceptionWithHTTPStatus: 404 /* Not Found */
476                           reason: @"user does not participate in this "
477                           @"appointment"];
478     }
479   else
480     ex = [NSException exceptionWithHTTPStatus:500 /* Server Error */
481                       reason:@"unable to parse appointment record"];
482
483   return ex;
484 }
485
486
487 /* message type */
488
489 - (NSString *) outlookMessageClass
490 {
491   return @"IPM.Appointment";
492 }
493
494 - (NSException *) saveContentString: (NSString *) contentString
495                         baseVersion: (unsigned int) baseVersion
496 {
497   NSString *newContentString, *oldContentString;
498   iCalCalendar *eventCalendar;
499   iCalEvent *event;
500   NSArray *organizers;
501
502   oldContentString = [self contentAsString];
503   if (oldContentString)
504     newContentString = contentString;
505   else
506     {
507       eventCalendar = [iCalCalendar parseSingleFromSource: contentString];
508       event = (iCalEvent *) [eventCalendar firstChildWithTag: [self componentTag]];
509       organizers = [event childrenWithTag: @"organizer"];
510       if ([organizers count])
511         newContentString = contentString;
512       else
513         {
514           [event setOrganizerWithUid: [[self container] ownerInContext: nil]];
515           newContentString = [eventCalendar versitString];
516         }
517     }
518
519   return [super saveContentString: newContentString
520                 baseVersion: baseVersion];
521 }
522
523 @end /* SOGoAppointmentObject */