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