#include <SOGo/SOGoAppointment.h>
#include <SaxObjC/SaxObjC.h>
#include <NGiCal/NGiCal.h>
+#include <NGMime/NGMime.h>
+#include <NGMail/NGMail.h>
+#include <NGMail/NGSendMail.h>
+#include "SOGoAptMailNotification.h"
#include "common.h"
+@interface NSMutableArray (iCalPersonConvenience)
+- (void)removePerson:(iCalPerson *)_person;
+@end
+
+@interface SOGoAppointmentObject (PrivateAPI)
+- (NSString *)homePageURLForPerson:(iCalPerson *)_person;
+- (NSTimeZone *)viewTimeZoneForPerson:(iCalPerson *)_person;
+
+- (void)sendEMailUsingTemplateNamed:(NSString *)_pageName
+ forOldAppointment:(SOGoAppointment *)_newApt
+ andNewAppointment:(SOGoAppointment *)_oldApt
+ toAttendees:(NSArray *)_attendees;
+
+- (void)sendInvitationEMailForAppointment:(SOGoAppointment *)_apt
+ toAttendees:(NSArray *)_attendees;
+- (void)sendAppointmentUpdateEMailForOldAppointment:(SOGoAppointment *)_oldApt
+ newAppointment:(SOGoAppointment *)_newApt
+ toAttendees:(NSArray *)_attendees;
+- (void)sendRemovalEMailForAppointment:(SOGoAppointment *)_apt
+ toAttendees:(NSArray *)_attendees;
+@end
+
@implementation SOGoAppointmentObject
static id<NSObject,SaxXMLReader> parser = nil;
static SaxObjectDecoder *sax = nil;
static NGLogger *logger = nil;
+static NSTimeZone *MET = nil;
+ (void)initialize {
NGLoggerManager *lm;
[parser setContentHandler:sax];
[parser setErrorHandler:sax];
+
+ MET = [[NSTimeZone timeZoneWithAbbreviation:@"MET"] retain];
}
- (void)dealloc {
- delete in removed folders
- send iMIP mail for all folders not found
*/
- SOGoAppointment *oldApt, *newApt;
- NSString *oldContent;
- NSArray *oldUIDs, *newUIDs;
- NSMutableArray *storeUIDs, *removedUIDs;
- unsigned i, count;
- NSException *storeError, *delError;
+ AgenorUserManager *um;
+ SOGoAppointment *oldApt, *newApt;
+ iCalEventChanges *changes;
+ iCalPerson *organizer;
+ NSString *oldContent, *uid;
+ NSArray *uids, *props;
+ NSMutableArray *attendees, *storeUIDs, *removedUIDs;
+ NSException *storeError, *delError;
+ BOOL didChangeAppointmentTime;
+ didChangeAppointmentTime = NO;
+
if ([_iCal length] == 0) {
return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
reason:@"got no iCalendar content to store!"];
}
-
+
+ um = [AgenorUserManager sharedUserManager];
+
/* handle old content */
oldContent = [self iCalString]; /* if nil, this is a new appointment */
// TODO
}
- oldUIDs = [oldApt isNotNull]
- ? [self attendeeUIDsFromAppointment:oldApt]
- : nil;
/* handle new content */
return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
reason:@"could not parse iCalendar content!"];
}
- if ((newUIDs = [self attendeeUIDsFromAppointment:newApt]) == nil)
- [self debugWithFormat:@"got no UIDs from appointment: %@", newApt];
/* diff */
- count = [oldUIDs count];
- removedUIDs = [NSMutableArray arrayWithCapacity:count];
- storeUIDs = [NSMutableArray arrayWithCapacity:count];
- for (i = 0; i < count; i++) {
- NSString *uid;
-
- uid = [oldUIDs objectAtIndex:i];
- if ([newUIDs containsObject:uid])
- [storeUIDs addObject:uid]; /* old ID is still available */
- else
- [removedUIDs addObject:uid]; /* old ID is not available anymore */
+ changes = [iCalEventChanges changesFromEvent:[oldApt event]
+ toEvent:[newApt event]];
+
+ uids = [um getUIDsForICalPersons:[changes deletedAttendees]
+ applyStrictMapping:NO];
+ removedUIDs = [NSMutableArray arrayWithArray:uids];
+
+ uids = [um getUIDsForICalPersons:[newApt attendees]
+ applyStrictMapping:NO];
+ storeUIDs = [NSMutableArray arrayWithArray:uids];
+ props = [changes updatedProperties];
+
+ /* preserve organizer */
+
+ organizer = [[newApt event] organizer];
+ uid = [um getUIDForICalPerson:organizer];
+ if (uid) {
+ if (![storeUIDs containsObject:uid])
+ [storeUIDs addObject:uid];
+ [removedUIDs removeObject:uid];
}
- count = [newUIDs count];
- for (i = 0; i < count; i++) {
- NSString *uid;
-
- uid = [newUIDs objectAtIndex:i];
- if ([storeUIDs containsObject:uid]) /* old ID is still available */
- continue;
- if ([removedUIDs containsObject:uid]) /* old ID is not available anymore */
- continue;
+
+ /* organizer might have changed completely */
+
+ if ((oldApt != nil) && ([props containsObject:@"organizer"])) {
+ uid = [um getUIDForICalPerson:[[oldApt event] organizer]];
+ if (uid) {
+ if (![storeUIDs containsObject:uid]) {
+ if (![removedUIDs containsObject:uid]) {
+ [removedUIDs addObject:uid];
+ }
+ }
+ }
+ }
+
+ [self debugWithFormat:@"UID ops:\n store: %@\n remove: %@",
+ storeUIDs, removedUIDs];
+
+ /* if time did change, all participants have to re-decide ...
+ * ... exception from that rule: the organizer
+ */
+
+ if (oldApt != nil &&
+ ([props containsObject:@"startDate"] ||
+ [props containsObject:@"endDate"] ||
+ [props containsObject:@"duration"]))
+ {
+ NSArray *ps;
+ unsigned i, count;
- /* new ID which is not part of the old set => store a new */
- [storeUIDs addObject:uid];
+ ps = [newApt attendees];
+ count = [ps count];
+ for (i = 0; i < count; i++) {
+ iCalPerson *p;
+
+ p = [ps objectAtIndex:i];
+ if (![p hasSameEmailAddress:organizer])
+ [p setParticipationStatus:iCalPersonPartStatNeedsAction];
+ }
+ _iCal = [newApt iCalString];
+ didChangeAppointmentTime = YES;
}
-
- [self debugWithFormat:
- @"UID ops:\n new: %@\n old: %@\n store: %@\n remove: %@",
- newUIDs, oldUIDs, storeUIDs, removedUIDs];
-
- /* perform */
-
+
+ /* perform storing */
+
storeError = [self saveContentString:_iCal inUIDs:storeUIDs];
delError = [self deleteInUIDs:removedUIDs];
-
+
// TODO: make compound
if (storeError != nil) return storeError;
if (delError != nil) return delError;
-
+
+ /* email notifications */
+
+ attendees = [NSMutableArray arrayWithArray:[changes insertedAttendees]];
+ [attendees removePerson:organizer];
+ [self sendInvitationEMailForAppointment:newApt
+ toAttendees:attendees];
+
+ if (didChangeAppointmentTime) {
+ attendees = [NSMutableArray arrayWithArray:[[newApt event] attendees]];
+ [attendees removeObjectsInArray:[changes insertedAttendees]];
+ [attendees removePerson:organizer];
+ [self sendAppointmentUpdateEMailForOldAppointment:oldApt
+ newAppointment:newApt
+ toAttendees:attendees];
+ }
+
+ attendees = [NSMutableArray arrayWithArray:[changes deletedAttendees]];
+ [attendees removePerson:organizer];
+ [self sendRemovalEMailForAppointment:newApt
+ toAttendees:attendees];
+
return nil;
}
return @"IPM.Appointment";
}
+/* EMail Notifications */
+
+- (NSString *)homePageURLForPerson:(iCalPerson *)_person {
+ static AgenorUserManager *um = nil;
+ static NSString *baseURL = nil;
+ NSString *uid;
+
+ if (!um) {
+ um = [[AgenorUserManager sharedUserManager] retain];
+ baseURL = @"http://agenor.opengroupware.org/foo/so";
+ }
+ uid = [um getUIDForEmail:[_person rfc822Email]];
+ if (!uid) return nil;
+ return [NSString stringWithFormat:@"%@/%@", baseURL, uid];
+}
+
+- (NSTimeZone *)viewTimeZoneForPerson:(iCalPerson *)_person {
+ /* TODO: get this from user config as soon as this is available and only
+ * fall back to default timeZone if config data is not available
+ */
+ return MET;
+}
+
+
+- (void)sendEMailUsingTemplateNamed:(NSString *)_pageName
+ forOldAppointment:(SOGoAppointment *)_oldApt
+ andNewAppointment:(SOGoAppointment *)_newApt
+ toAttendees:(NSArray *)_attendees
+{
+ NSString *pageName;
+ iCalPerson *organizer;
+ NSString *cn, *sender, *iCalString;
+ NGSendMail *sendmail;
+ WOApplication *app;
+ unsigned i, count;
+
+ if (![_attendees count]) return; // another job neatly done :-)
+
+ /* sender */
+
+ organizer = [_newApt organizer];
+ cn = [organizer cnWithoutQuotes];
+ if (cn) {
+ sender = [NSString stringWithFormat:@"%@ <%@>",
+ cn,
+ [organizer rfc822Email]];
+ }
+ else {
+ sender = [organizer rfc822Email];
+ }
+
+ /* generate iCalString once */
+ iCalString = [_newApt iCalString];
+
+ /* get sendmail object */
+ sendmail = [NGSendMail sharedSendMail];
+
+ /* get WOApplication instance */
+ app = [WOApplication application];
+
+ /* create page name */
+ pageName = [NSString stringWithFormat:@"SOGoAptMail%@", _pageName];
+
+ /* generate dynamic message content */
+
+ count = [_attendees count];
+ for (i = 0; i < count; i++) {
+ iCalPerson *attendee;
+ NSString *recipient;
+ SOGoAptMailNotification *p;
+ NSString *subject, *text;
+ NGMutableHashMap *headerMap;
+ NGMimeMessage *msg;
+ NGMimeBodyPart *bodyPart;
+ NGMimeMultipartBody *body;
+
+ attendee = [_attendees objectAtIndex:i];
+
+ /* construct recipient */
+#if 1
+ cn = [attendee cn];
+ if (cn) {
+ recipient = [NSString stringWithFormat:@"%@ <%@>",
+ cn,
+ [attendee rfc822Email]];
+ }
+ else {
+ recipient = [attendee rfc822Email];
+ }
+#else
+ recipient = @"Marcus Mueller <mm@skyrix.com>";
+#endif
+
+ /* construct message content */
+ p = [app pageWithName:pageName];
+ [p setNewApt:_newApt];
+ [p setOldApt:_oldApt];
+ [p setHomePageURL:[self homePageURLForPerson:attendee]];
+ [p setViewTZ:[self viewTimeZoneForPerson:attendee]];
+ subject = [p getSubject];
+ text = [p getBody];
+
+ /* construct message */
+ headerMap = [NGMutableHashMap hashMapWithCapacity:5];
+
+ /* NOTE: multipart/alternative seems like the correct choice but
+ * unfortunately Thunderbird doesn't offer the rich content alternative
+ * at all. Mail.app shows the rich content alternative _only_
+ * so we'll stick with multipart/mixed for the time being.
+ */
+ [headerMap setObject:@"multipart/mixed" forKey:@"content-type"];
+ [headerMap setObject:sender forKey:@"From"];
+ [headerMap setObject:recipient forKey:@"To"];
+ [headerMap setObject:[NSCalendarDate date] forKey:@"date"];
+ [headerMap setObject:subject forKey:@"Subject"];
+ msg = [NGMimeMessage messageWithHeader:headerMap];
+
+ /* multipart body */
+ body = [[NGMimeMultipartBody alloc] initWithPart:msg];
+
+ /* text part */
+ headerMap = [NGMutableHashMap hashMapWithCapacity:1];
+ [headerMap setObject:@"text/plain" forKey:@"content-type"];
+ bodyPart = [NGMimeBodyPart bodyPartWithHeader:headerMap];
+ [bodyPart setBody:text];
+
+ /* attach text part to multipart body */
+ [body addBodyPart:bodyPart];
+
+ /* calendar part */
+ headerMap = [NGMutableHashMap hashMapWithCapacity:1];
+ [headerMap setObject:@"text/calendar" forKey:@"content-type"];
+ bodyPart = [NGMimeBodyPart bodyPartWithHeader:headerMap];
+ [bodyPart setBody:iCalString];
+
+ /* attach calendar part to multipart body */
+ [body addBodyPart:bodyPart];
+
+ /* attach multipart body to message */
+ [msg setBody:body];
+ [body release];
+
+ /* send the damn thing */
+#if 1
+ [sendmail sendMimePart:msg
+ toRecipients:[NSArray arrayWithObject:[attendee rfc822Email]]
+ sender:[organizer rfc822Email]];
+#else
+ [sendmail sendMimePart:msg
+ toRecipients:[NSArray arrayWithObject:@"mm@skyrix.com"]
+ sender:[organizer rfc822Email]];
+#endif
+ }
+}
+
+- (void)sendInvitationEMailForAppointment:(SOGoAppointment *)_apt
+ toAttendees:(NSArray *)_attendees
+{
+ if (![_attendees count]) return; // another job neatly done :-)
+
+ [self sendEMailUsingTemplateNamed:@"Invitation"
+ forOldAppointment:nil
+ andNewAppointment:_apt
+ toAttendees:_attendees];
+}
+
+- (void)sendAppointmentUpdateEMailForOldAppointment:(SOGoAppointment *)_oldApt
+ newAppointment:(SOGoAppointment *)_newApt
+ toAttendees:(NSArray *)_attendees
+{
+ if (![_attendees count]) return;
+
+ [self sendEMailUsingTemplateNamed:@"Update"
+ forOldAppointment:_oldApt
+ andNewAppointment:_newApt
+ toAttendees:_attendees];
+}
+
+- (void)sendRemovalEMailForAppointment:(SOGoAppointment *)_apt
+ toAttendees:(NSArray *)_attendees
+{
+ if (![_attendees count]) return;
+}
+
@end /* SOGoAppointmentObject */
+
+@implementation NSMutableArray (iCalPersonConvenience)
+
+- (void)removePerson:(iCalPerson *)_person {
+ int i;
+
+ for (i = [self count] - 1; i >= 0; i--) {
+ iCalPerson *p;
+
+ p = [self objectAtIndex:i];
+ if ([p hasSameEmailAddress:_person])
+ [self removeObjectAtIndex:i];
+ }
+}
+
+@end /* NSMutableArray (iCalPersonConvenience) */
--- /dev/null
+/*
+ Copyright (C) 2000-2005 SKYRIX Software AG
+
+ This file is part of OpenGroupware.org.
+
+ OGo is free software; you can redistribute it and/or modify it under
+ the terms of the GNU Lesser General Public License as published by the
+ Free Software Foundation; either version 2, or (at your option) any
+ later version.
+
+ OGo is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
+ License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with OGo; see the file COPYING. If not, write to the
+ Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
+ 02111-1307, USA.
+*/
+
+#include "SOGoAptMailNotification.h"
+#include <SOGo/SOGoAppointment.h>
+#include "common.h"
+
+@interface SOGoAptMailNotification (PrivateAPI)
+- (BOOL)isSubject;
+- (void)setIsSubject:(BOOL)_isSubject;
+@end
+
+@implementation SOGoAptMailNotification
+
+static NSCharacterSet *wsSet = nil;
+static NSTimeZone *MET = nil;
+
++ (void)initialize {
+ static BOOL didInit = NO;
+
+ if (didInit) return;
+ didInit = YES;
+
+ wsSet = [[NSCharacterSet whitespaceAndNewlineCharacterSet] retain];
+ MET = [[NSTimeZone timeZoneWithAbbreviation:@"MET"] retain];
+}
+
+- (void)dealloc {
+ [self->oldApt release];
+ [self->newApt release];
+ [self->homePageURL release];
+ [self->viewTZ release];
+
+ [self->oldStartDate release];
+ [self->newStartDate release];
+ [super dealloc];
+}
+
+- (id)oldApt {
+ return self->oldApt;
+}
+- (void)setOldApt:(id)_oldApt {
+ ASSIGN(self->oldApt, _oldApt);
+}
+
+- (id)newApt {
+ return self->newApt;
+}
+- (void)setNewApt:(id)_newApt {
+ ASSIGN(self->newApt, _newApt);
+}
+
+- (NSString *)homePageURL {
+ return self->homePageURL;
+}
+- (void)setHomePageURL:(NSString *)_homePageURL {
+ ASSIGN(self->homePageURL, _homePageURL);
+}
+
+- (NSTimeZone *)viewTZ {
+ if (self->viewTZ) return self->viewTZ;
+ return MET;
+}
+- (void)setViewTZ:(NSTimeZone *)_viewTZ {
+ ASSIGN(self->viewTZ, _viewTZ);
+}
+
+- (BOOL)isSubject {
+ return self->isSubject;
+}
+- (void)setIsSubject:(BOOL)_isSubject {
+ self->isSubject = _isSubject;
+}
+
+
+/* Helpers */
+
+- (NSCalendarDate *)oldStartDate {
+ if (!self->oldStartDate) {
+ ASSIGN(self->oldStartDate, [[self oldApt] startDate]);
+ [self->oldStartDate setTimeZone:[self viewTZ]];
+ }
+ return self->oldStartDate;
+}
+
+- (NSCalendarDate *)newStartDate {
+ if (!self->newStartDate) {
+ ASSIGN(self->newStartDate, [[self newApt] startDate]);
+ [self->newStartDate setTimeZone:[self viewTZ]];
+ }
+ return self->newStartDate;
+}
+
+/* Generate Response */
+
+- (NSString *)getSubject {
+ [self setIsSubject:YES];
+ return [[[self generateResponse] contentAsString]
+ stringByTrimmingCharactersInSet:wsSet];
+}
+
+- (NSString *)getBody {
+ [self setIsSubject:NO];
+ return [[self generateResponse] contentAsString];
+}
+
+@end