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