2 Copyright (C) 2004-2005 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
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
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.
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
22 #import <Foundation/NSCalendarDate.h>
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>
32 #import <SoObjects/SOGo/LDAPUserManager.h>
33 #import <SoObjects/SOGo/SOGoObject.h>
34 #import <SoObjects/SOGo/SOGoPermissions.h>
36 #import "NSArray+Appointments.h"
37 #import "SOGoAppointmentFolder.h"
39 #import "SOGoAppointmentObject.h"
41 @implementation SOGoAppointmentObject
43 - (NSString *) componentTag
49 - (NSArray *) attendeeUIDsFromAppointment: (iCalEvent *) _apt
55 NSString *email, *uid;
57 if (![_apt isNotNull])
60 if ((attendees = [_apt attendees]) == nil)
62 count = [attendees count];
63 uids = [NSMutableArray arrayWithCapacity:count + 1];
65 um = [LDAPUserManager sharedUserManager];
69 email = [[_apt organizer] rfc822Email];
70 if ([email isNotNull]) {
71 uid = [um getUIDForEmail: email];
72 if ([uid isNotNull]) {
76 [self logWithFormat:@"Note: got no uid for organizer: '%@'", email];
81 for (i = 0; i < count; i++)
85 person = [attendees objectAtIndex:i];
86 email = [person rfc822Email];
87 if (![email isNotNull]) continue;
89 uid = [um getUIDForEmail:email];
90 if (![uid isNotNull]) {
91 [self logWithFormat:@"Note: got no uid for email: '%@'", email];
94 if (![uids containsObject:uid])
101 /* store in all the other folders */
103 - (NSException *) saveContentString: (NSString *) _iCal
104 inUIDs: (NSArray *) _uids
108 NSException *allErrors = nil;
110 e = [[container lookupCalendarFoldersForUIDs:_uids inContext: context]
112 while ((folder = [e nextObject]) != nil) {
114 SOGoAppointmentObject *apt;
116 if (![folder isNotNull]) /* no folder was found for given UID */
119 apt = [folder lookupName: [self nameInContainer] inContext: context
121 if ([apt isKindOfClass: [NSException class]])
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]];
130 if (![apt isNotNull]) {
131 [self logWithFormat:@"Note: did not find '%@' in folder: %@",
132 [self nameInContainer], folder];
135 if ([apt isKindOfClass: [NSException class]]) {
136 [self logWithFormat:@"Exception: %@", [(NSException *) apt reason]];
140 if ((error = [apt primarySaveContentString:_iCal]) != nil) {
141 [self logWithFormat:@"Note: failed to save iCal in folder: %@", folder];
142 // TODO: make compound
150 - (NSException *) deleteInUIDs: (NSArray *) _uids
154 NSException *allErrors = nil;
156 SOGoAppointmentObject *apt;
158 e = [[container lookupCalendarFoldersForUIDs:_uids inContext: context]
160 while ((folder = [e nextObject]))
162 apt = [folder lookupName: [self nameInContainer]
165 if ([apt isKindOfClass: [NSException class]]) {
166 [self logWithFormat: @"%@", [(NSException *) apt reason]];
170 if ((error = [apt primaryDelete]) != nil) {
171 [self logWithFormat:@"Note: failed to delete in folder: %@", folder];
172 // TODO: make compound
180 /* "iCal multifolder saves" */
181 - (BOOL) _aptIsStillRelevant: (iCalEvent *) appointment
185 now = [NSCalendarDate calendarDate];
187 return ([[appointment endDate] earlierDate: now] == now);
190 - (NSException *) saveContentString: (NSString *) _iCal
191 baseSequence: (int) _v
194 Note: we need to delete in all participants folders and send iMIP messages
195 for all external accounts.
198 - fetch stored 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
206 - delete in removed folders
207 - send iMIP mail for all folders not found
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;
219 updateForcesReconsider = NO;
221 if ([_iCal length] == 0)
222 return [NSException exceptionWithHTTPStatus: 400 /* Bad Request */
223 reason: @"got no iCalendar content to store!"];
225 um = [LDAPUserManager sharedUserManager];
227 /* handle old content */
229 oldContent = [self contentAsString]; /* if nil, this is a new appointment */
230 if ([oldContent length] == 0)
232 /* new appointment */
233 [self debugWithFormat:@"saving new appointment: %@", _iCal];
237 oldApt = (iCalEvent *) [self component: NO];
239 /* compare sequence if requested */
245 /* handle new content */
247 newApt = (iCalEvent *) [self component: NO];
249 return [NSException exceptionWithHTTPStatus: 400 /* Bad Request */
250 reason: @"could not parse iCalendar content!"];
254 changes = [iCalEventChanges changesFromEvent: oldApt toEvent: newApt];
255 uids = [self getUIDsForICalPersons: [changes deletedAttendees]];
256 removedUIDs = [NSMutableArray arrayWithArray: uids];
258 uids = [self getUIDsForICalPersons: [newApt attendees]];
259 storeUIDs = [NSMutableArray arrayWithArray: uids];
260 props = [changes updatedProperties];
262 /* detect whether sequence has to be increased */
263 if ([changes hasChanges])
264 [newApt increaseSequence];
266 /* preserve organizer */
268 organizer = [newApt organizer];
269 uid = [self getUIDForICalPerson: organizer];
271 uid = [self ownerInContext: nil];
273 if (![storeUIDs containsObject:uid])
274 [storeUIDs addObject:uid];
275 [removedUIDs removeObject:uid];
278 /* organizer might have changed completely */
280 if (oldApt && ([props containsObject: @"organizer"])) {
281 uid = [self getUIDForICalPerson:[oldApt organizer]];
283 if (![storeUIDs containsObject:uid]) {
284 if (![removedUIDs containsObject:uid]) {
285 [removedUIDs addObject:uid];
291 [self debugWithFormat:@"UID ops:\n store: %@\n remove: %@",
292 storeUIDs, removedUIDs];
294 /* if time did change, all participants have to re-decide ...
295 * ... exception from that rule: the organizer
299 ([props containsObject: @"startDate"] ||
300 [props containsObject: @"endDate"] ||
301 [props containsObject: @"duration"]))
306 ps = [newApt attendees];
308 for (i = 0; i < count; i++) {
311 p = [ps objectAtIndex:i];
312 if (![p hasSameEmailAddress:organizer])
313 [p setParticipationStatus:iCalPersonPartStatNeedsAction];
315 _iCal = [[newApt parent] versitString];
316 updateForcesReconsider = YES;
319 /* perform storing */
321 storeError = [self saveContentString: _iCal inUIDs: storeUIDs];
322 delError = [self deleteInUIDs: removedUIDs];
324 // TODO: make compound
325 if (storeError != nil) return storeError;
326 if (delError != nil) return delError;
328 /* email notifications */
329 if ([self sendEMailNotifications]
330 && [self _aptIsStillRelevant: newApt])
333 = [NSMutableArray arrayWithArray: [changes insertedAttendees]];
334 [attendees removePerson: organizer];
335 [self sendEMailUsingTemplateNamed: @"Invitation"
338 toAttendees: attendees];
340 if (updateForcesReconsider) {
341 attendees = [NSMutableArray arrayWithArray:[newApt attendees]];
342 [attendees removeObjectsInArray:[changes insertedAttendees]];
343 [attendees removePerson:organizer];
344 [self sendEMailUsingTemplateNamed: @"Update"
347 toAttendees: attendees];
351 = [NSMutableArray arrayWithArray: [changes deletedAttendees]];
352 [attendees removePerson: organizer];
353 if ([attendees count])
355 iCalEvent *cancelledApt;
357 cancelledApt = [newApt copy];
358 [(iCalCalendar *) [cancelledApt parent] setMethod: @"cancel"];
359 [self sendEMailUsingTemplateNamed: @"Removal"
361 andNewObject: cancelledApt
362 toAttendees: attendees];
363 [cancelledApt release];
370 - (NSException *)deleteWithBaseSequence:(int)_v {
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.
378 - fetch stored 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
386 NSMutableArray *attendees, *removedUIDs;
388 /* load existing content */
390 apt = (iCalEvent *) [self component: NO];
392 /* compare sequence if requested */
398 removedUIDs = [NSMutableArray arrayWithArray:
399 [self attendeeUIDsFromAppointment: apt]];
400 if (![removedUIDs containsObject: owner])
401 [removedUIDs addObject: owner];
403 if ([self sendEMailNotifications]
404 && [self _aptIsStillRelevant: apt])
406 /* send notification email to attendees excluding organizer */
407 attendees = [NSMutableArray arrayWithArray:[apt attendees]];
408 [attendees removePerson:[apt organizer]];
410 /* flag appointment as being cancelled */
411 [(iCalCalendar *) [apt parent] setMethod: @"cancel"];
412 [apt increaseSequence];
414 /* remove all attendees to signal complete removal */
415 [apt removeAllAttendees];
417 /* send notification email */
418 [self sendEMailUsingTemplateNamed: @"Deletion"
421 toAttendees: attendees];
426 return [self deleteInUIDs: removedUIDs];
429 - (NSException *) saveContentString: (NSString *) _iCalString
431 return [self saveContentString: _iCalString baseSequence: 0];
436 - (NSString *) outlookMessageClass
438 return @"IPM.Appointment";
441 @end /* SOGoAppointmentObject */