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 "SOGoAppointmentObject.h"
24 #import <NGCards/iCalCalendar.h>
25 #import <NGCards/iCalEvent.h>
26 #import <NGCards/iCalEventChanges.h>
27 #import <NGCards/iCalPerson.h>
29 #import <SOGo/AgenorUserManager.h>
30 #import <SOGo/SOGoObject.h>
31 #import <SOGo/SOGoPermissions.h>
33 #import "iCalEntityObject+Agenor.h"
37 #import "NSArray+Appointments.h"
39 @implementation SOGoAppointmentObject
41 - (NSString *) componentTag
47 - (NSArray *) attendeeUIDsFromAppointment: (iCalEvent *) _apt
49 AgenorUserManager *um;
53 NSString *email, *uid;
55 if (![_apt isNotNull])
58 if ((attendees = [_apt attendees]) == nil)
60 count = [attendees count];
61 uids = [NSMutableArray arrayWithCapacity:count + 1];
63 um = [AgenorUserManager sharedUserManager];
67 email = [[_apt organizer] rfc822Email];
68 if ([email isNotNull]) {
69 uid = [um getUIDForEmail:email];
70 if ([uid isNotNull]) {
74 [self logWithFormat:@"Note: got no uid for organizer: '%@'", email];
79 for (i = 0; i < count; i++)
83 person = [attendees objectAtIndex:i];
84 email = [person rfc822Email];
85 if (![email isNotNull]) continue;
87 uid = [um getUIDForEmail:email];
88 if (![uid isNotNull]) {
89 [self logWithFormat:@"Note: got no uid for email: '%@'", email];
92 if (![uids containsObject:uid])
99 /* folder management */
101 - (id)lookupHomeFolderForUID:(NSString *)_uid inContext:(id)_ctx {
102 // TODO: what does this do? lookup the home of the organizer?
103 return [[self container] lookupHomeFolderForUID:_uid inContext:_ctx];
105 - (NSArray *)lookupCalendarFoldersForUIDs:(NSArray *)_uids inContext:(id)_ctx {
106 return [[self container] lookupCalendarFoldersForUIDs:_uids inContext:_ctx];
109 /* store in all the other folders */
111 - (NSException *)saveContentString:(NSString *)_iCal inUIDs:(NSArray *)_uids {
114 NSException *allErrors = nil;
117 ctx = [[WOApplication application] context];
119 e = [[self lookupCalendarFoldersForUIDs:_uids inContext:ctx]
121 while ((folder = [e nextObject]) != nil) {
123 SOGoAppointmentObject *apt;
125 if (![folder isNotNull]) /* no folder was found for given UID */
128 apt = [folder lookupName: [self nameInContainer] inContext:ctx
130 if ([apt isKindOfClass: [NSException class]])
132 [self logWithFormat:@"Note: an exception occured finding '%@' in folder: %@",
133 [self nameInContainer], folder];
134 [self logWithFormat:@"the exception reason was: %@",
135 [(NSException *) apt reason]];
139 if (![apt isNotNull]) {
140 [self logWithFormat:@"Note: did not find '%@' in folder: %@",
141 [self nameInContainer], folder];
144 if ([apt isKindOfClass: [NSException class]]) {
145 [self logWithFormat:@"Exception: %@", [(NSException *) apt reason]];
149 if ((error = [apt primarySaveContentString:_iCal]) != nil) {
150 [self logWithFormat:@"Note: failed to save iCal in folder: %@", folder];
151 // TODO: make compound
159 - (NSException *)deleteInUIDs:(NSArray *)_uids {
162 NSException *allErrors = nil;
165 ctx = [[WOApplication application] context];
167 e = [[self lookupCalendarFoldersForUIDs:_uids inContext:ctx]
169 while ((folder = [e nextObject])) {
171 SOGoAppointmentObject *apt;
173 apt = [folder lookupName:[self nameInContainer] inContext:ctx
175 if ([apt isKindOfClass: [NSException class]]) {
176 [self logWithFormat: @"%@", [(NSException *) apt reason]];
180 if ((error = [apt primaryDelete]) != nil) {
181 [self logWithFormat:@"Note: failed to delete in folder: %@", folder];
182 // TODO: make compound
189 /* "iCal multifolder saves" */
191 - (NSException *) saveContentString: (NSString *) _iCal
192 baseSequence: (int) _v
195 Note: we need to delete in all participants folders and send iMIP messages
196 for all external accounts.
199 - fetch stored content
201 - check if sequence matches (or if 0=ignore)
202 - extract old attendee list + organizer (make unique)
203 - parse new content (ensure that sequence is increased!)
204 - extract new attendee list + organizer (make unique)
205 - make a diff => new, same, removed
207 - delete in removed folders
208 - send iMIP mail for all folders not found
210 AgenorUserManager *um;
211 iCalCalendar *newCalendar;
212 iCalEvent *oldApt, *newApt;
213 iCalEventChanges *changes;
214 iCalPerson *organizer;
215 NSString *oldContent, *uid;
216 NSArray *uids, *props;
217 NSMutableArray *attendees, *storeUIDs, *removedUIDs;
218 NSException *storeError, *delError;
219 BOOL updateForcesReconsider;
221 updateForcesReconsider = NO;
223 if ([_iCal length] == 0) {
224 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
225 reason:@"got no iCalendar content to store!"];
228 um = [AgenorUserManager sharedUserManager];
230 /* handle old content */
232 oldContent = [self contentAsString]; /* if nil, this is a new appointment */
233 if ([oldContent length] == 0)
235 /* new appointment */
236 [self debugWithFormat:@"saving new appointment: %@", _iCal];
240 oldApt = (iCalEvent *) [self component];
242 /* compare sequence if requested */
249 /* handle new content */
251 newCalendar = [iCalCalendar parseSingleFromSource: _iCal];
252 newApt = (iCalEvent *) [newCalendar firstChildWithTag: [self componentTag]];
254 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
255 reason:@"could not parse iCalendar content!"];
260 changes = [iCalEventChanges changesFromEvent: oldApt
263 uids = [um getUIDsForICalPersons:[changes deletedAttendees]
264 applyStrictMapping:NO];
265 removedUIDs = [NSMutableArray arrayWithArray:uids];
267 uids = [um getUIDsForICalPersons:[newApt attendees]
268 applyStrictMapping:NO];
269 storeUIDs = [NSMutableArray arrayWithArray:uids];
270 props = [changes updatedProperties];
272 /* detect whether sequence has to be increased */
273 if ([changes hasChanges])
274 [newApt increaseSequence];
276 /* preserve organizer */
278 organizer = [newApt organizer];
279 uid = [um getUIDForICalPerson:organizer];
281 if (![storeUIDs containsObject:uid])
282 [storeUIDs addObject:uid];
283 [removedUIDs removeObject:uid];
286 /* organizer might have changed completely */
288 if (oldApt && ([props containsObject: @"organizer"])) {
289 uid = [um getUIDForICalPerson:[oldApt organizer]];
291 if (![storeUIDs containsObject:uid]) {
292 if (![removedUIDs containsObject:uid]) {
293 [removedUIDs addObject:uid];
299 [self debugWithFormat:@"UID ops:\n store: %@\n remove: %@",
300 storeUIDs, removedUIDs];
302 /* if time did change, all participants have to re-decide ...
303 * ... exception from that rule: the organizer
307 ([props containsObject:@"startDate"] ||
308 [props containsObject:@"endDate"] ||
309 [props containsObject:@"duration"]))
314 ps = [newApt attendees];
316 for (i = 0; i < count; i++) {
319 p = [ps objectAtIndex:i];
320 if (![p hasSameEmailAddress:organizer])
321 [p setParticipationStatus:iCalPersonPartStatNeedsAction];
323 _iCal = [[newApt parent] versitString];
324 updateForcesReconsider = YES;
327 /* perform storing */
329 storeError = [self saveContentString:_iCal inUIDs:storeUIDs];
330 delError = [self deleteInUIDs:removedUIDs];
332 // TODO: make compound
333 if (storeError != nil) return storeError;
334 if (delError != nil) return delError;
336 /* email notifications */
337 if ([self sendEMailNotifications])
339 attendees = [NSMutableArray arrayWithArray: [changes insertedAttendees]];
340 [attendees removePerson: organizer];
341 [self sendEMailUsingTemplateNamed: @"Invitation"
344 toAttendees: attendees];
346 if (updateForcesReconsider) {
347 attendees = [NSMutableArray arrayWithArray:[newApt attendees]];
348 [attendees removeObjectsInArray:[changes insertedAttendees]];
349 [attendees removePerson:organizer];
350 [self sendEMailUsingTemplateNamed: @"Update"
353 toAttendees: attendees];
356 attendees = [NSMutableArray arrayWithArray:[changes deletedAttendees]];
357 [attendees removePerson: organizer];
358 if ([attendees count])
360 iCalEvent *canceledApt;
362 canceledApt = [newApt copy];
363 [(iCalCalendar *) [canceledApt parent] setMethod: @"cancel"];
364 [self sendEMailUsingTemplateNamed: @"Removal"
366 andNewObject: canceledApt
367 toAttendees: attendees];
368 [canceledApt release];
375 - (NSException *)deleteWithBaseSequence:(int)_v {
377 Note: We need to delete in all participants folders and send iMIP messages
378 for all external accounts.
379 Delete is basically identical to save with all attendees and the
380 organizer being deleted.
383 - fetch stored content
385 - check if sequence matches (or if 0=ignore)
386 - extract old attendee list + organizer (make unique)
387 - delete in removed folders
388 - send iMIP mail for all folders not found
391 NSArray *removedUIDs;
392 NSMutableArray *attendees;
394 /* load existing content */
396 apt = (iCalEvent *) [self component];
398 /* compare sequence if requested */
404 removedUIDs = [self attendeeUIDsFromAppointment:apt];
406 if ([self sendEMailNotifications])
408 /* send notification email to attendees excluding organizer */
409 attendees = [NSMutableArray arrayWithArray:[apt attendees]];
410 [attendees removePerson:[apt organizer]];
412 /* flag appointment as being canceled */
413 [(iCalCalendar *) [apt parent] setMethod: @"cancel"];
414 [apt increaseSequence];
416 /* remove all attendees to signal complete removal */
417 [apt removeAllAttendees];
419 /* send notification email */
420 [self sendEMailUsingTemplateNamed: @"Deletion"
423 toAttendees: attendees];
428 return [self deleteInUIDs:removedUIDs];
431 - (NSException *) saveContentString: (NSString *) _iCalString
433 return [self saveContentString: _iCalString baseSequence: 0];
436 - (NSException *) changeParticipationStatus: (NSString *) _status
441 NSString *newContent;
447 // TODO: do we need to use SOGoAppointment? (prefer iCalEvent?)
448 apt = (iCalEvent *) [self component];
452 myEMail = [[_ctx activeUser] email];
453 p = [apt findParticipantWithEmail: myEMail];
456 // TODO: send iMIP reply mails?
458 [p setPartStat:_status];
459 newContent = [[apt parent] versitString];
462 ex = [self saveContentString:newContent];
464 // TODO: why is the exception wrapped?
466 ex = [NSException exceptionWithHTTPStatus: 500
467 reason: [ex reason]];
471 = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
472 reason: @"Could not generate iCalendar data ..."];
475 ex = [NSException exceptionWithHTTPStatus: 404 /* Not Found */
476 reason: @"user does not participate in this "
480 ex = [NSException exceptionWithHTTPStatus:500 /* Server Error */
481 reason:@"unable to parse appointment record"];
489 - (NSString *) outlookMessageClass
491 return @"IPM.Appointment";
494 - (NSException *) saveContentString: (NSString *) contentString
495 baseVersion: (unsigned int) baseVersion
497 NSString *newContentString, *oldContentString;
498 iCalCalendar *eventCalendar;
502 oldContentString = [self contentAsString];
503 if (oldContentString)
504 newContentString = contentString;
507 eventCalendar = [iCalCalendar parseSingleFromSource: contentString];
508 event = (iCalEvent *) [eventCalendar firstChildWithTag: [self componentTag]];
509 organizers = [event childrenWithTag: @"organizer"];
510 if ([organizers count])
511 newContentString = contentString;
514 [event setOrganizerWithUid: [[self container] ownerInContext: nil]];
515 newContentString = [eventCalendar versitString];
519 return [super saveContentString: newContentString
520 baseVersion: baseVersion];
523 @end /* SOGoAppointmentObject */