]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Appointments/SOGoAppointmentObject.m
33e14028227b76b2908e7d2c0dd14098843d6217
[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 <Foundation/NSCalendarDate.h>
23
24 #import <NGObjWeb/NSException+HTTP.h>
25 #import <NGExtensions/NSNull+misc.h>
26 #import <NGExtensions/NSObject+Logs.h>
27 #import <NGCards/iCalCalendar.h>
28 #import <NGCards/iCalEvent.h>
29 #import <NGCards/iCalEventChanges.h>
30 #import <NGCards/iCalPerson.h>
31
32 #import <SoObjects/SOGo/LDAPUserManager.h>
33 #import <SoObjects/SOGo/SOGoObject.h>
34 #import <SoObjects/SOGo/SOGoPermissions.h>
35
36 #import "NSArray+Appointments.h"
37 #import "SOGoAppointmentObject.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   LDAPUserManager *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 = [LDAPUserManager 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
112                              inUIDs: (NSArray *) _uids
113 {
114   NSEnumerator *e;
115   id folder;
116   NSException *allErrors = nil;
117   
118   e = [[self lookupCalendarFoldersForUIDs:_uids inContext: context]
119              objectEnumerator];
120   while ((folder = [e nextObject]) != nil) {
121     NSException           *error;
122     SOGoAppointmentObject *apt;
123     
124     if (![folder isNotNull]) /* no folder was found for given UID */
125       continue;
126
127     apt = [folder lookupName: [self nameInContainer] inContext: context
128                   acquire: NO];
129     if ([apt isKindOfClass: [NSException class]])
130       {
131         [self logWithFormat:@"Note: an exception occured finding '%@' in folder: %@",
132               [self nameInContainer], folder];
133         [self logWithFormat:@"the exception reason was: %@",
134               [(NSException *) apt reason]];
135         continue;
136       }
137
138     if (![apt isNotNull]) {
139       [self logWithFormat:@"Note: did not find '%@' in folder: %@",
140               [self nameInContainer], folder];
141       continue;
142     }
143     if ([apt isKindOfClass: [NSException class]]) {
144       [self logWithFormat:@"Exception: %@", [(NSException *) apt reason]];
145       continue;
146     }
147     
148     if ((error = [apt primarySaveContentString:_iCal]) != nil) {
149       [self logWithFormat:@"Note: failed to save iCal in folder: %@", folder];
150       // TODO: make compound
151       allErrors = error;
152     }
153   }
154
155   return allErrors;
156 }
157
158 - (NSException *)deleteInUIDs:(NSArray *)_uids {
159   NSEnumerator *e;
160   id folder;
161   NSException *allErrors = nil;
162   
163   e = [[self lookupCalendarFoldersForUIDs:_uids inContext: context]
164              objectEnumerator];
165   while ((folder = [e nextObject])) {
166     NSException           *error;
167     SOGoAppointmentObject *apt;
168     
169     apt = [folder lookupName:[self nameInContainer] inContext: context
170                   acquire:NO];
171     if ([apt isKindOfClass: [NSException class]]) {
172       [self logWithFormat: @"%@", [(NSException *) apt reason]];
173       continue;
174     }
175     
176     if ((error = [apt primaryDelete]) != nil) {
177       [self logWithFormat:@"Note: failed to delete in folder: %@", folder];
178       // TODO: make compound
179       allErrors = error;
180     }
181   }
182   return allErrors;
183 }
184
185 /* "iCal multifolder saves" */
186 - (BOOL) _aptIsStillRelevant: (iCalEvent *) appointment
187 {
188   NSCalendarDate *now;
189
190   now = [NSCalendarDate calendarDate];
191
192   return ([[appointment endDate] earlierDate: now] == now);
193 }
194
195 - (NSException *) saveContentString: (NSString *) _iCal
196                        baseSequence: (int) _v
197 {
198   /* 
199      Note: we need to delete in all participants folders and send iMIP messages
200            for all external accounts.
201      
202      Steps:
203      - fetch stored content
204      - parse old content
205      - check if sequence matches (or if 0=ignore)
206      - extract old attendee list + organizer (make unique)
207      - parse new content (ensure that sequence is increased!)
208      - extract new attendee list + organizer (make unique)
209      - make a diff => new, same, removed
210      - write to new, same
211      - delete in removed folders
212      - send iMIP mail for all folders not found
213   */
214   LDAPUserManager *um;
215   iCalEvent *oldApt, *newApt;
216   iCalEventChanges *changes;
217   iCalPerson *organizer;
218   NSString *oldContent, *uid;
219   NSArray *uids, *props;
220   NSMutableArray *attendees, *storeUIDs, *removedUIDs;
221   NSException *storeError, *delError;
222   BOOL updateForcesReconsider;
223   
224   updateForcesReconsider = NO;
225
226   if ([_iCal length] == 0)
227     return [NSException exceptionWithHTTPStatus: 400 /* Bad Request */
228                         reason: @"got no iCalendar content to store!"];
229
230   um = [LDAPUserManager sharedUserManager];
231
232   /* handle old content */
233   
234   oldContent = [self contentAsString]; /* if nil, this is a new appointment */
235   if ([oldContent length] == 0)
236     {
237     /* new appointment */
238       [self debugWithFormat:@"saving new appointment: %@", _iCal];
239       oldApt = nil;
240     }
241   else
242     oldApt = (iCalEvent *) [self component: NO];
243   
244   /* compare sequence if requested */
245
246   if (_v != 0) {
247     // TODO
248   }
249   
250   /* handle new content */
251   
252   newApt = (iCalEvent *) [self component: NO];
253   if (!newApt)
254     return [NSException exceptionWithHTTPStatus: 400 /* Bad Request */
255                         reason: @"could not parse iCalendar content!"];
256
257   /* diff */
258   
259   changes = [iCalEventChanges changesFromEvent: oldApt toEvent: newApt];
260   uids = [self getUIDsForICalPersons: [changes deletedAttendees]];
261   removedUIDs = [NSMutableArray arrayWithArray: uids];
262
263   uids = [self getUIDsForICalPersons: [newApt attendees]];
264   storeUIDs = [NSMutableArray arrayWithArray: uids];
265   props = [changes updatedProperties];
266
267   /* detect whether sequence has to be increased */
268   if ([changes hasChanges])
269     [newApt increaseSequence];
270
271   /* preserve organizer */
272
273   organizer = [newApt organizer];
274   uid = [self getUIDForICalPerson: organizer];
275   if (!uid)
276     uid = [self ownerInContext: nil];
277   if (uid) {
278     if (![storeUIDs containsObject:uid])
279       [storeUIDs addObject:uid];
280     [removedUIDs removeObject:uid];
281   }
282
283   /* organizer might have changed completely */
284
285   if (oldApt && ([props containsObject: @"organizer"])) {
286     uid = [self getUIDForICalPerson:[oldApt organizer]];
287     if (uid) {
288       if (![storeUIDs containsObject:uid]) {
289         if (![removedUIDs containsObject:uid]) {
290           [removedUIDs addObject:uid];
291         }
292       }
293     }
294   }
295
296   [self debugWithFormat:@"UID ops:\n  store: %@\n  remove: %@",
297                         storeUIDs, removedUIDs];
298
299   /* if time did change, all participants have to re-decide ...
300    * ... exception from that rule: the organizer
301    */
302
303   if (oldApt != nil &&
304       ([props containsObject: @"startDate"] ||
305        [props containsObject: @"endDate"]   ||
306        [props containsObject: @"duration"]))
307   {
308     NSArray  *ps;
309     unsigned i, count;
310     
311     ps    = [newApt attendees];
312     count = [ps count];
313     for (i = 0; i < count; i++) {
314       iCalPerson *p;
315       
316       p = [ps objectAtIndex:i];
317       if (![p hasSameEmailAddress:organizer])
318         [p setParticipationStatus:iCalPersonPartStatNeedsAction];
319     }
320     _iCal = [[newApt parent] versitString];
321     updateForcesReconsider = YES;
322   }
323
324   /* perform storing */
325
326   storeError = [self saveContentString: _iCal inUIDs: storeUIDs];
327   delError = [self deleteInUIDs: removedUIDs];
328
329   // TODO: make compound
330   if (storeError != nil) return storeError;
331   if (delError   != nil) return delError;
332
333   /* email notifications */
334   if ([self sendEMailNotifications]
335       && [self _aptIsStillRelevant: newApt])
336     {
337       attendees
338         = [NSMutableArray arrayWithArray: [changes insertedAttendees]];
339       [attendees removePerson: organizer];
340       [self sendEMailUsingTemplateNamed: @"Invitation"
341             forOldObject: nil
342             andNewObject: newApt
343             toAttendees: attendees];
344
345       if (updateForcesReconsider) {
346         attendees = [NSMutableArray arrayWithArray:[newApt attendees]];
347         [attendees removeObjectsInArray:[changes insertedAttendees]];
348         [attendees removePerson:organizer];
349         [self sendEMailUsingTemplateNamed: @"Update"
350               forOldObject: oldApt
351               andNewObject: newApt
352               toAttendees: attendees];
353       }
354
355       attendees
356         = [NSMutableArray arrayWithArray: [changes deletedAttendees]];
357       [attendees removePerson: organizer];
358       if ([attendees count])
359         {
360           iCalEvent *cancelledApt;
361     
362           cancelledApt = [newApt copy];
363           [(iCalCalendar *) [cancelledApt parent] setMethod: @"cancel"];
364           [self sendEMailUsingTemplateNamed: @"Removal"
365                 forOldObject: nil
366                 andNewObject: cancelledApt
367                 toAttendees: attendees];
368           [cancelledApt 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   NSMutableArray *attendees, *removedUIDs;
392
393   /* load existing content */
394
395   apt = (iCalEvent *) [self component: NO];
396   
397   /* compare sequence if requested */
398
399 //   if (_v != 0) {
400 //     // TODO
401 //   }
402   
403   removedUIDs = [NSMutableArray arrayWithArray:
404                                   [self attendeeUIDsFromAppointment: apt]];
405   if (![removedUIDs containsObject: owner])
406     [removedUIDs addObject: owner];
407
408   if ([self sendEMailNotifications])
409     {
410       /* send notification email to attendees excluding organizer */
411       attendees = [NSMutableArray arrayWithArray:[apt attendees]];
412       [attendees removePerson:[apt organizer]];
413   
414       /* flag appointment as being cancelled */
415       [(iCalCalendar *) [apt parent] setMethod: @"cancel"];
416       [apt increaseSequence];
417
418       /* remove all attendees to signal complete removal */
419       [apt removeAllAttendees];
420
421       /* send notification email */
422       [self sendEMailUsingTemplateNamed: @"Deletion"
423             forOldObject: nil
424             andNewObject: apt
425             toAttendees: attendees];
426     }
427
428   /* perform */
429
430   return [self deleteInUIDs: removedUIDs];
431 }
432
433 - (NSException *) saveContentString: (NSString *) _iCalString
434 {
435   return [self saveContentString: _iCalString baseSequence: 0];
436 }
437
438 /* message type */
439
440 - (NSString *) outlookMessageClass
441 {
442   return @"IPM.Appointment";
443 }
444
445 @end /* SOGoAppointmentObject */