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)
38 - (NSString *)homePageURLForPerson:(iCalPerson *)_person;
40 - (void)sendEMailUsingTemplateNamed:(NSString *)_pageName
41 forOldTask:(iCalToDo *)_newApt
42 andNewTask:(iCalToDo *)_oldApt
43 toAttendees:(NSArray *)_attendees;
45 - (void)sendInvitationEMailForTask:(iCalToDo *)_task
46 toAttendees:(NSArray *)_attendees;
47 - (void)sendTaskUpdateEMailForOldTask:(iCalToDo *)_oldApt
48 newTask:(iCalToDo *)_newApt
49 toAttendees:(NSArray *)_attendees;
50 - (void)sendAttendeeRemovalEMailForTask:(iCalToDo *)_task
51 toAttendees:(NSArray *)_attendees;
52 - (void)sendTaskDeletionEMailForTask:(iCalToDo *)_task
53 toAttendees:(NSArray *)_attendees;
56 @implementation SOGoTaskObject
58 static NSString *mailTemplateDefaultLanguage = nil;
62 static BOOL didInit = NO;
67 ud = [NSUserDefaults standardUserDefaults];
68 mailTemplateDefaultLanguage = [[ud stringForKey:@"SOGoDefaultLanguage"]
70 if (!mailTemplateDefaultLanguage)
71 mailTemplateDefaultLanguage = @"French";
78 return [self firstTaskFromCalendar: [self calendar]];
83 - (NSArray *)attendeeUIDsFromTask:(iCalToDo *)_task {
84 AgenorUserManager *um;
88 NSString *email, *uid;
90 if (![_task isNotNull])
93 if ((attendees = [_task attendees]) == nil)
95 count = [attendees count];
96 uids = [NSMutableArray arrayWithCapacity:count + 1];
98 um = [AgenorUserManager sharedUserManager];
102 email = [[_task organizer] rfc822Email];
103 if ([email isNotNull]) {
104 uid = [um getUIDForEmail:email];
105 if ([uid isNotNull]) {
106 [uids addObject:uid];
109 [self logWithFormat:@"Note: got no uid for organizer: '%@'", email];
114 for (i = 0; i < count; i++) {
117 person = [attendees objectAtIndex:i];
118 email = [person rfc822Email];
119 if (![email isNotNull]) continue;
121 uid = [um getUIDForEmail:email];
122 if (![uid isNotNull]) {
123 [self logWithFormat:@"Note: got no uid for email: '%@'", email];
126 if (![uids containsObject:uid])
127 [uids addObject:uid];
133 /* folder management */
135 - (id)lookupHomeFolderForUID:(NSString *)_uid inContext:(id)_ctx {
136 // TODO: what does this do? lookup the home of the organizer?
137 return [[self container] lookupHomeFolderForUID:_uid inContext:_ctx];
139 - (NSArray *)lookupCalendarFoldersForUIDs:(NSArray *)_uids inContext:(id)_ctx {
140 return [[self container] lookupCalendarFoldersForUIDs:_uids inContext:_ctx];
143 /* store in all the other folders */
145 - (NSException *)saveContentString:(NSString *)_iCal inUIDs:(NSArray *)_uids {
148 NSException *allErrors = nil;
151 ctx = [[WOApplication application] context];
153 e = [[self lookupCalendarFoldersForUIDs:_uids inContext:ctx]
155 while ((folder = [e nextObject]) != nil) {
157 SOGoTaskObject *task;
159 if (![folder isNotNull]) /* no folder was found for given UID */
162 task = [folder lookupName:[self nameInContainer] inContext:ctx
164 if ([task isKindOfClass: [NSException class]])
166 [self logWithFormat:@"Note: an exception occured finding '%@' in folder: %@",
167 [self nameInContainer], folder];
168 [self logWithFormat:@"the exception reason was: %@",
169 [(NSException *) task reason]];
173 if (![task isNotNull]) {
174 [self logWithFormat:@"Note: did not find '%@' in folder: %@",
175 [self nameInContainer], folder];
179 if ((error = [task primarySaveContentString:_iCal]) != nil) {
180 [self logWithFormat:@"Note: failed to save iCal in folder: %@", folder];
181 // TODO: make compound
187 - (NSException *)deleteInUIDs:(NSArray *)_uids {
190 NSException *allErrors = nil;
193 ctx = [[WOApplication application] context];
195 e = [[self lookupCalendarFoldersForUIDs:_uids inContext:ctx]
197 while ((folder = [e nextObject])) {
199 SOGoTaskObject *task;
201 task = [folder lookupName:[self nameInContainer] inContext:ctx
203 if (![task isNotNull]) {
204 [self logWithFormat:@"Note: did not find '%@' in folder: %@",
205 [self nameInContainer], folder];
208 if ([task isKindOfClass: [NSException class]]) {
209 [self logWithFormat:@"Exception: %@", [(NSException *) task reason]];
213 if ((error = [task primaryDelete]) != nil) {
214 [self logWithFormat:@"Note: failed to delete in folder: %@", folder];
215 // TODO: make compound
222 - (iCalToDo *) firstTaskFromCalendar: (iCalCalendar *) aCalendar
227 tasks = [aCalendar childrenWithTag: @"vtodo"];
229 task = (iCalToDo *) [[tasks objectAtIndex: 0]
230 groupWithClass: [iCalToDo class]];
237 /* "iCal multifolder saves" */
239 - (NSException *) saveContentString: (NSString *) _iCal
240 baseSequence: (int) _v
243 Note: we need to delete in all participants folders and send iMIP messages
244 for all external accounts.
247 - fetch stored content
249 - check if sequence matches (or if 0=ignore)
250 - extract old attendee list + organizer (make unique)
251 - parse new content (ensure that sequence is increased!)
252 - extract new attendee list + organizer (make unique)
253 - make a diff => new, same, removed
255 - delete in removed folders
256 - send iMIP mail for all folders not found
258 // AgenorUserManager *um;
259 // iCalCalendar *calendar;
260 // iCalToDo *oldApt, *newApt;
261 // // iCalToDoChanges *changes;
262 // iCalPerson *organizer;
263 // NSString *oldContent, *uid;
264 // NSArray *uids, *props;
265 // NSMutableArray *attendees, *storeUIDs, *removedUIDs;
266 NSException *storeError, *delError;
267 // BOOL updateForcesReconsider;
269 // updateForcesReconsider = NO;
271 // if ([_iCal length] == 0) {
272 // return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
273 // reason:@"got no iCalendar content to store!"];
276 // um = [AgenorUserManager sharedUserManager];
278 // /* handle old content */
280 // oldContent = [self iCalString]; /* if nil, this is a new task */
281 // if ([oldContent length] == 0)
284 // [self debugWithFormat:@"saving new task: %@", _iCal];
289 // calendar = [iCalCalendar parseSingleFromSource: oldContent];
290 // oldApt = [self firstTaskFromCalendar: calendar];
293 // /* compare sequence if requested */
300 // /* handle new content */
302 // calendar = [iCalCalendar parseSingleFromSource: _iCal];
303 // newApt = [self firstTaskFromCalendar: calendar];
304 // if (newApt == nil) {
305 // return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
306 // reason:@"could not parse iCalendar content!"];
311 // changes = [iCalToDoChanges changesFromEvent: oldApt
314 // uids = [um getUIDsForICalPersons:[changes deletedAttendees]
315 // applyStrictMapping:NO];
316 // removedUIDs = [NSMutableArray arrayWithArray:uids];
318 // uids = [um getUIDsForICalPersons:[newApt attendees]
319 // applyStrictMapping:NO];
320 // storeUIDs = [NSMutableArray arrayWithArray:uids];
321 // props = [changes updatedProperties];
323 // /* detect whether sequence has to be increased */
324 // if ([changes hasChanges])
325 // [newApt increaseSequence];
327 // /* preserve organizer */
329 // organizer = [newApt organizer];
330 // uid = [um getUIDForICalPerson:organizer];
332 // if (![storeUIDs containsObject:uid])
333 // [storeUIDs addObject:uid];
334 // [removedUIDs removeObject:uid];
337 // /* organizer might have changed completely */
339 // if (oldApt && ([props containsObject: @"organizer"])) {
340 // uid = [um getUIDForICalPerson:[oldApt organizer]];
342 // if (![storeUIDs containsObject:uid]) {
343 // if (![removedUIDs containsObject:uid]) {
344 // [removedUIDs addObject:uid];
350 // [self debugWithFormat:@"UID ops:\n store: %@\n remove: %@",
351 // storeUIDs, removedUIDs];
353 // /* if time did change, all participants have to re-decide ...
354 // * ... exception from that rule: the organizer
357 // if (oldApt != nil &&
358 // ([props containsObject:@"startDate"] ||
359 // [props containsObject:@"endDate"] ||
360 // [props containsObject:@"duration"]))
363 // unsigned i, count;
365 // ps = [newApt attendees];
366 // count = [ps count];
367 // for (i = 0; i < count; i++) {
370 // p = [ps objectAtIndex:i];
371 // if (![p hasSameEmailAddress:organizer])
372 // [p setParticipationStatus:iCalPersonPartStatNeedsAction];
374 // _iCal = [[newApt parent] versitString];
375 // updateForcesReconsider = YES;
378 // /* perform storing */
380 storeError = [self primarySaveContentString: _iCal];
382 // storeError = [self saveContentString:_iCal inUIDs:storeUIDs];
383 // delError = [self deleteInUIDs:removedUIDs];
385 // TODO: make compound
386 if (storeError != nil) return storeError;
387 // if (delError != nil) return delError;
389 /* email notifications */
391 // attendees = [NSMutableArray arrayWithArray:[changes insertedAttendees]];
392 // [attendees removePerson:organizer];
393 // [self sendInvitationEMailForTask:newApt
394 // toAttendees:attendees];
396 // if (updateForcesReconsider) {
397 // attendees = [NSMutableArray arrayWithArray:[newApt attendees]];
398 // [attendees removeObjectsInArray:[changes insertedAttendees]];
399 // [attendees removePerson:organizer];
400 // [self sendTaskUpdateEMailForOldTask:oldApt
402 // toAttendees:attendees];
405 // attendees = [NSMutableArray arrayWithArray:[changes deletedAttendees]];
406 // [attendees removePerson: organizer];
407 // if ([attendees count]) {
408 // iCalToDo *canceledApt;
410 // canceledApt = [newApt copy];
411 // [(iCalCalendar *) [canceledApt parent] setMethod: @"cancel"];
412 // [self sendAttendeeRemovalEMailForTask:canceledApt
413 // toAttendees:attendees];
414 // [canceledApt release];
419 - (NSException *)deleteWithBaseSequence:(int)_v {
421 Note: We need to delete in all participants folders and send iMIP messages
422 for all external accounts.
423 Delete is basically identical to save with all attendees and the
424 organizer being deleted.
427 - fetch stored content
429 - check if sequence matches (or if 0=ignore)
430 - extract old attendee list + organizer (make unique)
431 - delete in removed folders
432 - send iMIP mail for all folders not found
435 NSArray *removedUIDs;
436 NSMutableArray *attendees;
438 /* load existing content */
442 /* compare sequence if requested */
448 removedUIDs = [self attendeeUIDsFromTask:task];
450 /* send notification email to attendees excluding organizer */
451 attendees = [NSMutableArray arrayWithArray:[task attendees]];
452 [attendees removePerson:[task organizer]];
454 /* flag task as being canceled */
455 [(iCalCalendar *) [task parent] setMethod: @"cancel"];
456 [task increaseSequence];
458 /* remove all attendees to signal complete removal */
459 [task removeAllAttendees];
461 /* send notification email */
462 [self sendTaskDeletionEMailForTask:task
463 toAttendees:attendees];
467 return [self deleteInUIDs:removedUIDs];
470 - (NSException *)saveContentString:(NSString *)_iCalString {
471 return [self saveContentString:_iCalString baseSequence:0];
474 - (NSException *)changeParticipationStatus:(NSString *)_status
479 NSString *newContent;
483 // TODO: do we need to use SOGoTask? (prefer iCalToDo?)
487 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
488 reason:@"unable to parse task record"];
491 myEMail = [[_ctx activeUser] email];
492 if ((p = [task findParticipantWithEmail:myEMail]) == nil) {
493 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
494 reason:@"user does not participate in this "
498 [p setPartStat:_status];
499 newContent = [[task parent] versitString];
501 // TODO: send iMIP reply mails?
503 // [task release]; task = nil;
505 if (newContent == nil) {
506 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
507 reason:@"Could not generate iCalendar data ..."];
510 if ((ex = [self saveContentString:newContent]) != nil) {
511 // TODO: why is the exception wrapped?
512 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
516 return nil /* means: no error */;
522 - (NSString *)outlookMessageClass {
526 /* EMail Notifications */
528 - (NSString *)homePageURLForPerson:(iCalPerson *)_person {
529 static AgenorUserManager *um = nil;
530 static NSString *baseURL = nil;
535 NSArray *traversalObjects;
537 um = [[AgenorUserManager sharedUserManager] retain];
539 /* generate URL from traversal stack */
540 ctx = [[WOApplication application] context];
541 traversalObjects = [ctx objectTraversalStack];
542 if ([traversalObjects count] >= 1) {
543 baseURL = [[[traversalObjects objectAtIndex:0] baseURLInContext:ctx]
547 [self warnWithFormat:@"Unable to create baseURL from context!"];
548 baseURL = @"http://localhost/";
551 uid = [um getUIDForEmail:[_person rfc822Email]];
552 if (!uid) return nil;
553 return [NSString stringWithFormat:@"%@%@", baseURL, uid];
556 - (void)sendEMailUsingTemplateNamed:(NSString *)_pageName
557 forOldTask:(iCalToDo *)_oldApt
558 andNewTask:(iCalToDo *)_newApt
559 toAttendees:(NSArray *)_attendees
562 iCalPerson *organizer;
563 NSString *cn, *sender, *iCalString;
564 NGSendMail *sendmail;
568 if (![_attendees count]) return; // another job neatly done :-)
572 organizer = [_newApt organizer];
573 cn = [organizer cnWithoutQuotes];
575 sender = [NSString stringWithFormat:@"%@ <%@>",
577 [organizer rfc822Email]];
580 sender = [organizer rfc822Email];
583 /* generate iCalString once */
584 iCalString = [[_newApt parent] versitString];
586 /* get sendmail object */
587 sendmail = [NGSendMail sharedSendMail];
589 /* get WOApplication instance */
590 app = [WOApplication application];
592 /* generate dynamic message content */
594 count = [_attendees count];
595 for (i = 0; i < count; i++) {
596 iCalPerson *attendee;
598 SOGoAptMailNotification *p;
599 NSString *subject, *text, *header;
600 NGMutableHashMap *headerMap;
602 NGMimeBodyPart *bodyPart;
603 NGMimeMultipartBody *body;
605 attendee = [_attendees objectAtIndex:i];
607 /* construct recipient */
610 recipient = [NSString stringWithFormat:@"%@ <%@>",
612 [attendee rfc822Email]];
615 recipient = [attendee rfc822Email];
618 /* create page name */
619 // TODO: select user's default language?
620 pageName = [NSString stringWithFormat:@"SOGoAptMail%@%@",
621 mailTemplateDefaultLanguage,
623 /* construct message content */
624 p = [app pageWithName:pageName inContext:[WOContext context]];
625 [p setNewApt: _newApt];
626 [p setOldApt: _oldApt];
627 [p setHomePageURL:[self homePageURLForPerson:attendee]];
628 [p setViewTZ: [self userTimeZone: cn]];
629 subject = [p getSubject];
632 /* construct message */
633 headerMap = [NGMutableHashMap hashMapWithCapacity:5];
635 /* NOTE: multipart/alternative seems like the correct choice but
636 * unfortunately Thunderbird doesn't offer the rich content alternative
637 * at all. Mail.app shows the rich content alternative _only_
638 * so we'll stick with multipart/mixed for the time being.
640 [headerMap setObject:@"multipart/mixed" forKey:@"content-type"];
641 [headerMap setObject:sender forKey:@"From"];
642 [headerMap setObject:recipient forKey:@"To"];
643 [headerMap setObject:[NSCalendarDate date] forKey:@"date"];
644 [headerMap setObject:subject forKey:@"Subject"];
645 msg = [NGMimeMessage messageWithHeader:headerMap];
648 body = [[NGMimeMultipartBody alloc] initWithPart:msg];
651 headerMap = [NGMutableHashMap hashMapWithCapacity:1];
652 [headerMap setObject:@"text/plain; charset=utf-8" forKey:@"content-type"];
653 bodyPart = [NGMimeBodyPart bodyPartWithHeader:headerMap];
654 [bodyPart setBody:[text dataUsingEncoding:NSUTF8StringEncoding]];
656 /* attach text part to multipart body */
657 [body addBodyPart:bodyPart];
660 header = [NSString stringWithFormat:@"text/calendar; method=%@;"
662 [(iCalCalendar *) [_newApt parent] method]];
663 headerMap = [NGMutableHashMap hashMapWithCapacity:1];
664 [headerMap setObject:header forKey:@"content-type"];
665 bodyPart = [NGMimeBodyPart bodyPartWithHeader:headerMap];
666 [bodyPart setBody:[iCalString dataUsingEncoding:NSUTF8StringEncoding]];
668 /* attach calendar part to multipart body */
669 [body addBodyPart:bodyPart];
671 /* attach multipart body to message */
675 /* send the damn thing */
676 [sendmail sendMimePart:msg
677 toRecipients:[NSArray arrayWithObject:[attendee rfc822Email]]
678 sender:[organizer rfc822Email]];
682 - (void)sendInvitationEMailForTask:(iCalToDo *)_task
683 toAttendees:(NSArray *)_attendees
685 if (![_attendees count]) return; // another job neatly done :-)
687 [self sendEMailUsingTemplateNamed:@"Invitation"
690 toAttendees:_attendees];
693 - (void)sendTaskUpdateEMailForOldTask:(iCalToDo *)_oldApt
694 newTask:(iCalToDo *)_newApt
695 toAttendees:(NSArray *)_attendees
697 if (![_attendees count]) return;
699 [self sendEMailUsingTemplateNamed:@"Update"
702 toAttendees:_attendees];
705 - (void) sendAttendeeRemovalEMailForTask:(iCalToDo *)_task
706 toAttendees:(NSArray *)_attendees
708 if (![_attendees count]) return;
710 [self sendEMailUsingTemplateNamed:@"Removal"
713 toAttendees:_attendees];
716 - (void) sendTaskDeletionEMailForTask: (iCalToDo *) _task
717 toAttendees: (NSArray *) _attendees
719 if (![_attendees count]) return;
721 [self sendEMailUsingTemplateNamed:@"Deletion"
724 toAttendees:_attendees];
727 - (NSString *) davContentType
729 return @"text/calendar";
732 @end /* SOGoTaskObject */