/* SOGoCalendarComponent.m - this file is part of SOGo * * Copyright (C) 2006-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This file 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "SOGoAptMailICalReply.h" #import "SOGoAptMailNotification.h" #import "SOGoAptMailReceipt.h" #import "SOGoEMailAlarmsManager.h" #import "iCalEntityObject+SOGo.h" #import "iCalPerson+SOGo.h" #import "iCalRepeatableEntityObject+SOGo.h" #import "SOGoCalendarComponent.h" #import "SOGoComponentOccurence.h" @implementation SOGoCalendarComponent + (SOGoWebDAVAclManager *) webdavAclManager { static SOGoWebDAVAclManager *aclManager = nil; NSString *nsD, *nsI; if (!aclManager) { nsD = @"DAV:"; nsI = @"urn:inverse:params:xml:ns:inverse-dav"; aclManager = [SOGoWebDAVAclManager new]; [aclManager registerDAVPermission: davElement (@"read", nsD) abstract: NO withEquivalent: @"SOGoDAVReadPermission" /* hackish */ asChildOf: davElement (@"all", nsD)]; [aclManager registerDAVPermission: davElement (@"view-whole-component", nsI) abstract: NO withEquivalent: SOGoCalendarPerm_ViewAllComponent asChildOf: davElement (@"all", nsD)]; [aclManager registerDAVPermission: davElement (@"view-date-and-time", nsI) abstract: NO withEquivalent: SOGoCalendarPerm_ViewDAndT asChildOf: davElement (@"all", nsD)]; [aclManager registerDAVPermission: davElement (@"read-current-user-privilege-set", nsD) abstract: NO withEquivalent: SoPerm_WebDAVAccess asChildOf: davElement (@"all", nsD)]; [aclManager registerDAVPermission: davElement (@"write", nsD) abstract: NO withEquivalent: SOGoCalendarPerm_ModifyComponent asChildOf: davElement (@"all", nsD)]; [aclManager registerDAVPermission: davElement (@"write-properties", nsD) abstract: YES withEquivalent: SoPerm_ChangePermissions /* hackish */ asChildOf: davElement (@"write", nsD)]; [aclManager registerDAVPermission: davElement (@"write-content", nsD) abstract: YES withEquivalent: nil asChildOf: davElement (@"write", nsD)]; [aclManager registerDAVPermission: davElement (@"respond-to-component", nsI) abstract: NO withEquivalent: SOGoCalendarPerm_RespondToComponent asChildOf: davElement (@"write-content", nsD)]; [aclManager registerDAVPermission: davElement (@"admin", nsI) abstract: YES withEquivalent: nil asChildOf: davElement (@"all", nsD)]; [aclManager registerDAVPermission: davElement (@"read-acl", nsD) abstract: YES withEquivalent: SOGoPerm_ReadAcls asChildOf: davElement (@"admin", nsI)]; [aclManager registerDAVPermission: davElement (@"write-acl", nsD) abstract: YES withEquivalent: nil asChildOf: davElement (@"admin", nsI)]; } return aclManager; } - (id) init { if ((self = [super init])) { fullCalendar = nil; safeCalendar = nil; originalCalendar = nil; componentTag = nil; } return self; } - (void) dealloc { [fullCalendar release]; [safeCalendar release]; [originalCalendar release]; [componentTag release]; [super dealloc]; } - (void) flush { DESTROY(fullCalendar); DESTROY(safeCalendar); DESTROY(originalCalendar); } - (Class *) parsingClass { return [iCalCalendar class]; } - (NSString *) davContentType { return @"text/calendar"; } - (NSString *) componentTag { if (!componentTag) [self subclassResponsibility: _cmd]; return componentTag; } - (void) setComponentTag: (NSString *) theTag { ASSIGN(componentTag, theTag); } - (void) _filterComponent: (iCalEntityObject *) component { NSString *type, *summary; int classification; type = @"vtodo"; classification = 0; if ([component isKindOfClass: [iCalEvent class]]) type = @"vevent"; if ([component symbolicAccessClass] == iCalAccessPrivate) classification = 1; else if ([component symbolicAccessClass] == iCalAccessConfidential) classification = 2; summary = [self labelForKey: [NSString stringWithFormat: @"%@_class%d", type, classification] inContext: context]; [component setSummary: summary]; [component setComment: @""]; [component setUserComment: @""]; [component setLocation: @""]; [component setCategories: [NSArray array]]; [component setUrl: @""]; [component removeAllAttendees]; [component removeAllAlarms]; } - (NSString *) secureContentAsString { iCalRepeatableEntityObject *tmpComponent; iCalCalendar *tmpCalendar; NSArray *allComponents; SoSecurityManager *sm; NSString *iCalString; int i; sm = [SoSecurityManager sharedSecurityManager]; if (activeUserIsOwner || [[self ownerInContext: context] isEqualToString: [[context activeUser] login]] || ![sm validatePermission: SOGoCalendarPerm_ViewAllComponent onObject: self inContext: context]) iCalString = content; else if (![sm validatePermission: SOGoCalendarPerm_ViewDAndT onObject: self inContext: context]) { tmpCalendar = [[self calendar: NO secure: NO] mutableCopy]; // We filter all components, in case we have RECURRENCE-ID allComponents = [tmpCalendar childrenWithTag: [self componentTag]]; for (i = 0; i < [allComponents count]; i++) { tmpComponent = (iCalRepeatableEntityObject *)[allComponents objectAtIndex:i]; [self _filterComponent: tmpComponent]; // We add an additional header here to inform clients (if necessary) that // we churned the content of the calendar. [tmpComponent addChild: [CardElement simpleElementWithTag: @"X-SOGo-Secure" value: @"YES"]]; } iCalString = [tmpCalendar versitString]; [tmpCalendar release]; } else iCalString = nil; return iCalString; } - (iCalRepeatableEntityObject *) lookupOccurrence: (NSString *) recID { [self subclassResponsibility: _cmd]; return nil; } - (SOGoComponentOccurence *) occurence: (iCalRepeatableEntityObject *) component { [self subclassResponsibility: _cmd]; return nil; } - (iCalRepeatableEntityObject *) newOccurenceWithID: (NSString *) recID { iCalRepeatableEntityObject *masterOccurence, *newOccurence; iCalCalendar *calendar; NSCalendarDate *recDate; NSTimeZone *timeZone; iCalPerson *organizer; recDate = [NSCalendarDate dateWithTimeIntervalSince1970: [recID intValue]]; masterOccurence = [self component: NO secure: NO]; timeZone = [[[context activeUser] userDefaults] timeZone]; [recDate setTimeZone: timeZone]; if ([masterOccurence doesOccurOnDate: recDate]) { newOccurence = [masterOccurence mutableCopy]; organizer = [masterOccurence organizer]; [newOccurence autorelease]; [newOccurence removeAllRecurrenceRules]; [newOccurence removeAllExceptionRules]; [newOccurence removeAllExceptionDates]; // It is important to set the organizer as some DAV clients (iCal // and Thunderbird 10/Lightning 1.2) will prompt the event "edition" // dialog instead of the event "invitation" (for accept/decline/tentative) // if the organizer isn't found in a specific recurrence [newOccurence setOrganizer: organizer]; [newOccurence setRecurrenceId: recDate]; calendar = [masterOccurence parent]; [calendar addChild: newOccurence]; } else newOccurence = nil; return newOccurence; } - (id) toManyRelationshipKeys { return nil; } - (id) toOneRelationshipKeys { NSMutableArray *keys; NSArray *occurences; NSCalendarDate *recID; unsigned int count, max, seconds; keys = [NSMutableArray array]; [keys addObject: @"master"]; occurences = [[self calendar: NO secure: NO] allObjects]; max = [occurences count]; for (count = 1; count < max; count++) { recID = [[occurences objectAtIndex: count] recurrenceId]; if (recID) { seconds = [recID timeIntervalSince1970]; [keys addObject: [NSString stringWithFormat: @"occurence%d", seconds]]; } } return keys; } - (id) lookupName: (NSString *) lookupName inContext: (id) localContext acquire: (BOOL) acquire { id obj; iCalRepeatableEntityObject *occurence; NSString *recID; BOOL isNewOccurence; obj = [super lookupName: lookupName inContext: localContext acquire: acquire]; if (!obj) { if ([lookupName isEqualToString: @"master"]) obj = [self occurence: [self component: NO secure: NO]]; else if ([lookupName hasPrefix: @"occurence"]) { recID = [lookupName substringFromIndex: 9]; occurence = [self lookupOccurrence: recID]; if (occurence) isNewOccurence = NO; else { occurence = [self newOccurenceWithID: recID]; isNewOccurence = YES; } if (occurence) { obj = [self occurence: occurence]; if (isNewOccurence) [obj setIsNew: isNewOccurence]; } } } return obj; } - (NSString *) _secureContentWithoutAlarms { iCalCalendar *calendar; NSArray *allComponents; iCalEntityObject *currentComponent; NSUInteger count, max; calendar = [self calendar: NO secure: YES]; allComponents = [calendar childrenWithTag: [self componentTag]]; max = [allComponents count]; for (count = 0; count < max; count++) { currentComponent = [allComponents objectAtIndex: count]; [currentComponent removeAllAlarms]; } return [calendar versitString]; } - (NSString *) contentAsString { NSString *secureContent; if ([[context request] isSoWebDAVRequest]) { if ([container showCalendarAlarms]) secureContent = [self secureContentAsString]; else secureContent = [self _secureContentWithoutAlarms]; } else secureContent = [super contentAsString]; return secureContent; } - (NSString *) davCalendarData { return [self contentAsString]; } - (iCalCalendar *) calendar: (BOOL) create secure: (BOOL) secure { iCalRepeatableEntityObject *newComponent; iCalCalendar **calendar, *returnedCopy; NSString *iCalString, *tag, *prodID; if (secure) calendar = &safeCalendar; else calendar = &fullCalendar; if (!*calendar) { if (secure) iCalString = [self secureContentAsString]; else iCalString = content; if ([iCalString length] > 0) { ASSIGN (*calendar, [iCalCalendar parseSingleFromSource: iCalString]); if (!secure) originalCalendar = [*calendar copy]; } else { if (create) { ASSIGN (*calendar, [iCalCalendar groupWithTag: @"vcalendar"]); [*calendar setVersion: @"2.0"]; prodID = [NSString stringWithFormat: @"-//Inverse inc./SOGo %@//EN", SOGoVersion]; [*calendar setProdID: prodID]; tag = [[self componentTag] uppercaseString]; newComponent = [[*calendar classForTag: tag] groupWithTag: tag]; [newComponent setUid: [self globallyUniqueObjectId]]; [*calendar addChild: newComponent]; } } } returnedCopy = [*calendar mutableCopy]; [returnedCopy autorelease]; return returnedCopy; } - (id) component: (BOOL) create secure: (BOOL) secure { return [[self calendar: create secure: secure] firstChildWithTag: [self componentTag]]; } // // Returs "YES" if a a group was decomposed among attendees. // // It can also return yes if an attendee was found in the list // matching the organizer. In which case, it was removed. // - (BOOL) expandGroupsInEvent: (iCalEvent *) theEvent { NSString *organizerEmail, *domain; NSMutableArray *allAttendees; iCalPerson *currentAttendee; NSEnumerator *enumerator; NSAutoreleasePool *pool; SOGoGroup *group; BOOL eventWasModified; unsigned int i, j; domain = [[context activeUser] domain]; organizerEmail = [[theEvent organizer] rfc822Email]; eventWasModified = NO; allAttendees = [NSMutableArray arrayWithArray: [theEvent attendees]]; enumerator = [[theEvent attendees] objectEnumerator]; j = 0; pool = [[NSAutoreleasePool alloc] init]; while ((currentAttendee = [enumerator nextObject])) { if (j%5 == 0) { RELEASE(pool); pool = [[NSAutoreleasePool alloc] init]; } group = [SOGoGroup groupWithEmail: [currentAttendee rfc822Email] inDomain: domain]; if (group) { iCalPerson *person; NSArray *members; SOGoUser *user; // We did decompose a group... [allAttendees removeObject: currentAttendee]; members = [group members]; for (i = 0; i < [members count]; i++) { user = [members objectAtIndex: i]; eventWasModified = YES; // If the organizer is part of the group, we skip it from // the addition to the attendees' list if ([user hasEmail: organizerEmail]) continue; person = [self iCalPersonWithUID: [user login]]; [person setTag: @"ATTENDEE"]; [person setParticipationStatus: [currentAttendee participationStatus]]; [person setRsvp: [currentAttendee rsvp]]; [person setRole: [currentAttendee role]]; if (![allAttendees containsObject: person]) [allAttendees addObject: person]; } } else { // We remove any attendees matching the organizer. Apple iCal will do that when // you invite someone. It'll add the organizer in the attendee list, which will // confuse itself! if ([[currentAttendee rfc822Email] caseInsensitiveCompare: organizerEmail] == NSOrderedSame) { [allAttendees removeObject: currentAttendee]; eventWasModified = YES; } } j++; } // while (currentAttendee ... if (eventWasModified) [theEvent setAttendees: allAttendees]; RELEASE(pool); return eventWasModified; } - (void) _updateRecurrenceIDsWithEvent: (iCalRepeatableEntityObject*) newEvent { iCalRepeatableEntityObject *oldMaster, *currentComponent; iCalDateTime *currentDate; int deltaSecs; NSArray *components, *dates; NSMutableArray *newDates; unsigned int count, max; NSCalendarDate *recID, *newDate; // Compute time interval from previous event definition. if (!originalCalendar) { if (content) ASSIGN (originalCalendar, [iCalCalendar parseSingleFromSource: content]); else [self warnWithFormat: @"content not available, we will crash"]; } oldMaster = (iCalRepeatableEntityObject *) [originalCalendar firstChildWithTag: [self componentTag]]; deltaSecs = [[newEvent startDate] timeIntervalSinceDate: [oldMaster startDate]]; components = [[newEvent parent] events]; max = [components count]; if (max > 0) { // Update recurrence-id attribute of occurences. for (count = 1; count < max; count++) { currentComponent = [components objectAtIndex: count]; recID = [[currentComponent recurrenceId] addTimeInterval: deltaSecs]; [currentComponent setRecurrenceId: recID]; } // Update exception dates in master vEvent. currentComponent = [components objectAtIndex: 0]; dates = [currentComponent childrenWithTag: @"exdate"]; max = [dates count]; if (max > 0) { newDates = [NSMutableArray arrayWithCapacity: max]; for (count = 0; count < max; count++) { currentDate = [dates objectAtIndex: count]; newDate = [[currentDate dateTime] addTimeInterval: deltaSecs]; [newDates addObject: newDate]; } [currentComponent removeAllExceptionDates]; for (count = 0; count < max; count++) [currentComponent addToExceptionDates: [newDates objectAtIndex: count]]; } } } - (void) updateComponent: (iCalRepeatableEntityObject *) newObject { NSString *newUid; if (!isNew && [newObject isRecurrent]) // We update an repeating event -- update exception dates // and recurrence-ids. [self _updateRecurrenceIDsWithEvent: newObject]; // As much as we can, we try to use c_name == c_uid in order // to avoid tricky scenarios with some CalDAV clients. For example, // if Alice invites Bob (both use SOGo) and Bob accepts the invitation // using Lightning before having refreshed his calendar, he'll end up // with a duplicate of the event in his database tables. if (isNew) { newUid = nameInContainer; if ([newUid hasSuffix: @".ics"]) newUid = [newUid substringToIndex: [newUid length]-4]; [newObject setUid: newUid]; } if ([[SOGoSystemDefaults sharedSystemDefaults] enableEMailAlarms]) { SOGoEMailAlarmsManager *eaMgr; eaMgr = [SOGoEMailAlarmsManager sharedEMailAlarmsManager]; [eaMgr handleAlarmsInCalendar: [newObject parent] fromComponent: self]; } } - (NSException *) saveCalendar: (iCalCalendar *) newCalendar { [super saveComponent: newCalendar]; return nil; } - (NSException *) saveComponent: (iCalRepeatableEntityObject *) newObject { return [self saveCalendar: [newObject parent]]; } /* raw saving */ /* EMail Notifications */ - (NSString *) homePageURLForPerson: (iCalPerson *) _person { NSString *baseURL; NSString *uid; NSArray *traversalObjects; /* generate URL from traversal stack */ traversalObjects = [context objectTraversalStack]; if ([traversalObjects count] > 0) baseURL = [[traversalObjects objectAtIndex:0] baseURLInContext: context]; else { baseURL = @"http://localhost/"; [self warnWithFormat:@"Unable to create baseURL from context!"]; } uid = [_person uid]; return ((uid) ? [NSString stringWithFormat:@"%@%@", baseURL, uid] : nil); } - (NSTimeZone *) timeZoneForUser: (NSString *) email { NSString *uid; SOGoUserDefaults *ud; uid = [[SOGoUserManager sharedUserManager] getUIDForEmail: email]; ud = [[SOGoUser userWithLogin: uid] userDefaults]; return [ud timeZone]; } - (NGMimeBodyPart *) _bodyPartForICalObject: (iCalRepeatableEntityObject *) object { NGMimeBodyPart *bodyPart; NGMutableHashMap *headerMap; NSString *iCalString, *header, *charset; NSData *objectData; iCalCalendar *parent; parent = [object parent]; iCalString = [NSString stringWithFormat: @"%@\r\n", [parent versitString]]; if ([iCalString canBeConvertedToEncoding: NSISOLatin1StringEncoding]) { objectData = [iCalString dataUsingEncoding: NSISOLatin1StringEncoding]; charset = @"ISO-8859-1"; } else { objectData = [iCalString dataUsingEncoding: NSUTF8StringEncoding]; charset = @"UTF-8"; } header = [NSString stringWithFormat: @"text/calendar; method=%@;" @" charset=\"%@\"", [(iCalCalendar *) [object parent] method], charset]; headerMap = [NGMutableHashMap hashMapWithCapacity: 3]; [headerMap setObject: @"urn:content-classes:calendarmessage" forKey: @"Content-Class"]; [headerMap setObject: header forKey: @"content-type"]; [headerMap setObject: @"quoted-printable" forKey: @"content-transfer-encoding"]; bodyPart = [NGMimeBodyPart bodyPartWithHeader: headerMap]; [bodyPart setBody: [objectData dataByEncodingQuotedPrintable]]; return bodyPart; } // // // - (void) sendEMailUsingTemplateNamed: (NSString *) newPageName forObject: (iCalRepeatableEntityObject *) object previousObject: (iCalRepeatableEntityObject *) previousObject toAttendees: (NSArray *) attendees withType: (NSString *) msgType { NSString *pageName; NSString *senderEmail, *shortSenderEmail, *email; WOApplication *app; unsigned i, count; iCalPerson *attendee; NSString *recipient; SOGoAptMailNotification *p; NSString *mailDate, *subject, *text; NGMutableHashMap *headerMap; NGMimeMessage *msg; NGMimeBodyPart *bodyPart, *eventBodyPart; NGMimeMultipartBody *body; SOGoUser *ownerUser; SOGoDomainDefaults *dd; // If defined, we return immediately. When not defined, we send the notifications correctly if ([object firstChildWithTag: @"X-SOGo-Send-Appointment-Notifications"]) return; ownerUser = [SOGoUser userWithLogin: owner]; dd = [ownerUser domainDefaults]; if ([dd appointmentSendEMailNotifications] && [object isStillRelevant]) { count = [attendees count]; if (count) { /* sender */ shortSenderEmail = [[object organizer] rfc822Email]; if ([shortSenderEmail length]) { senderEmail = [[object organizer] mailAddress]; } else { shortSenderEmail = [[previousObject organizer] rfc822Email]; senderEmail = [[previousObject organizer] mailAddress]; } /* calendar part */ eventBodyPart = [self _bodyPartForICalObject: object]; /* get WOApplication instance */ app = [WOApplication application]; /* generate dynamic message content */ for (i = 0; i < count; i++) { attendee = [attendees objectAtIndex: i]; // Don't send a notification to the event organizer nor a deletion // notification to an attendee who already declined the invitation. if (![[attendee uid] isEqualToString: owner] && !([[attendee partStat] compare: @"DECLINED"] == NSOrderedSame && [newPageName compare: @"Deletion"] == NSOrderedSame)) { /* construct recipient */ recipient = [attendee mailAddress]; email = [attendee rfc822Email]; #warning this could be optimized in a class hierarchy common with the \ SOGoObject acl notification mechanism /* create page name */ pageName = [NSString stringWithFormat: @"SOGoAptMail%@", newPageName]; /* construct message content */ p = [app pageWithName: pageName inContext: context]; [p setApt: (iCalEvent *) object]; [p setPreviousApt: (iCalEvent *) previousObject]; if ([[object organizer] cn] && [[[object organizer] cn] length]) { [p setOrganizerName: [[object organizer] cn]]; } else { [p setOrganizerName: [ownerUser cn]]; } subject = [[p getSubject] asQPSubjectString: @"UTF-8"]; 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. */ #warning SOPE is just plain stupid here - if you change the case of keys, it will break the encoding of fields [headerMap setObject: @"multipart/mixed" forKey: @"content-type"]; [headerMap setObject: @"1.0" forKey: @"MIME-Version"]; [headerMap setObject: senderEmail forKey: @"from"]; [headerMap setObject: recipient forKey: @"to"]; mailDate = [[NSCalendarDate date] rfc822DateString]; [headerMap setObject: mailDate forKey: @"date"]; [headerMap setObject: subject forKey: @"subject"]; [headerMap setObject: [NSString generateMessageID] forKey: @"message-id"]; if ([msgType length] > 0) [headerMap setObject: msgType forKey: @"x-sogo-message-type"]; msg = [NGMimeMessage messageWithHeader: headerMap]; /* multipart body */ body = [[NGMimeMultipartBody alloc] initWithPart: msg]; /* text part */ headerMap = [NGMutableHashMap hashMapWithCapacity: 1]; [headerMap setObject: @"text/html; charset=utf-8" forKey: @"content-type"]; bodyPart = [NGMimeBodyPart bodyPartWithHeader: headerMap]; [bodyPart setBody: [text dataUsingEncoding: NSUTF8StringEncoding]]; /* attach text part to multipart body */ [body addBodyPart: bodyPart]; /* attach calendar part to multipart body */ [body addBodyPart: eventBodyPart]; /* attach multipart body to message */ [msg setBody: body]; [body release]; /* send the damn thing */ [[SOGoMailer mailerWithDomainDefaults: dd] sendMimePart: msg toRecipients: [NSArray arrayWithObject: email] sender: shortSenderEmail withAuthenticator: [self authenticatorInContext: context] inContext: context]; } } } } } #warning fix this when sendEmailUsing blabla has been cleaned up - (void) sendIMIPReplyForEvent: (iCalRepeatableEntityObject *) event from: (SOGoUser *) from to: (iCalPerson *) recipient { NSString *pageName, *mailDate, *email; WOApplication *app; iCalPerson *attendee; SOGoAptMailICalReply *p; NGMutableHashMap *headerMap; NGMimeMessage *msg; NGMimeBodyPart *bodyPart; NGMimeMultipartBody *body; NSData *bodyData; SOGoDomainDefaults *dd; dd = [from domainDefaults]; if ([dd appointmentSendEMailNotifications] && [event isStillRelevant]) { /* get WOApplication instance */ app = [WOApplication application]; /* create page name */ pageName = @"SOGoAptMailICalReply"; /* construct message content */ p = [app pageWithName: pageName inContext: context]; [p setApt: (iCalEvent *) event]; attendee = [event userAsAttendee: from]; [p setAttendee: attendee]; /* 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. */ #warning SOPE is just plain stupid here - if you change the case of keys, it will break the encoding of fields [headerMap setObject: [attendee mailAddress] forKey: @"from"]; [headerMap setObject: [recipient mailAddress] forKey: @"to"]; mailDate = [[NSCalendarDate date] rfc822DateString]; [headerMap setObject: mailDate forKey: @"date"]; [headerMap setObject: [[p getSubject] asQPSubjectString: @"UTF-8"] forKey: @"subject"]; [headerMap setObject: [NSString generateMessageID] forKey: @"message-id"]; [headerMap setObject: @"1.0" forKey: @"MIME-Version"]; [headerMap setObject: @"multipart/mixed" forKey: @"content-type"]; [headerMap setObject: @"calendar:invitation-reply" forKey: @"x-sogo-message-type"]; msg = [NGMimeMessage messageWithHeader: headerMap]; /* multipart body */ body = [[NGMimeMultipartBody alloc] initWithPart: msg]; /* text part */ headerMap = [NGMutableHashMap hashMapWithCapacity: 1]; [headerMap setObject: @"text/html; charset=utf-8" forKey: @"content-type"]; bodyPart = [NGMimeBodyPart bodyPartWithHeader: headerMap]; bodyData = [[p getBody] dataUsingEncoding: NSUTF8StringEncoding]; [bodyPart setBody: bodyData]; /* attach text part to multipart body */ [body addBodyPart: bodyPart]; /* attach calendar part to multipart body */ [body addBodyPart: [self _bodyPartForICalObject: event]]; /* attach multipart body to message */ [msg setBody: body]; [body release]; /* send the damn thing */ email = [recipient rfc822Email]; [[SOGoMailer mailerWithDomainDefaults: dd] sendMimePart: msg toRecipients: [NSArray arrayWithObject: email] sender: [attendee rfc822Email] withAuthenticator: [self authenticatorInContext: context] inContext: context]; } } // // // - (void) sendResponseToOrganizer: (iCalRepeatableEntityObject *) newComponent from: (SOGoUser *) from { iCalPerson *organizer, *attendee; iCalEvent *event; SOGoUser *ownerUser; event = [newComponent itipEntryWithMethod: @"reply"]; ownerUser = [SOGoUser userWithLogin: owner]; if (![event userIsOrganizer: ownerUser]) { organizer = [event organizer]; attendee = [event userAsAttendee: ownerUser]; [event setAttendees: [NSArray arrayWithObject: attendee]]; [self sendIMIPReplyForEvent: event from: from to: organizer]; } } // // // - (void) sendReceiptEmailForObject: (iCalRepeatableEntityObject *) object addedAttendees: (NSArray *) theAddedAttendees deletedAttendees: (NSArray *) theDeletedAttendees updatedAttendees: (NSArray *) theUpdatedAttendees operation: (SOGoEventOperation) theOperation { NSString *calendarName, *mailDate, *mailText, *fullSenderEmail, *senderEmail, *fullRecipientEmail, *recipientEmail; NSDictionary *senderIdentity, *recipientIdentity; id authenticator; SOGoUser *currentUser, *ownerUser; NGMutableHashMap *headerMap; SOGoAptMailReceipt *page; SOGoDomainDefaults *dd; NGMimeMessage *msg; calendarName = [[self container] displayName]; // We must handle three cases here: // - Receive a mail when I modify my calendar // - Receive a mail when someone else modifies my calendar // - When I modify my calendar, send a mail to: // We first built the template that we'll slightly adjust for our three use-cases page = [[WOApplication application] pageWithName: @"SOGoAptMailReceipt" inContext: context]; [page setApt: (iCalEvent *) object]; [page setAddedAttendees: theAddedAttendees]; [page setDeletedAttendees: theDeletedAttendees]; [page setUpdatedAttendees: theUpdatedAttendees]; [page setOperation: theOperation]; [page setCalendarName: calendarName]; currentUser = [context activeUser]; senderIdentity = [currentUser primaryIdentity]; dd = [currentUser domainDefaults]; #warning SOPE is just plain stupid here - if you change the case of keys, it will break the encoding of fields headerMap = [NGMutableHashMap hashMapWithCapacity: 5]; // Sender can vary, base on whom modifies the actual event (owner vs. someone else) senderEmail = [senderIdentity objectForKey: @"email"]; fullSenderEmail = [senderIdentity keysWithFormat: @"%{fullName} <%{email}>"]; [headerMap setObject: fullSenderEmail forKey: @"from"]; // Recipient is fixed, which is the calendar owner ownerUser = [SOGoUser userWithLogin: self->owner]; recipientIdentity = [ownerUser primaryIdentity]; // Safety net for broken configurations if (!recipientIdentity) return; recipientEmail = [recipientIdentity objectForKey: @"email"]; fullRecipientEmail = [recipientIdentity keysWithFormat: @"%{fullName} <%{email}>"]; [headerMap setObject: fullRecipientEmail forKey: @"to"]; mailDate = [[NSCalendarDate date] rfc822DateString]; [headerMap setObject: mailDate forKey: @"date"]; [headerMap setObject: [page getSubject] forKey: @"subject"]; [headerMap setObject: [NSString generateMessageID] forKey: @"message-id"]; [headerMap setObject: @"1.0" forKey: @"MIME-Version"]; [headerMap setObject: @"text/html; charset=utf-8" forKey: @"content-type"]; msg = [NGMimeMessage messageWithHeader: headerMap]; /* text part */ mailText = [page getBody]; [msg setBody: [mailText dataUsingEncoding: NSUTF8StringEncoding]]; authenticator = [self authenticatorInContext: context]; if ([self->owner isEqualToString: [currentUser login]]) { if ([[self container] notifyOnPersonalModifications]) { [[SOGoMailer mailerWithDomainDefaults: dd] sendMimePart: msg toRecipients: [NSArray arrayWithObject: recipientEmail] sender: senderEmail withAuthenticator: authenticator inContext: context]; } if ([[self container] notifyUserOnPersonalModifications]) { id o; o = [headerMap objectForKey: @"to"]; recipientEmail = [[self container] notifiedUserOnPersonalModifications]; [headerMap setObject: recipientEmail forKey: @"to"]; [[SOGoMailer mailerWithDomainDefaults: dd] sendMimePart: msg toRecipients: [NSArray arrayWithObject: recipientEmail] sender: senderEmail withAuthenticator: authenticator inContext: context]; [headerMap setObject: o forKey: @"to"]; } } if ([[self container] notifyOnExternalModifications] && ![self->owner isEqualToString: [currentUser login]]) { [[SOGoMailer mailerWithDomainDefaults: dd] sendMimePart: msg toRecipients: [NSArray arrayWithObject: recipientEmail] sender: senderEmail withAuthenticator: authenticator inContext: context]; } } // // // - (iCalPerson *) findParticipantWithUID: (NSString *) uid { iCalEntityObject *component; SOGoUser *user; user = [SOGoUser userWithLogin: uid]; component = [self component: NO secure: NO]; return [component userAsAttendee: user]; } // // // - (iCalPerson *) iCalPersonWithUID: (NSString *) uid { iCalPerson *person; SOGoUserManager *um; NSDictionary *contactInfos; um = [SOGoUserManager sharedUserManager]; contactInfos = [um contactInfosForUserWithUIDorEmail: uid]; person = [iCalPerson new]; [person autorelease]; [person setCn: [contactInfos objectForKey: @"cn"]]; [person setEmail: [contactInfos objectForKey: @"c_email"]]; return person; } // // // - (NSArray *) getUIDsForICalPersons: (NSArray *) iCalPersons { iCalPerson *currentPerson; NSEnumerator *persons; NSMutableArray *uids; NSString *uid; uids = [NSMutableArray array]; persons = [iCalPersons objectEnumerator]; while ((currentPerson = [persons nextObject])) { uid = [currentPerson uid]; if (uid) [uids addObject: uid]; } return uids; } - (NSException *) copyToFolder: (SOGoGCSFolder *) newFolder { return [self copyComponent: [self calendar: NO secure: NO] toFolder: newFolder]; } - (NSException *) copyComponent: (iCalCalendar *) calendar toFolder: (SOGoGCSFolder *) newFolder { NSArray *elements; NSString *newUID; unsigned int count, max; SOGoCalendarComponent *newComponent; newUID = [self globallyUniqueObjectId]; elements = [calendar allObjects]; max = [elements count]; for (count = 0; count < max; count++) [[elements objectAtIndex: count] setUid: newUID]; newComponent = [[self class] objectWithName: [NSString stringWithFormat: @"%@.ics", newUID] inContainer: newFolder]; return [newComponent saveCalendar: calendar]; } - (NSException *) moveToFolder: (SOGoGCSFolder *) newFolder { NSException *ex; ex = [self copyToFolder: newFolder]; if (!ex) ex = [self delete]; return ex; } #warning Should we not remove the concept of Organizer and Participant roles? - (NSString *) _roleOfOwner: (iCalRepeatableEntityObject *) component { NSString *role; iCalPerson *organizer; SOGoUser *ownerUser; if (isNew) role = SOGoCalendarRole_Organizer; else { organizer = [component organizer]; if ([[organizer rfc822Email] length] > 0) { ownerUser = [SOGoUser userWithLogin: owner]; if ([component userIsOrganizer: ownerUser]) role = SOGoCalendarRole_Organizer; else if ([component userIsAttendee: ownerUser]) role = SOGoCalendarRole_Participant; else role = SOGoRole_None; } else role = SOGoCalendarRole_Organizer; } return role; } - (NSString *) _compiledRoleForOwner: (NSString *) ownerRole andUser: (NSString *) userRole { NSString *role; if ([userRole isEqualToString: SOGoCalendarRole_ComponentModifier] || ([userRole isEqualToString: SOGoCalendarRole_ComponentResponder] && [ownerRole isEqualToString: SOGoCalendarRole_Participant])) role = ownerRole; else role = SOGoRole_None; return role; } - (NSArray *) aclsForUser: (NSString *) uid { NSMutableArray *roles; NSArray *superAcls; iCalRepeatableEntityObject *component; NSString *accessRole, *ownerRole; SOGoUser *aclUser; roles = [NSMutableArray array]; superAcls = [super aclsForUser: uid]; if ([superAcls count] > 0) [roles addObjectsFromArray: superAcls]; component = [self component: NO secure: NO]; ownerRole = [self _roleOfOwner: component]; if ([owner isEqualToString: uid]) [roles addObject: ownerRole]; else { if (isNew) { if ([roles containsObject: SOGoRole_ObjectCreator]) [roles addObject: SOGoCalendarRole_Organizer]; } else { if (component) { aclUser = [SOGoUser userWithLogin: uid]; if ([component userIsOrganizer: aclUser]) [roles addObject: SOGoCalendarRole_Organizer]; else if ([component userIsAttendee: aclUser]) [roles addObject: SOGoCalendarRole_Participant]; accessRole = [container roleForComponentsWithAccessClass: [component symbolicAccessClass] forUser: uid]; if ([accessRole length] > 0) { [roles addObject: accessRole]; [roles addObject: [self _compiledRoleForOwner: ownerRole andUser: accessRole]]; } } } } return roles; } - (void) snoozeAlarm: (unsigned int) minutes { NSDictionary *quickFields; GCSFolder *folder; unsigned int nextAlarm; folder = [[self container] ocsFolder]; if (!folder) { [self errorWithFormat:@"(%s): missing folder for update!", __PRETTY_FUNCTION__]; return; } nextAlarm = [[NSCalendarDate calendarDate] timeIntervalSince1970] + minutes * 60; quickFields = [NSDictionary dictionaryWithObject: [NSNumber numberWithInt: nextAlarm] forKey: @"c_nextalarm"]; [folder updateQuickFields: quickFields whereColumn: @"c_name" isEqualTo: nameInContainer]; } /* SOGoComponentOccurence protocol */ - (iCalRepeatableEntityObject *) occurence { return [self component: YES secure: NO]; } #warning alarms: we do not handle occurrences - (NSException *) prepareDelete { if ([[SOGoSystemDefaults sharedSystemDefaults] enableEMailAlarms]) { SOGoEMailAlarmsManager *eaMgr; eaMgr = [SOGoEMailAlarmsManager sharedEMailAlarmsManager]; [eaMgr deleteAlarmsFromComponent: self]; } return nil; } - (id) PUTAction: (WOContext *) localContext { WORequest *rq; iCalCalendar *putCalendar; rq = [localContext request]; putCalendar = [iCalCalendar parseSingleFromSource: [rq contentAsString]]; if ([[SOGoSystemDefaults sharedSystemDefaults] enableEMailAlarms]) { SOGoEMailAlarmsManager *eaMgr; eaMgr = [SOGoEMailAlarmsManager sharedEMailAlarmsManager]; [eaMgr handleAlarmsInCalendar: putCalendar fromComponent: self]; } return [super PUTAction: localContext]; } // /* Overriding this method dramatically speeds up PROPFIND request, but may // otherwise be a bad idea... Wait and see. */ // - (NSDictionary*) valuesForKeys: (NSArray*)keys // { // NSMutableDictionary *values; // values = [NSMutableDictionary dictionaryWithCapacity: [keys count]]; // [values setObject: [self davCreationDate] forKey: @"davCreationDate"]; // [values setObject: [self davContentLength] forKey: @"davContentLength"]; // [values setObject: [self davLastModified] forKey: @"davLastModified"]; // [values setObject: @"text/calendar" forKey: @"davContentType"]; // [values setObject: [self baseURL] forKey: @"davURL"]; // return values; // } @end