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 SOGoAppointmentObject *apt;
112 e = [[container lookupCalendarFoldersForUIDs:_uids inContext: context]
114 while ((folder = [e nextObject]))
116 apt = [SOGoAppointmentObject objectWithName: nameInContainer
117 inContainer: folder];
118 error = [apt primarySaveContentString:_iCal];
121 [self logWithFormat:@"Note: failed to save iCal in folder: %@", folder];
122 // TODO: make compound
130 - (NSException *) deleteInUIDs: (NSArray *) _uids
134 NSException *allErrors = nil;
136 SOGoAppointmentObject *apt;
138 e = [[container lookupCalendarFoldersForUIDs:_uids inContext: context]
140 while ((folder = [e nextObject]))
142 apt = [folder lookupName: [self nameInContainer]
145 if ([apt isKindOfClass: [NSException class]]) {
146 [self logWithFormat: @"%@", [(NSException *) apt reason]];
150 if ((error = [apt primaryDelete]) != nil) {
151 [self logWithFormat:@"Note: failed to delete in folder: %@", folder];
152 // TODO: make compound
160 /* "iCal multifolder saves" */
161 - (BOOL) _aptIsStillRelevant: (iCalEvent *) appointment
165 now = [NSCalendarDate calendarDate];
167 return ([[appointment endDate] earlierDate: now] == now);
170 - (NSException *) saveContentString: (NSString *) _iCal
171 baseSequence: (int) _v
174 Note: we need to delete in all participants folders and send iMIP messages
175 for all external accounts.
178 - fetch stored 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
186 - delete in removed folders
187 - send iMIP mail for all folders not found
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;
199 updateForcesReconsider = NO;
201 if ([_iCal length] == 0)
202 return [NSException exceptionWithHTTPStatus: 400 /* Bad Request */
203 reason: @"got no iCalendar content to store!"];
205 um = [LDAPUserManager sharedUserManager];
207 /* handle old content */
209 oldContent = [self contentAsString]; /* if nil, this is a new appointment */
210 if ([oldContent length] == 0)
212 /* new appointment */
213 [self debugWithFormat:@"saving new appointment: %@", _iCal];
217 oldApt = (iCalEvent *) [self component: NO];
219 /* compare sequence if requested */
225 /* handle new content */
227 newApt = (iCalEvent *) [self component: NO];
229 return [NSException exceptionWithHTTPStatus: 400 /* Bad Request */
230 reason: @"could not parse iCalendar content!"];
234 changes = [iCalEventChanges changesFromEvent: oldApt toEvent: newApt];
235 uids = [self getUIDsForICalPersons: [changes deletedAttendees]];
236 removedUIDs = [NSMutableArray arrayWithArray: uids];
238 uids = [self getUIDsForICalPersons: [newApt attendees]];
239 storeUIDs = [NSMutableArray arrayWithArray: uids];
240 props = [changes updatedProperties];
242 /* detect whether sequence has to be increased */
243 if ([changes hasChanges])
244 [newApt increaseSequence];
246 /* preserve organizer */
248 organizer = [newApt organizer];
249 uid = [self getUIDForICalPerson: organizer];
251 uid = [self ownerInContext: nil];
253 if (![storeUIDs containsObject:uid])
254 [storeUIDs addObject:uid];
255 [removedUIDs removeObject:uid];
258 /* organizer might have changed completely */
260 if (oldApt && ([props containsObject: @"organizer"])) {
261 uid = [self getUIDForICalPerson:[oldApt organizer]];
263 if (![storeUIDs containsObject:uid]) {
264 if (![removedUIDs containsObject:uid]) {
265 [removedUIDs addObject:uid];
271 [self debugWithFormat:@"UID ops:\n store: %@\n remove: %@",
272 storeUIDs, removedUIDs];
274 /* if time did change, all participants have to re-decide ...
275 * ... exception from that rule: the organizer
279 ([props containsObject: @"startDate"] ||
280 [props containsObject: @"endDate"] ||
281 [props containsObject: @"duration"]))
286 ps = [newApt attendees];
288 for (i = 0; i < count; i++) {
291 p = [ps objectAtIndex:i];
292 if (![p hasSameEmailAddress:organizer])
293 [p setParticipationStatus:iCalPersonPartStatNeedsAction];
295 _iCal = [[newApt parent] versitString];
296 updateForcesReconsider = YES;
299 /* perform storing */
301 storeError = [self saveContentString: _iCal inUIDs: storeUIDs];
302 delError = [self deleteInUIDs: removedUIDs];
304 // TODO: make compound
305 if (storeError != nil) return storeError;
306 if (delError != nil) return delError;
308 /* email notifications */
309 if ([self sendEMailNotifications]
310 && [self _aptIsStillRelevant: newApt])
313 = [NSMutableArray arrayWithArray: [changes insertedAttendees]];
314 [attendees removePerson: organizer];
315 [self sendEMailUsingTemplateNamed: @"Invitation"
318 toAttendees: attendees];
320 if (updateForcesReconsider) {
321 attendees = [NSMutableArray arrayWithArray:[newApt attendees]];
322 [attendees removeObjectsInArray:[changes insertedAttendees]];
323 [attendees removePerson:organizer];
324 [self sendEMailUsingTemplateNamed: @"Update"
327 toAttendees: attendees];
331 = [NSMutableArray arrayWithArray: [changes deletedAttendees]];
332 [attendees removePerson: organizer];
333 if ([attendees count])
335 iCalEvent *cancelledApt;
337 cancelledApt = [newApt copy];
338 [(iCalCalendar *) [cancelledApt parent] setMethod: @"cancel"];
339 [self sendEMailUsingTemplateNamed: @"Removal"
341 andNewObject: cancelledApt
342 toAttendees: attendees];
343 [cancelledApt release];
350 - (NSException *)deleteWithBaseSequence:(int)_v {
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.
358 - fetch stored 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
366 NSMutableArray *attendees, *removedUIDs;
368 /* load existing content */
370 apt = (iCalEvent *) [self component: NO];
372 /* compare sequence if requested */
378 removedUIDs = [NSMutableArray arrayWithArray:
379 [self attendeeUIDsFromAppointment: apt]];
380 if (![removedUIDs containsObject: owner])
381 [removedUIDs addObject: owner];
383 if ([self sendEMailNotifications]
384 && [self _aptIsStillRelevant: apt])
386 /* send notification email to attendees excluding organizer */
387 attendees = [NSMutableArray arrayWithArray:[apt attendees]];
388 [attendees removePerson:[apt organizer]];
390 /* flag appointment as being cancelled */
391 [(iCalCalendar *) [apt parent] setMethod: @"cancel"];
392 [apt increaseSequence];
394 /* remove all attendees to signal complete removal */
395 [apt removeAllAttendees];
397 /* send notification email */
398 [self sendEMailUsingTemplateNamed: @"Deletion"
401 toAttendees: attendees];
406 return [self deleteInUIDs: removedUIDs];
409 - (NSException *) saveContentString: (NSString *) _iCalString
411 return [self saveContentString: _iCalString baseSequence: 0];
416 - (NSString *) outlookMessageClass
418 return @"IPM.Appointment";
421 @end /* SOGoAppointmentObject */