]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Appointments/SOGoAppointmentObject.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1139 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 <NGObjWeb/NSException+HTTP.h>
23 #import <NGExtensions/NSNull+misc.h>
24 #import <NGExtensions/NSObject+Logs.h>
25 #import <NGCards/iCalCalendar.h>
26 #import <NGCards/iCalEvent.h>
27 #import <NGCards/iCalEventChanges.h>
28 #import <NGCards/iCalPerson.h>
29
30 #import <SoObjects/SOGo/LDAPUserManager.h>
31 #import <SoObjects/SOGo/SOGoObject.h>
32 #import <SoObjects/SOGo/SOGoPermissions.h>
33
34 #import "NSArray+Appointments.h"
35 #import "SOGoAppointmentObject.h"
36
37 @implementation SOGoAppointmentObject
38
39 - (NSString *) componentTag
40 {
41   return @"vevent";
42 }
43
44 /* iCal handling */
45 - (NSArray *) attendeeUIDsFromAppointment: (iCalEvent *) _apt
46 {
47   LDAPUserManager *um;
48   NSMutableArray *uids;
49   NSArray *attendees;
50   unsigned i, count;
51   NSString *email, *uid;
52   
53   if (![_apt isNotNull])
54     return nil;
55   
56   if ((attendees = [_apt attendees]) == nil)
57     return nil;
58   count = [attendees count];
59   uids = [NSMutableArray arrayWithCapacity:count + 1];
60   
61   um = [LDAPUserManager sharedUserManager];
62   
63   /* add organizer */
64   
65   email = [[_apt organizer] rfc822Email];
66   if ([email isNotNull]) {
67     uid = [um getUIDForEmail: email];
68     if ([uid isNotNull]) {
69       [uids addObject:uid];
70     }
71     else
72       [self logWithFormat:@"Note: got no uid for organizer: '%@'", email];
73   }
74
75   /* add attendees */
76   
77   for (i = 0; i < count; i++)
78     {
79       iCalPerson *person;
80     
81       person = [attendees objectAtIndex:i];
82       email  = [person rfc822Email];
83       if (![email isNotNull]) continue;
84     
85       uid = [um getUIDForEmail:email];
86       if (![uid isNotNull]) {
87         [self logWithFormat:@"Note: got no uid for email: '%@'", email];
88         continue;
89       }
90       if (![uids containsObject:uid])
91         [uids addObject:uid];
92     }
93
94   return uids;
95 }
96
97 /* folder management */
98
99 - (id)lookupHomeFolderForUID:(NSString *)_uid inContext:(id)_ctx {
100   // TODO: what does this do? lookup the home of the organizer?
101   return [[self container] lookupHomeFolderForUID:_uid inContext:_ctx];
102 }
103 - (NSArray *)lookupCalendarFoldersForUIDs:(NSArray *)_uids inContext:(id)_ctx {
104   return [[self container] lookupCalendarFoldersForUIDs:_uids inContext:_ctx];
105 }
106
107 /* store in all the other folders */
108
109 - (NSException *)saveContentString:(NSString *)_iCal inUIDs:(NSArray *)_uids {
110   NSEnumerator *e;
111   id folder;
112   NSException *allErrors = nil;
113   
114   e = [[self lookupCalendarFoldersForUIDs:_uids inContext: context]
115              objectEnumerator];
116   while ((folder = [e nextObject]) != nil) {
117     NSException           *error;
118     SOGoAppointmentObject *apt;
119     
120     if (![folder isNotNull]) /* no folder was found for given UID */
121       continue;
122
123     apt = [folder lookupName: [self nameInContainer] inContext: context
124                   acquire: NO];
125     if ([apt isKindOfClass: [NSException class]])
126       {
127         [self logWithFormat:@"Note: an exception occured finding '%@' in folder: %@",
128               [self nameInContainer], folder];
129         [self logWithFormat:@"the exception reason was: %@",
130               [(NSException *) apt reason]];
131         continue;
132       }
133
134     if (![apt isNotNull]) {
135       [self logWithFormat:@"Note: did not find '%@' in folder: %@",
136               [self nameInContainer], folder];
137       continue;
138     }
139     if ([apt isKindOfClass: [NSException class]]) {
140       [self logWithFormat:@"Exception: %@", [(NSException *) apt reason]];
141       continue;
142     }
143     
144     if ((error = [apt primarySaveContentString:_iCal]) != nil) {
145       [self logWithFormat:@"Note: failed to save iCal in folder: %@", folder];
146       // TODO: make compound
147       allErrors = error;
148     }
149   }
150
151   return allErrors;
152 }
153
154 - (NSException *)deleteInUIDs:(NSArray *)_uids {
155   NSEnumerator *e;
156   id folder;
157   NSException *allErrors = nil;
158   
159   e = [[self lookupCalendarFoldersForUIDs:_uids inContext: context]
160              objectEnumerator];
161   while ((folder = [e nextObject])) {
162     NSException           *error;
163     SOGoAppointmentObject *apt;
164     
165     apt = [folder lookupName:[self nameInContainer] inContext: context
166                   acquire:NO];
167     if ([apt isKindOfClass: [NSException class]]) {
168       [self logWithFormat: @"%@", [(NSException *) apt reason]];
169       continue;
170     }
171     
172     if ((error = [apt primaryDelete]) != nil) {
173       [self logWithFormat:@"Note: failed to delete in folder: %@", folder];
174       // TODO: make compound
175       allErrors = error;
176     }
177   }
178   return allErrors;
179 }
180
181 /* "iCal multifolder saves" */
182
183 - (NSException *) saveContentString: (NSString *) _iCal
184                        baseSequence: (int) _v
185 {
186   /* 
187      Note: we need to delete in all participants folders and send iMIP messages
188            for all external accounts.
189      
190      Steps:
191      - fetch stored content
192      - parse old content
193      - check if sequence matches (or if 0=ignore)
194      - extract old attendee list + organizer (make unique)
195      - parse new content (ensure that sequence is increased!)
196      - extract new attendee list + organizer (make unique)
197      - make a diff => new, same, removed
198      - write to new, same
199      - delete in removed folders
200      - send iMIP mail for all folders not found
201   */
202   LDAPUserManager *um;
203   iCalEvent *oldApt, *newApt;
204   iCalEventChanges *changes;
205   iCalPerson *organizer;
206   NSString *oldContent, *uid;
207   NSArray *uids, *props;
208   NSMutableArray *attendees, *storeUIDs, *removedUIDs;
209   NSException *storeError, *delError;
210   BOOL updateForcesReconsider;
211   
212   updateForcesReconsider = NO;
213
214   if ([_iCal length] == 0)
215     return [NSException exceptionWithHTTPStatus: 400 /* Bad Request */
216                         reason: @"got no iCalendar content to store!"];
217
218   um = [LDAPUserManager sharedUserManager];
219
220   /* handle old content */
221   
222   oldContent = [self contentAsString]; /* if nil, this is a new appointment */
223   if ([oldContent length] == 0)
224     {
225     /* new appointment */
226       [self debugWithFormat:@"saving new appointment: %@", _iCal];
227       oldApt = nil;
228     }
229   else
230     oldApt = (iCalEvent *) [self component: NO];
231   
232   /* compare sequence if requested */
233
234   if (_v != 0) {
235     // TODO
236   }
237   
238   /* handle new content */
239   
240   newApt = (iCalEvent *) [self component: NO];
241   if (!newApt)
242     return [NSException exceptionWithHTTPStatus: 400 /* Bad Request */
243                         reason: @"could not parse iCalendar content!"];
244
245   /* diff */
246   
247   changes = [iCalEventChanges changesFromEvent: oldApt toEvent: newApt];
248   uids = [self getUIDsForICalPersons: [changes deletedAttendees]];
249   removedUIDs = [NSMutableArray arrayWithArray: uids];
250
251   uids = [self getUIDsForICalPersons: [newApt attendees]];
252   storeUIDs = [NSMutableArray arrayWithArray: uids];
253   props = [changes updatedProperties];
254
255   /* detect whether sequence has to be increased */
256   if ([changes hasChanges])
257     [newApt increaseSequence];
258
259   /* preserve organizer */
260
261   organizer = [newApt organizer];
262   uid = [self getUIDForICalPerson: organizer];
263   if (!uid)
264     uid = [self ownerInContext: nil];
265   if (uid) {
266     if (![storeUIDs containsObject:uid])
267       [storeUIDs addObject:uid];
268     [removedUIDs removeObject:uid];
269   }
270
271   /* organizer might have changed completely */
272
273   if (oldApt && ([props containsObject: @"organizer"])) {
274     uid = [self getUIDForICalPerson:[oldApt organizer]];
275     if (uid) {
276       if (![storeUIDs containsObject:uid]) {
277         if (![removedUIDs containsObject:uid]) {
278           [removedUIDs addObject:uid];
279         }
280       }
281     }
282   }
283
284   [self debugWithFormat:@"UID ops:\n  store: %@\n  remove: %@",
285                         storeUIDs, removedUIDs];
286
287   /* if time did change, all participants have to re-decide ...
288    * ... exception from that rule: the organizer
289    */
290
291   if (oldApt != nil &&
292       ([props containsObject: @"startDate"] ||
293        [props containsObject: @"endDate"]   ||
294        [props containsObject: @"duration"]))
295   {
296     NSArray  *ps;
297     unsigned i, count;
298     
299     ps    = [newApt attendees];
300     count = [ps count];
301     for (i = 0; i < count; i++) {
302       iCalPerson *p;
303       
304       p = [ps objectAtIndex:i];
305       if (![p hasSameEmailAddress:organizer])
306         [p setParticipationStatus:iCalPersonPartStatNeedsAction];
307     }
308     _iCal = [[newApt parent] versitString];
309     updateForcesReconsider = YES;
310   }
311
312   /* perform storing */
313
314   storeError = [self saveContentString: _iCal inUIDs: storeUIDs];
315   delError = [self deleteInUIDs: removedUIDs];
316
317   // TODO: make compound
318   if (storeError != nil) return storeError;
319   if (delError   != nil) return delError;
320
321   /* email notifications */
322   if ([self sendEMailNotifications])
323     {
324       attendees = [NSMutableArray arrayWithArray: [changes insertedAttendees]];
325       [attendees removePerson: organizer];
326       [self sendEMailUsingTemplateNamed: @"Invitation"
327             forOldObject: nil
328             andNewObject: newApt
329             toAttendees: attendees];
330
331       if (updateForcesReconsider) {
332         attendees = [NSMutableArray arrayWithArray:[newApt attendees]];
333         [attendees removeObjectsInArray:[changes insertedAttendees]];
334         [attendees removePerson:organizer];
335         [self sendEMailUsingTemplateNamed: @"Update"
336               forOldObject: oldApt
337               andNewObject: newApt
338               toAttendees: attendees];
339       }
340
341       attendees = [NSMutableArray arrayWithArray:[changes deletedAttendees]];
342       [attendees removePerson: organizer];
343       if ([attendees count])
344         {
345           iCalEvent *canceledApt;
346     
347           canceledApt = [newApt copy];
348           [(iCalCalendar *) [canceledApt parent] setMethod: @"cancel"];
349           [self sendEMailUsingTemplateNamed: @"Removal"
350                 forOldObject: nil
351                 andNewObject: canceledApt
352                 toAttendees: attendees];
353           [canceledApt release];
354         }
355     }
356
357   return nil;
358 }
359
360 - (NSException *)deleteWithBaseSequence:(int)_v {
361   /* 
362      Note: We need to delete in all participants folders and send iMIP messages
363            for all external accounts.
364            Delete is basically identical to save with all attendees and the
365            organizer being deleted.
366
367      Steps:
368      - fetch stored content
369      - parse old content
370      - check if sequence matches (or if 0=ignore)
371      - extract old attendee list + organizer (make unique)
372      - delete in removed folders
373      - send iMIP mail for all folders not found
374   */
375   iCalEvent *apt;
376   NSMutableArray *attendees, *removedUIDs;
377
378   /* load existing content */
379
380   apt = (iCalEvent *) [self component: NO];
381   
382   /* compare sequence if requested */
383
384 //   if (_v != 0) {
385 //     // TODO
386 //   }
387   
388   removedUIDs = [NSMutableArray arrayWithArray:
389                                   [self attendeeUIDsFromAppointment: apt]];
390   if (![removedUIDs containsObject: owner])
391     [removedUIDs addObject: owner];
392
393   if ([self sendEMailNotifications])
394     {
395       /* send notification email to attendees excluding organizer */
396       attendees = [NSMutableArray arrayWithArray:[apt attendees]];
397       [attendees removePerson:[apt organizer]];
398   
399       /* flag appointment as being canceled */
400       [(iCalCalendar *) [apt parent] setMethod: @"cancel"];
401       [apt increaseSequence];
402
403       /* remove all attendees to signal complete removal */
404       [apt removeAllAttendees];
405
406       /* send notification email */
407       [self sendEMailUsingTemplateNamed: @"Deletion"
408             forOldObject: nil
409             andNewObject: apt
410             toAttendees: attendees];
411     }
412
413   /* perform */
414
415   return [self deleteInUIDs: removedUIDs];
416 }
417
418 - (NSException *) saveContentString: (NSString *) _iCalString
419 {
420   return [self saveContentString: _iCalString baseSequence: 0];
421 }
422
423 /* message type */
424
425 - (NSString *) outlookMessageClass
426 {
427   return @"IPM.Appointment";
428 }
429
430 @end /* SOGoAppointmentObject */