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>
28 #import <NGMime/NGMime.h>
29 #import <NGMail/NGMail.h>
30 #import <NGMail/NGSendMail.h>
32 #import <SOGo/AgenorUserManager.h>
33 #import <SOGo/SOGoObject.h>
35 #import "SOGoAptMailNotification.h"
36 #import "iCalEntityObject+Agenor.h"
40 #import "NSArray+Appointments.h"
42 @interface SOGoAppointmentObject (PrivateAPI)
43 - (NSString *) homePageURLForPerson: (iCalPerson *) _person;
45 - (void) sendEMailUsingTemplateNamed: (NSString *) _pageName
46 forOldAppointment: (iCalEvent *) _newApt
47 andNewAppointment: (iCalEvent *) _oldApt
48 toAttendees: (NSArray *) _attendees;
50 - (void) sendInvitationEMailForAppointment: (iCalEvent *) _apt
51 toAttendees: (NSArray *) _attendees;
52 - (void) sendAppointmentUpdateEMailForOldAppointment: (iCalEvent *) _oldApt
53 newAppointment: (iCalEvent *) _newApt
54 toAttendees: (NSArray *) _attendees;
55 - (void) sendAttendeeRemovalEMailForAppointment: (iCalEvent *) _apt
56 toAttendees: (NSArray *) _attendees;
57 - (void) sendAppointmentDeletionEMailForAppointment: (iCalEvent *) _apt
58 toAttendees: (NSArray *) _attendees;
61 @implementation SOGoAppointmentObject
63 static NSString *mailTemplateDefaultLanguage = nil;
64 static BOOL sendEMailNotifications = NO;
69 static BOOL didInit = NO;
75 ud = [NSUserDefaults standardUserDefaults];
76 mailTemplateDefaultLanguage = [[ud stringForKey:@"SOGoDefaultLanguage"]
78 if (!mailTemplateDefaultLanguage)
79 mailTemplateDefaultLanguage = @"French";
81 sendEMailNotifications
82 = [ud boolForKey: @"SOGoAppointmentSendEMailNotifications"];
90 return [self firstEventFromCalendar: [self calendar]];
94 - (NSArray *) attendeeUIDsFromAppointment: (iCalEvent *) _apt
96 AgenorUserManager *um;
100 NSString *email, *uid;
102 if (![_apt isNotNull])
105 if ((attendees = [_apt attendees]) == nil)
107 count = [attendees count];
108 uids = [NSMutableArray arrayWithCapacity:count + 1];
110 um = [AgenorUserManager sharedUserManager];
114 email = [[_apt organizer] rfc822Email];
115 if ([email isNotNull]) {
116 uid = [um getUIDForEmail:email];
117 if ([uid isNotNull]) {
118 [uids addObject:uid];
121 [self logWithFormat:@"Note: got no uid for organizer: '%@'", email];
126 for (i = 0; i < count; i++) {
129 person = [attendees objectAtIndex:i];
130 email = [person rfc822Email];
131 if (![email isNotNull]) continue;
133 uid = [um getUIDForEmail:email];
134 if (![uid isNotNull]) {
135 [self logWithFormat:@"Note: got no uid for email: '%@'", email];
138 if (![uids containsObject:uid])
139 [uids addObject:uid];
145 /* folder management */
147 - (id)lookupHomeFolderForUID:(NSString *)_uid inContext:(id)_ctx {
148 // TODO: what does this do? lookup the home of the organizer?
149 return [[self container] lookupHomeFolderForUID:_uid inContext:_ctx];
151 - (NSArray *)lookupCalendarFoldersForUIDs:(NSArray *)_uids inContext:(id)_ctx {
152 return [[self container] lookupCalendarFoldersForUIDs:_uids inContext:_ctx];
155 /* store in all the other folders */
157 - (NSException *)saveContentString:(NSString *)_iCal inUIDs:(NSArray *)_uids {
160 NSException *allErrors = nil;
163 ctx = [[WOApplication application] context];
165 e = [[self lookupCalendarFoldersForUIDs:_uids inContext:ctx]
167 while ((folder = [e nextObject]) != nil) {
169 SOGoAppointmentObject *apt;
171 if (![folder isNotNull]) /* no folder was found for given UID */
174 apt = [folder lookupName: [self nameInContainer] inContext:ctx
176 if ([apt isKindOfClass: [NSException class]])
178 [self logWithFormat:@"Note: an exception occured finding '%@' in folder: %@",
179 [self nameInContainer], folder];
180 [self logWithFormat:@"the exception reason was: %@",
181 [(NSException *) apt reason]];
185 if (![apt isNotNull]) {
186 [self logWithFormat:@"Note: did not find '%@' in folder: %@",
187 [self nameInContainer], folder];
190 if ([apt isKindOfClass: [NSException class]]) {
191 [self logWithFormat:@"Exception: %@", [(NSException *) apt reason]];
195 if ((error = [apt primarySaveContentString:_iCal]) != nil) {
196 [self logWithFormat:@"Note: failed to save iCal in folder: %@", folder];
197 // TODO: make compound
205 - (NSException *)deleteInUIDs:(NSArray *)_uids {
208 NSException *allErrors = nil;
211 ctx = [[WOApplication application] context];
213 e = [[self lookupCalendarFoldersForUIDs:_uids inContext:ctx]
215 while ((folder = [e nextObject])) {
217 SOGoAppointmentObject *apt;
219 apt = [folder lookupName:[self nameInContainer] inContext:ctx
221 if ([apt isKindOfClass: [NSException class]]) {
222 [self logWithFormat: @"%@", [(NSException *) apt reason]];
226 if ((error = [apt primaryDelete]) != nil) {
227 [self logWithFormat:@"Note: failed to delete in folder: %@", folder];
228 // TODO: make compound
235 - (iCalEvent *) firstEventFromCalendar: (iCalCalendar *) aCalendar
240 events = [aCalendar childrenWithTag: @"vevent"];
242 event = (iCalEvent *) [[events objectAtIndex: 0]
243 groupWithClass: [iCalEvent class]];
250 /* "iCal multifolder saves" */
252 - (NSException *) saveContentString: (NSString *) _iCal
253 baseSequence: (int) _v
256 Note: we need to delete in all participants folders and send iMIP messages
257 for all external accounts.
260 - fetch stored content
262 - check if sequence matches (or if 0=ignore)
263 - extract old attendee list + organizer (make unique)
264 - parse new content (ensure that sequence is increased!)
265 - extract new attendee list + organizer (make unique)
266 - make a diff => new, same, removed
268 - delete in removed folders
269 - send iMIP mail for all folders not found
271 AgenorUserManager *um;
272 iCalCalendar *newCalendar;
273 iCalEvent *oldApt, *newApt;
274 iCalEventChanges *changes;
275 iCalPerson *organizer;
276 NSString *oldContent, *uid;
277 NSArray *uids, *props;
278 NSMutableArray *attendees, *storeUIDs, *removedUIDs;
279 NSException *storeError, *delError;
280 BOOL updateForcesReconsider;
282 updateForcesReconsider = NO;
284 if ([_iCal length] == 0) {
285 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
286 reason:@"got no iCalendar content to store!"];
289 um = [AgenorUserManager sharedUserManager];
291 /* handle old content */
293 oldContent = [self iCalString]; /* if nil, this is a new appointment */
294 if ([oldContent length] == 0)
296 /* new appointment */
297 [self debugWithFormat:@"saving new appointment: %@", _iCal];
301 oldApt = [self firstEventFromCalendar: [self calendar]];
303 /* compare sequence if requested */
310 /* handle new content */
312 newCalendar = [iCalCalendar parseSingleFromSource: _iCal];
313 newApt = [self firstEventFromCalendar: newCalendar];
315 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
316 reason:@"could not parse iCalendar content!"];
321 changes = [iCalEventChanges changesFromEvent: oldApt
324 uids = [um getUIDsForICalPersons:[changes deletedAttendees]
325 applyStrictMapping:NO];
326 removedUIDs = [NSMutableArray arrayWithArray:uids];
328 uids = [um getUIDsForICalPersons:[newApt attendees]
329 applyStrictMapping:NO];
330 storeUIDs = [NSMutableArray arrayWithArray:uids];
331 props = [changes updatedProperties];
333 /* detect whether sequence has to be increased */
334 if ([changes hasChanges])
335 [newApt increaseSequence];
337 /* preserve organizer */
339 organizer = [newApt organizer];
340 uid = [um getUIDForICalPerson:organizer];
342 if (![storeUIDs containsObject:uid])
343 [storeUIDs addObject:uid];
344 [removedUIDs removeObject:uid];
347 /* organizer might have changed completely */
349 if (oldApt && ([props containsObject: @"organizer"])) {
350 uid = [um getUIDForICalPerson:[oldApt organizer]];
352 if (![storeUIDs containsObject:uid]) {
353 if (![removedUIDs containsObject:uid]) {
354 [removedUIDs addObject:uid];
360 [self debugWithFormat:@"UID ops:\n store: %@\n remove: %@",
361 storeUIDs, removedUIDs];
363 /* if time did change, all participants have to re-decide ...
364 * ... exception from that rule: the organizer
368 ([props containsObject:@"startDate"] ||
369 [props containsObject:@"endDate"] ||
370 [props containsObject:@"duration"]))
375 ps = [newApt attendees];
377 for (i = 0; i < count; i++) {
380 p = [ps objectAtIndex:i];
381 if (![p hasSameEmailAddress:organizer])
382 [p setParticipationStatus:iCalPersonPartStatNeedsAction];
384 _iCal = [[newApt parent] versitString];
385 updateForcesReconsider = YES;
388 /* perform storing */
390 storeError = [self saveContentString:_iCal inUIDs:storeUIDs];
391 delError = [self deleteInUIDs:removedUIDs];
393 // TODO: make compound
394 if (storeError != nil) return storeError;
395 if (delError != nil) return delError;
397 /* email notifications */
398 if (sendEMailNotifications)
400 attendees = [NSMutableArray arrayWithArray:[changes insertedAttendees]];
401 [attendees removePerson:organizer];
402 [self sendInvitationEMailForAppointment:newApt
403 toAttendees:attendees];
405 if (updateForcesReconsider) {
406 attendees = [NSMutableArray arrayWithArray:[newApt attendees]];
407 [attendees removeObjectsInArray:[changes insertedAttendees]];
408 [attendees removePerson:organizer];
409 [self sendAppointmentUpdateEMailForOldAppointment:oldApt
410 newAppointment:newApt
411 toAttendees:attendees];
414 attendees = [NSMutableArray arrayWithArray:[changes deletedAttendees]];
415 [attendees removePerson: organizer];
416 if ([attendees count])
418 iCalEvent *canceledApt;
420 canceledApt = [newApt copy];
421 [(iCalCalendar *) [canceledApt parent] setMethod: @"cancel"];
422 [self sendAttendeeRemovalEMailForAppointment:canceledApt
423 toAttendees: attendees];
424 [canceledApt release];
431 - (NSException *)deleteWithBaseSequence:(int)_v {
433 Note: We need to delete in all participants folders and send iMIP messages
434 for all external accounts.
435 Delete is basically identical to save with all attendees and the
436 organizer being deleted.
439 - fetch stored content
441 - check if sequence matches (or if 0=ignore)
442 - extract old attendee list + organizer (make unique)
443 - delete in removed folders
444 - send iMIP mail for all folders not found
447 NSArray *removedUIDs;
448 NSMutableArray *attendees;
450 /* load existing content */
454 /* compare sequence if requested */
460 removedUIDs = [self attendeeUIDsFromAppointment:apt];
462 if (sendEMailNotifications)
464 /* send notification email to attendees excluding organizer */
465 attendees = [NSMutableArray arrayWithArray:[apt attendees]];
466 [attendees removePerson:[apt organizer]];
468 /* flag appointment as being canceled */
469 [(iCalCalendar *) [apt parent] setMethod: @"cancel"];
470 [apt increaseSequence];
472 /* remove all attendees to signal complete removal */
473 [apt removeAllAttendees];
475 /* send notification email */
476 [self sendAppointmentDeletionEMailForAppointment:apt
477 toAttendees:attendees];
482 return [self deleteInUIDs:removedUIDs];
485 - (NSException *) saveContentString: (NSString *) _iCalString
487 return [self saveContentString: _iCalString baseSequence: 0];
490 - (NSException *) changeParticipationStatus: (NSString *) _status
495 NSString *newContent;
501 // TODO: do we need to use SOGoAppointment? (prefer iCalEvent?)
506 myEMail = [[_ctx activeUser] email];
507 p = [apt findParticipantWithEmail: myEMail];
510 // TODO: send iMIP reply mails?
512 [p setPartStat:_status];
513 newContent = [[apt parent] versitString];
516 ex = [self saveContentString:newContent];
518 // TODO: why is the exception wrapped?
520 ex = [NSException exceptionWithHTTPStatus: 500
521 reason: [ex reason]];
525 = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
526 reason: @"Could not generate iCalendar data ..."];
529 ex = [NSException exceptionWithHTTPStatus: 404 /* Not Found */
530 reason: @"user does not participate in this "
534 ex = [NSException exceptionWithHTTPStatus:500 /* Server Error */
535 reason:@"unable to parse appointment record"];
543 - (NSString *) outlookMessageClass
545 return @"IPM.Appointment";
548 /* EMail Notifications */
550 - (NSString *) homePageURLForPerson: (iCalPerson *) _person
555 NSArray *traversalObjects;
557 /* generate URL from traversal stack */
558 ctx = [[WOApplication application] context];
559 traversalObjects = [ctx objectTraversalStack];
560 if ([traversalObjects count] > 0)
561 baseURL = [[traversalObjects objectAtIndex:0] baseURLInContext:ctx];
564 baseURL = @"http://localhost/";
565 [self warnWithFormat:@"Unable to create baseURL from context!"];
567 uid = [[AgenorUserManager sharedUserManager]
568 getUIDForEmail: [_person rfc822Email]];
571 ? [NSString stringWithFormat:@"%@%@", baseURL, uid]
575 - (NSException *) saveContentString: (NSString *) contentString
576 baseVersion: (unsigned int) baseVersion
578 NSString *newContentString, *oldContentString;
579 iCalCalendar *eventCalendar;
583 oldContentString = [self iCalString];
584 if (oldContentString)
585 newContentString = contentString;
588 eventCalendar = [iCalCalendar parseSingleFromSource: contentString];
589 event = [self firstEventFromCalendar: eventCalendar];
590 organizers = [event childrenWithTag: @"organizer"];
591 if ([organizers count])
592 newContentString = contentString;
595 [event setOrganizerWithUid: [[self container] ownerInContext: nil]];
596 newContentString = [eventCalendar versitString];
600 return [super saveContentString: newContentString
601 baseVersion: baseVersion];
604 - (void) sendEMailUsingTemplateNamed: (NSString *) _pageName
605 forOldAppointment: (iCalEvent *) _oldApt
606 andNewAppointment: (iCalEvent *) _newApt
607 toAttendees: (NSArray *) _attendees
610 iCalPerson *organizer;
611 NSString *cn, *sender, *iCalString;
612 NGSendMail *sendmail;
615 iCalPerson *attendee;
617 SOGoAptMailNotification *p;
618 NSString *subject, *text, *header;
619 NGMutableHashMap *headerMap;
621 NGMimeBodyPart *bodyPart;
622 NGMimeMultipartBody *body;
624 if ([_attendees count])
628 organizer = [_newApt organizer];
629 cn = [organizer cnWithoutQuotes];
631 sender = [NSString stringWithFormat:@"%@ <%@>",
633 [organizer rfc822Email]];
635 sender = [organizer rfc822Email];
637 /* generate iCalString once */
638 iCalString = [[_newApt parent] versitString];
640 /* get sendmail object */
641 sendmail = [NGSendMail sharedSendMail];
643 /* get WOApplication instance */
644 app = [WOApplication application];
646 /* generate dynamic message content */
648 count = [_attendees count];
649 for (i = 0; i < count; i++)
651 attendee = [_attendees objectAtIndex:i];
653 /* construct recipient */
656 recipient = [NSString stringWithFormat: @"%@ <%@>",
658 [attendee rfc822Email]];
660 recipient = [attendee rfc822Email];
662 /* create page name */
663 // TODO: select user's default language?
664 pageName = [NSString stringWithFormat: @"SOGoAptMail%@%@",
665 mailTemplateDefaultLanguage,
667 /* construct message content */
668 p = [app pageWithName: pageName inContext: [WOContext context]];
669 [p setNewApt: _newApt];
670 [p setOldApt: _oldApt];
671 [p setHomePageURL: [self homePageURLForPerson: attendee]];
672 [p setViewTZ: [self userTimeZone: cn]];
673 subject = [p getSubject];
676 /* construct message */
677 headerMap = [NGMutableHashMap hashMapWithCapacity: 5];
679 /* NOTE: multipart/alternative seems like the correct choice but
680 * unfortunately Thunderbird doesn't offer the rich content alternative
681 * at all. Mail.app shows the rich content alternative _only_
682 * so we'll stick with multipart/mixed for the time being.
684 [headerMap setObject: @"multipart/mixed" forKey: @"content-type"];
685 [headerMap setObject: sender forKey: @"From"];
686 [headerMap setObject: recipient forKey: @"To"];
687 [headerMap setObject: [NSCalendarDate date] forKey: @"date"];
688 [headerMap setObject: subject forKey: @"Subject"];
689 msg = [NGMimeMessage messageWithHeader: headerMap];
692 body = [[NGMimeMultipartBody alloc] initWithPart: msg];
695 headerMap = [NGMutableHashMap hashMapWithCapacity: 1];
696 [headerMap setObject: @"text/plain; charset=utf-8"
697 forKey: @"content-type"];
698 bodyPart = [NGMimeBodyPart bodyPartWithHeader: headerMap];
699 [bodyPart setBody: [text dataUsingEncoding: NSUTF8StringEncoding]];
701 /* attach text part to multipart body */
702 [body addBodyPart: bodyPart];
705 header = [NSString stringWithFormat: @"text/calendar; method=%@;"
707 [(iCalCalendar *) [_newApt parent] method]];
708 headerMap = [NGMutableHashMap hashMapWithCapacity: 1];
709 [headerMap setObject:header forKey: @"content-type"];
710 bodyPart = [NGMimeBodyPart bodyPartWithHeader: headerMap];
711 [bodyPart setBody: [iCalString dataUsingEncoding: NSUTF8StringEncoding]];
713 /* attach calendar part to multipart body */
714 [body addBodyPart: bodyPart];
716 /* attach multipart body to message */
720 /* send the damn thing */
721 [sendmail sendMimePart: msg
722 toRecipients: [NSArray arrayWithObject: [attendee rfc822Email]]
723 sender: [organizer rfc822Email]];
728 - (void) sendInvitationEMailForAppointment: (iCalEvent *) _apt
729 toAttendees: (NSArray *) _attendees
731 if ([_attendees count])
732 [self sendEMailUsingTemplateNamed: @"Invitation"
733 forOldAppointment: nil
734 andNewAppointment: _apt
735 toAttendees: _attendees];
738 - (void) sendAppointmentUpdateEMailForOldAppointment: (iCalEvent *) _oldApt
739 newAppointment: (iCalEvent *) _newApt
740 toAttendees: (NSArray *) _attendees
742 if ([_attendees count])
743 [self sendEMailUsingTemplateNamed: @"Update"
744 forOldAppointment: _oldApt
745 andNewAppointment: _newApt
746 toAttendees: _attendees];
749 - (void) sendAttendeeRemovalEMailForAppointment: (iCalEvent *) _apt
750 toAttendees: (NSArray *) _attendees
752 if ([_attendees count])
753 [self sendEMailUsingTemplateNamed: @"Removal"
754 forOldAppointment: nil
755 andNewAppointment: _apt
756 toAttendees: _attendees];
759 - (void) sendAppointmentDeletionEMailForAppointment: (iCalEvent *) _apt
760 toAttendees: (NSArray *) _attendees
762 if ([_attendees count])
763 [self sendEMailUsingTemplateNamed: @"Deletion"
764 forOldAppointment: nil
765 andNewAppointment: _apt
766 toAttendees: _attendees];
769 - (NSString *) davContentType
771 return @"text/calendar";
774 - (NSString *) roleOfUser: (NSString *) login
775 inContext: (WOContext *) context
777 AgenorUserManager *um;
779 NSString *role, *email;
781 um = [AgenorUserManager sharedUserManager];
782 email = [um getEmailForUID: login];
784 event = [self event];
785 if ([event isOrganizer: email])
787 else if ([event isParticipant: email])
788 role = @"Participant";
795 @end /* SOGoAppointmentObject */