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 "SOGoTaskObject.h"
24 #import <NGCards/iCalCalendar.h>
25 #import <NGCards/iCalToDo.h>
26 #import <NGCards/iCalEventChanges.h>
27 #import <NGCards/iCalPerson.h>
28 #import <SOGo/AgenorUserManager.h>
29 #import <NGMime/NGMime.h>
30 #import <NGMail/NGMail.h>
31 #import <NGMail/NGSendMail.h>
32 #import "SOGoAptMailNotification.h"
35 #import "NSArray+Appointments.h"
37 @interface SOGoTaskObject (PrivateAPI)
39 - (NSString *) homePageURLForPerson: (iCalPerson *) _person;
43 @implementation SOGoTaskObject
45 static NSString *mailTemplateDefaultLanguage = nil;
49 static BOOL didInit = NO;
54 ud = [NSUserDefaults standardUserDefaults];
55 mailTemplateDefaultLanguage = [[ud stringForKey:@"SOGoDefaultLanguage"]
57 if (!mailTemplateDefaultLanguage)
58 mailTemplateDefaultLanguage = @"French";
65 return [self firstTaskFromCalendar: [self calendar]];
70 - (NSArray *)attendeeUIDsFromTask:(iCalToDo *)_task {
71 AgenorUserManager *um;
75 NSString *email, *uid;
77 if (![_task isNotNull])
80 if ((attendees = [_task attendees]) == nil)
82 count = [attendees count];
83 uids = [NSMutableArray arrayWithCapacity:count + 1];
85 um = [AgenorUserManager sharedUserManager];
89 email = [[_task organizer] rfc822Email];
90 if ([email isNotNull]) {
91 uid = [um getUIDForEmail:email];
92 if ([uid isNotNull]) {
96 [self logWithFormat:@"Note: got no uid for organizer: '%@'", email];
101 for (i = 0; i < count; i++) {
104 person = [attendees objectAtIndex:i];
105 email = [person rfc822Email];
106 if (![email isNotNull]) continue;
108 uid = [um getUIDForEmail:email];
109 if (![uid isNotNull]) {
110 [self logWithFormat:@"Note: got no uid for email: '%@'", email];
113 if (![uids containsObject:uid])
114 [uids addObject:uid];
120 /* folder management */
122 - (id)lookupHomeFolderForUID:(NSString *)_uid inContext:(id)_ctx {
123 // TODO: what does this do? lookup the home of the organizer?
124 return [[self container] lookupHomeFolderForUID:_uid inContext:_ctx];
126 - (NSArray *)lookupCalendarFoldersForUIDs:(NSArray *)_uids inContext:(id)_ctx {
127 return [[self container] lookupCalendarFoldersForUIDs:_uids inContext:_ctx];
130 /* store in all the other folders */
132 - (NSException *)saveContentString:(NSString *)_iCal inUIDs:(NSArray *)_uids {
135 NSException *allErrors = nil;
138 ctx = [[WOApplication application] context];
140 e = [[self lookupCalendarFoldersForUIDs:_uids inContext:ctx]
142 while ((folder = [e nextObject]) != nil) {
144 SOGoTaskObject *task;
146 if (![folder isNotNull]) /* no folder was found for given UID */
149 task = [folder lookupName:[self nameInContainer] inContext:ctx
151 if ([task isKindOfClass: [NSException class]])
153 [self logWithFormat:@"Note: an exception occured finding '%@' in folder: %@",
154 [self nameInContainer], folder];
155 [self logWithFormat:@"the exception reason was: %@",
156 [(NSException *) task reason]];
160 if (![task isNotNull]) {
161 [self logWithFormat:@"Note: did not find '%@' in folder: %@",
162 [self nameInContainer], folder];
166 if ((error = [task primarySaveContentString:_iCal]) != nil) {
167 [self logWithFormat:@"Note: failed to save iCal in folder: %@", folder];
168 // TODO: make compound
174 - (NSException *)deleteInUIDs:(NSArray *)_uids {
177 NSException *allErrors = nil;
180 ctx = [[WOApplication application] context];
182 e = [[self lookupCalendarFoldersForUIDs:_uids inContext:ctx]
184 while ((folder = [e nextObject])) {
186 SOGoTaskObject *task;
188 task = [folder lookupName:[self nameInContainer] inContext:ctx
190 if (![task isNotNull]) {
191 [self logWithFormat:@"Note: did not find '%@' in folder: %@",
192 [self nameInContainer], folder];
195 if ([task isKindOfClass: [NSException class]]) {
196 [self logWithFormat:@"Exception: %@", [(NSException *) task reason]];
200 if ((error = [task primaryDelete]) != nil) {
201 [self logWithFormat:@"Note: failed to delete in folder: %@", folder];
202 // TODO: make compound
209 - (iCalToDo *) firstTaskFromCalendar: (iCalCalendar *) aCalendar
214 tasks = [aCalendar childrenWithTag: @"vtodo"];
216 task = (iCalToDo *) [[tasks objectAtIndex: 0]
217 groupWithClass: [iCalToDo class]];
224 /* "iCal multifolder saves" */
226 - (NSException *) saveContentString: (NSString *) _iCal
227 baseSequence: (int) _v
230 Note: we need to delete in all participants folders and send iMIP messages
231 for all external accounts.
234 - fetch stored content
236 - check if sequence matches (or if 0=ignore)
237 - extract old attendee list + organizer (make unique)
238 - parse new content (ensure that sequence is increased!)
239 - extract new attendee list + organizer (make unique)
240 - make a diff => new, same, removed
242 - delete in removed folders
243 - send iMIP mail for all folders not found
245 // AgenorUserManager *um;
246 // iCalCalendar *calendar;
247 // iCalToDo *oldApt, *newApt;
248 // // iCalToDoChanges *changes;
249 // iCalPerson *organizer;
250 // NSString *oldContent, *uid;
251 // NSArray *uids, *props;
252 // NSMutableArray *attendees, *storeUIDs, *removedUIDs;
253 NSException *storeError, *delError;
254 // BOOL updateForcesReconsider;
256 // updateForcesReconsider = NO;
258 // if ([_iCal length] == 0) {
259 // return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
260 // reason:@"got no iCalendar content to store!"];
263 // um = [AgenorUserManager sharedUserManager];
265 // /* handle old content */
267 // oldContent = [self contentAsString]; /* if nil, this is a new task */
268 // if ([oldContent length] == 0)
271 // [self debugWithFormat:@"saving new task: %@", _iCal];
276 // calendar = [iCalCalendar parseSingleFromSource: oldContent];
277 // oldApt = [self firstTaskFromCalendar: calendar];
280 // /* compare sequence if requested */
287 // /* handle new content */
289 // calendar = [iCalCalendar parseSingleFromSource: _iCal];
290 // newApt = [self firstTaskFromCalendar: calendar];
291 // if (newApt == nil) {
292 // return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
293 // reason:@"could not parse iCalendar content!"];
298 // changes = [iCalToDoChanges changesFromEvent: oldApt
301 // uids = [um getUIDsForICalPersons:[changes deletedAttendees]
302 // applyStrictMapping:NO];
303 // removedUIDs = [NSMutableArray arrayWithArray:uids];
305 // uids = [um getUIDsForICalPersons:[newApt attendees]
306 // applyStrictMapping:NO];
307 // storeUIDs = [NSMutableArray arrayWithArray:uids];
308 // props = [changes updatedProperties];
310 // /* detect whether sequence has to be increased */
311 // if ([changes hasChanges])
312 // [newApt increaseSequence];
314 // /* preserve organizer */
316 // organizer = [newApt organizer];
317 // uid = [um getUIDForICalPerson:organizer];
319 // if (![storeUIDs containsObject:uid])
320 // [storeUIDs addObject:uid];
321 // [removedUIDs removeObject:uid];
324 // /* organizer might have changed completely */
326 // if (oldApt && ([props containsObject: @"organizer"])) {
327 // uid = [um getUIDForICalPerson:[oldApt organizer]];
329 // if (![storeUIDs containsObject:uid]) {
330 // if (![removedUIDs containsObject:uid]) {
331 // [removedUIDs addObject:uid];
337 // [self debugWithFormat:@"UID ops:\n store: %@\n remove: %@",
338 // storeUIDs, removedUIDs];
340 // /* if time did change, all participants have to re-decide ...
341 // * ... exception from that rule: the organizer
344 // if (oldApt != nil &&
345 // ([props containsObject:@"startDate"] ||
346 // [props containsObject:@"endDate"] ||
347 // [props containsObject:@"duration"]))
350 // unsigned i, count;
352 // ps = [newApt attendees];
353 // count = [ps count];
354 // for (i = 0; i < count; i++) {
357 // p = [ps objectAtIndex:i];
358 // if (![p hasSameEmailAddress:organizer])
359 // [p setParticipationStatus:iCalPersonPartStatNeedsAction];
361 // _iCal = [[newApt parent] versitString];
362 // updateForcesReconsider = YES;
365 // /* perform storing */
367 storeError = [self primarySaveContentString: _iCal];
369 // storeError = [self saveContentString:_iCal inUIDs:storeUIDs];
370 // delError = [self deleteInUIDs:removedUIDs];
372 // TODO: make compound
373 if (storeError != nil) return storeError;
374 // if (delError != nil) return delError;
376 /* email notifications */
377 // if ([self sendEMailNotifications])
379 // attendees = [NSMutableArray arrayWithArray:[changes insertedAttendees]];
380 // [attendees removePerson:organizer];
381 // [self sendInvitationEMailForTask:newApt
382 // toAttendees:attendees];
384 // if (updateForcesReconsider) {
385 // attendees = [NSMutableArray arrayWithArray:[newApt attendees]];
386 // [attendees removeObjectsInArray:[changes insertedAttendees]];
387 // [attendees removePerson:organizer];
388 // [self sendEMailUsingTemplateNamed: @"Update"
389 // forOldObject: oldApt
390 // andNewObject: newApt
391 // toAttendees: attendees];
394 // attendees = [NSMutableArray arrayWithArray:[changes deletedAttendees]];
395 // [attendees removePerson: organizer];
396 // if ([attendees count]) {
397 // iCalToDo *canceledApt;
399 // canceledApt = [newApt copy];
400 // [(iCalCalendar *) [canceledApt parent] setMethod: @"cancel"];
401 // [self sendEMailUsingTemplateNamed: @"Removal"
403 // andNewObject: canceledApt
404 // toAttendees: attendees];
405 // [canceledApt release];
412 - (NSException *)deleteWithBaseSequence:(int)_v {
414 Note: We need to delete in all participants folders and send iMIP messages
415 for all external accounts.
416 Delete is basically identical to save with all attendees and the
417 organizer being deleted.
420 - fetch stored content
422 - check if sequence matches (or if 0=ignore)
423 - extract old attendee list + organizer (make unique)
424 - delete in removed folders
425 - send iMIP mail for all folders not found
428 NSArray *removedUIDs;
429 NSMutableArray *attendees;
431 /* load existing content */
435 /* compare sequence if requested */
441 removedUIDs = [self attendeeUIDsFromTask:task];
443 if ([self sendEMailNotifications])
445 /* send notification email to attendees excluding organizer */
446 attendees = [NSMutableArray arrayWithArray:[task attendees]];
447 [attendees removePerson:[task organizer]];
449 /* flag task as being canceled */
450 [(iCalCalendar *) [task parent] setMethod: @"cancel"];
451 [task increaseSequence];
453 /* remove all attendees to signal complete removal */
454 [task removeAllAttendees];
456 /* send notification email */
457 [self sendEMailUsingTemplateNamed: @"Deletion"
460 toAttendees: attendees];
465 return [self deleteInUIDs:removedUIDs];
468 - (NSException *)saveContentString:(NSString *)_iCalString {
469 return [self saveContentString:_iCalString baseSequence:0];
472 - (NSException *)changeParticipationStatus:(NSString *)_status
477 NSString *newContent;
481 // TODO: do we need to use SOGoTask? (prefer iCalToDo?)
485 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
486 reason:@"unable to parse task record"];
489 myEMail = [[_ctx activeUser] email];
490 if ((p = [task findParticipantWithEmail:myEMail]) == nil) {
491 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
492 reason:@"user does not participate in this "
496 [p setPartStat:_status];
497 newContent = [[task parent] versitString];
499 // TODO: send iMIP reply mails?
501 // [task release]; task = nil;
503 if (newContent == nil) {
504 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
505 reason:@"Could not generate iCalendar data ..."];
508 if ((ex = [self saveContentString:newContent]) != nil) {
509 // TODO: why is the exception wrapped?
510 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
514 return nil /* means: no error */;
520 - (NSString *)outlookMessageClass {
524 /* EMail Notifications */
526 - (NSString *)homePageURLForPerson:(iCalPerson *)_person {
527 static AgenorUserManager *um = nil;
528 static NSString *baseURL = nil;
533 NSArray *traversalObjects;
535 um = [[AgenorUserManager sharedUserManager] retain];
537 /* generate URL from traversal stack */
538 ctx = [[WOApplication application] context];
539 traversalObjects = [ctx objectTraversalStack];
540 if ([traversalObjects count] >= 1) {
541 baseURL = [[[traversalObjects objectAtIndex:0] baseURLInContext:ctx]
545 [self warnWithFormat:@"Unable to create baseURL from context!"];
546 baseURL = @"http://localhost/";
549 uid = [um getUIDForEmail:[_person rfc822Email]];
550 if (!uid) return nil;
551 return [NSString stringWithFormat:@"%@%@", baseURL, uid];
554 @end /* SOGoTaskObject */