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>
32 #import "iCalEntityObject+Agenor.h"
36 #import "NSArray+Appointments.h"
38 @implementation SOGoAppointmentObject
44 return [self firstEventFromCalendar: [self calendar]];
48 - (NSArray *) attendeeUIDsFromAppointment: (iCalEvent *) _apt
50 AgenorUserManager *um;
54 NSString *email, *uid;
56 if (![_apt isNotNull])
59 if ((attendees = [_apt attendees]) == nil)
61 count = [attendees count];
62 uids = [NSMutableArray arrayWithCapacity:count + 1];
64 um = [AgenorUserManager sharedUserManager];
68 email = [[_apt organizer] rfc822Email];
69 if ([email isNotNull]) {
70 uid = [um getUIDForEmail:email];
71 if ([uid isNotNull]) {
75 [self logWithFormat:@"Note: got no uid for organizer: '%@'", email];
80 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 - (iCalEvent *) firstEventFromCalendar: (iCalCalendar *) aCalendar
194 events = [aCalendar childrenWithTag: @"vevent"];
196 event = (iCalEvent *) [[events objectAtIndex: 0]
197 groupWithClass: [iCalEvent class]];
204 /* "iCal multifolder saves" */
206 - (NSException *) saveContentString: (NSString *) _iCal
207 baseSequence: (int) _v
210 Note: we need to delete in all participants folders and send iMIP messages
211 for all external accounts.
214 - fetch stored content
216 - check if sequence matches (or if 0=ignore)
217 - extract old attendee list + organizer (make unique)
218 - parse new content (ensure that sequence is increased!)
219 - extract new attendee list + organizer (make unique)
220 - make a diff => new, same, removed
222 - delete in removed folders
223 - send iMIP mail for all folders not found
225 AgenorUserManager *um;
226 iCalCalendar *newCalendar;
227 iCalEvent *oldApt, *newApt;
228 iCalEventChanges *changes;
229 iCalPerson *organizer;
230 NSString *oldContent, *uid;
231 NSArray *uids, *props;
232 NSMutableArray *attendees, *storeUIDs, *removedUIDs;
233 NSException *storeError, *delError;
234 BOOL updateForcesReconsider;
236 updateForcesReconsider = NO;
238 if ([_iCal length] == 0) {
239 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
240 reason:@"got no iCalendar content to store!"];
243 um = [AgenorUserManager sharedUserManager];
245 /* handle old content */
247 oldContent = [self contentAsString]; /* if nil, this is a new appointment */
248 if ([oldContent length] == 0)
250 /* new appointment */
251 [self debugWithFormat:@"saving new appointment: %@", _iCal];
255 oldApt = [self firstEventFromCalendar: [self calendar]];
257 /* compare sequence if requested */
264 /* handle new content */
266 newCalendar = [iCalCalendar parseSingleFromSource: _iCal];
267 newApt = [self firstEventFromCalendar: newCalendar];
269 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
270 reason:@"could not parse iCalendar content!"];
275 changes = [iCalEventChanges changesFromEvent: oldApt
278 uids = [um getUIDsForICalPersons:[changes deletedAttendees]
279 applyStrictMapping:NO];
280 removedUIDs = [NSMutableArray arrayWithArray:uids];
282 uids = [um getUIDsForICalPersons:[newApt attendees]
283 applyStrictMapping:NO];
284 storeUIDs = [NSMutableArray arrayWithArray:uids];
285 props = [changes updatedProperties];
287 /* detect whether sequence has to be increased */
288 if ([changes hasChanges])
289 [newApt increaseSequence];
291 /* preserve organizer */
293 organizer = [newApt organizer];
294 uid = [um getUIDForICalPerson:organizer];
296 if (![storeUIDs containsObject:uid])
297 [storeUIDs addObject:uid];
298 [removedUIDs removeObject:uid];
301 /* organizer might have changed completely */
303 if (oldApt && ([props containsObject: @"organizer"])) {
304 uid = [um getUIDForICalPerson:[oldApt organizer]];
306 if (![storeUIDs containsObject:uid]) {
307 if (![removedUIDs containsObject:uid]) {
308 [removedUIDs addObject:uid];
314 [self debugWithFormat:@"UID ops:\n store: %@\n remove: %@",
315 storeUIDs, removedUIDs];
317 /* if time did change, all participants have to re-decide ...
318 * ... exception from that rule: the organizer
322 ([props containsObject:@"startDate"] ||
323 [props containsObject:@"endDate"] ||
324 [props containsObject:@"duration"]))
329 ps = [newApt attendees];
331 for (i = 0; i < count; i++) {
334 p = [ps objectAtIndex:i];
335 if (![p hasSameEmailAddress:organizer])
336 [p setParticipationStatus:iCalPersonPartStatNeedsAction];
338 _iCal = [[newApt parent] versitString];
339 updateForcesReconsider = YES;
342 /* perform storing */
344 storeError = [self saveContentString:_iCal inUIDs:storeUIDs];
345 delError = [self deleteInUIDs:removedUIDs];
347 // TODO: make compound
348 if (storeError != nil) return storeError;
349 if (delError != nil) return delError;
351 /* email notifications */
352 if ([self sendEMailNotifications])
354 attendees = [NSMutableArray arrayWithArray: [changes insertedAttendees]];
355 [attendees removePerson: organizer];
356 [self sendEMailUsingTemplateNamed: @"Invitation"
359 toAttendees: attendees];
361 if (updateForcesReconsider) {
362 attendees = [NSMutableArray arrayWithArray:[newApt attendees]];
363 [attendees removeObjectsInArray:[changes insertedAttendees]];
364 [attendees removePerson:organizer];
365 [self sendEMailUsingTemplateNamed: @"Update"
368 toAttendees: attendees];
371 attendees = [NSMutableArray arrayWithArray:[changes deletedAttendees]];
372 [attendees removePerson: organizer];
373 if ([attendees count])
375 iCalEvent *canceledApt;
377 canceledApt = [newApt copy];
378 [(iCalCalendar *) [canceledApt parent] setMethod: @"cancel"];
379 [self sendEMailUsingTemplateNamed: @"Removal"
381 andNewObject: canceledApt
382 toAttendees: attendees];
383 [canceledApt release];
390 - (NSException *)deleteWithBaseSequence:(int)_v {
392 Note: We need to delete in all participants folders and send iMIP messages
393 for all external accounts.
394 Delete is basically identical to save with all attendees and the
395 organizer being deleted.
398 - fetch stored content
400 - check if sequence matches (or if 0=ignore)
401 - extract old attendee list + organizer (make unique)
402 - delete in removed folders
403 - send iMIP mail for all folders not found
406 NSArray *removedUIDs;
407 NSMutableArray *attendees;
409 /* load existing content */
413 /* compare sequence if requested */
419 removedUIDs = [self attendeeUIDsFromAppointment:apt];
421 if ([self sendEMailNotifications])
423 /* send notification email to attendees excluding organizer */
424 attendees = [NSMutableArray arrayWithArray:[apt attendees]];
425 [attendees removePerson:[apt organizer]];
427 /* flag appointment as being canceled */
428 [(iCalCalendar *) [apt parent] setMethod: @"cancel"];
429 [apt increaseSequence];
431 /* remove all attendees to signal complete removal */
432 [apt removeAllAttendees];
434 /* send notification email */
435 [self sendEMailUsingTemplateNamed: @"Deletion"
438 toAttendees: attendees];
443 return [self deleteInUIDs:removedUIDs];
446 - (NSException *) saveContentString: (NSString *) _iCalString
448 return [self saveContentString: _iCalString baseSequence: 0];
451 - (NSException *) changeParticipationStatus: (NSString *) _status
456 NSString *newContent;
462 // TODO: do we need to use SOGoAppointment? (prefer iCalEvent?)
467 myEMail = [[_ctx activeUser] email];
468 p = [apt findParticipantWithEmail: myEMail];
471 // TODO: send iMIP reply mails?
473 [p setPartStat:_status];
474 newContent = [[apt parent] versitString];
477 ex = [self saveContentString:newContent];
479 // TODO: why is the exception wrapped?
481 ex = [NSException exceptionWithHTTPStatus: 500
482 reason: [ex reason]];
486 = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
487 reason: @"Could not generate iCalendar data ..."];
490 ex = [NSException exceptionWithHTTPStatus: 404 /* Not Found */
491 reason: @"user does not participate in this "
495 ex = [NSException exceptionWithHTTPStatus:500 /* Server Error */
496 reason:@"unable to parse appointment record"];
504 - (NSString *) outlookMessageClass
506 return @"IPM.Appointment";
509 - (NSException *) saveContentString: (NSString *) contentString
510 baseVersion: (unsigned int) baseVersion
512 NSString *newContentString, *oldContentString;
513 iCalCalendar *eventCalendar;
517 oldContentString = [self contentAsString];
518 if (oldContentString)
519 newContentString = contentString;
522 eventCalendar = [iCalCalendar parseSingleFromSource: contentString];
523 event = [self firstEventFromCalendar: eventCalendar];
524 organizers = [event childrenWithTag: @"organizer"];
525 if ([organizers count])
526 newContentString = contentString;
529 [event setOrganizerWithUid: [[self container] ownerInContext: nil]];
530 newContentString = [eventCalendar versitString];
534 return [super saveContentString: newContentString
535 baseVersion: baseVersion];
538 - (NSString *) roleOfUser: (NSString *) login
539 inContext: (WOContext *) context
541 AgenorUserManager *um;
543 NSString *role, *email;
545 um = [AgenorUserManager sharedUserManager];
546 email = [um getEmailForUID: login];
548 event = [self event];
549 if ([event isOrganizer: email])
551 else if ([event isParticipant: email])
552 role = @"Participant";
559 @end /* SOGoAppointmentObject */