sogo/OpenChange/MAPIStoreCalendarMessage.m

679 lines
21 KiB
Objective-C

/* MAPIStoreCalendarMessage.m - this file is part of SOGo
*
* Copyright (C) 2011-2012 Inverse inc
*
* Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
*
* 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 3, 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.
*/
/* TODO:
- merge common code with tasks
- take the tz definitions from Outlook */
#include <talloc.h>
#include <util/attr.h>
#import <Foundation/NSArray.h>
#import <Foundation/NSCalendarDate.h>
#import <Foundation/NSData.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSString.h>
#import <Foundation/NSTimeZone.h>
#import <EOControl/EOQualifier.h>
#import <EOControl/EOFetchSpecification.h>
#import <NGObjWeb/WOContext+SoObjects.h>
#import <NGExtensions/NSObject+Logs.h>
#import <GDLContentStore/GCSFolder.h>
#import <NGCards/iCalAlarm.h>
#import <NGCards/iCalCalendar.h>
#import <NGCards/iCalEvent.h>
#import <NGCards/iCalDateTime.h>
#import <NGCards/iCalPerson.h>
#import <NGCards/iCalTimeZone.h>
#import <NGCards/iCalTrigger.h>
#import <SOGo/SOGoPermissions.h>
#import <SOGo/SOGoUser.h>
#import <SOGo/SOGoUserManager.h>
#import <Appointments/SOGoAppointmentFolder.h>
#import <Appointments/SOGoAppointmentObject.h>
#import <Appointments/iCalEntityObject+SOGo.h>
#import <Mailer/NSString+Mail.h>
#import "iCalEvent+MAPIStore.h"
#import "MAPIStoreAppointmentWrapper.h"
#import "MAPIStoreCalendarAttachment.h"
#import "MAPIStoreCalendarFolder.h"
#import "MAPIStoreContext.h"
#import "MAPIStoreMapping.h"
#import "MAPIStoreRecurrenceUtils.h"
#import "MAPIStoreTypes.h"
#import "MAPIStoreUserContext.h"
#import "NSDate+MAPIStore.h"
#import "NSData+MAPIStore.h"
#import "NSObject+MAPIStore.h"
#import "NSString+MAPIStore.h"
#import "NSValue+MAPIStore.h"
#import "MAPIStoreCalendarMessage.h"
#undef DEBUG
#include <stdbool.h>
#include <gen_ndr/exchange.h>
#include <gen_ndr/property.h>
#include <libmapi/libmapi.h>
#include <mapistore/mapistore.h>
#include <mapistore/mapistore_errors.h>
#include <mapistore/mapistore_nameid.h>
// extern void ndr_print_AppointmentRecurrencePattern(struct ndr_print *ndr, const char *name, const struct AppointmentRecurrencePattern *r);
static Class NSArrayK, MAPIStoreAppointmentWrapperK;
@implementation SOGoAppointmentObject (MAPIStoreExtension)
- (Class) mapistoreMessageClass
{
return [MAPIStoreCalendarMessage class];
}
@end
@implementation MAPIStoreCalendarMessage
+ (void) initialize
{
NSArrayK = [NSArray class];
MAPIStoreAppointmentWrapperK = [MAPIStoreAppointmentWrapper class];
}
+ (enum mapistore_error) getAvailableProperties: (struct SPropTagArray **) propertiesP
inMemCtx: (TALLOC_CTX *) memCtx
{
BOOL listedProperties[65536];
NSUInteger count;
uint16_t propId;
memset (listedProperties, NO, 65536 * sizeof (BOOL));
[super getAvailableProperties: propertiesP inMemCtx: memCtx];
for (count = 0; count < (*propertiesP)->cValues; count++)
{
propId = ((*propertiesP)->aulPropTag[count] >> 16) & 0xffff;
listedProperties[propId] = YES;
}
[MAPIStoreAppointmentWrapper fillAvailableProperties: *propertiesP
withExclusions: listedProperties];
return MAPISTORE_SUCCESS;
}
- (id) init
{
if ((self = [super init]))
{
calendar = nil;
masterEvent = nil;
}
return self;
}
- (void) _setupAttachmentParts
{
NSUInteger count, max;
NSArray *events;
NSString *newKey;
MAPIStoreCalendarAttachment *attachment;
NSUInteger aid;
iCalEvent *event;
events = [calendar events];
max = [events count];
for (count = 1; count < max; count++)
{
attachment = [MAPIStoreCalendarAttachment
mapiStoreObjectInContainer: self];
/* we now that there are no attachments yet, so we can assume that the
right AID is 0 from the start */
aid = count - 1;
[attachment setAID: aid];
event = [events objectAtIndex: count];
[attachment setEvent: event];
newKey = [[event uniqueChildWithTag: @"recurrence-id"]
flattenedValuesForKey: @""];
[attachmentParts setObject: attachment forKey: newKey];
}
}
- (id) initWithSOGoObject: (id) newSOGoObject
inContainer: (MAPIStoreObject *) newFolder
{
MAPIStoreContext *context;
MAPIStoreUserContext *userContext;
iCalCalendar *origCalendar;
MAPIStoreAppointmentWrapper *appointmentWrapper;
if ((self = [super initWithSOGoObject: newSOGoObject
inContainer: newFolder]))
{
if ([newSOGoObject isNew])
{
ASSIGN (calendar, [iCalCalendar groupWithTag: @"vcalendar"]);
[calendar setVersion: @"2.0"];
[calendar setProdID: @"-//Inverse inc.//OpenChange+SOGo//EN"];
masterEvent = [iCalEvent groupWithTag: @"vevent"];
[calendar addChild: masterEvent];
[masterEvent setCreated: [NSCalendarDate date]];
}
else
{
origCalendar = [sogoObject calendar: YES secure: YES];
if (!origCalendar)
{
[self errorWithFormat: @"Incorrect calendar event %@. Empty message is created",
[self url]];
return self;
}
calendar = [origCalendar mutableCopy];
masterEvent = [[calendar events] objectAtIndex: 0];
[self _setupAttachmentParts];
}
context = [self context];
userContext = [self userContext];
appointmentWrapper
= [MAPIStoreAppointmentWrapper wrapperWithICalEvent: masterEvent
andUser: [userContext sogoUser]
andSenderEmail: nil
withConnectionInfo: [context connectionInfo]];
[self addProxy: appointmentWrapper];
}
return self;
}
- (void) dealloc
{
//NSLog(@"MAPIStoreCalendarMessage: -dealloc (%p)", self);
[calendar release];
[super dealloc];
}
- (MAPIStoreAppointmentWrapper *) _appointmentWrapper
{
NSUInteger i, max;
id proxy;
max = [proxies count];
for (i = 0; i < max; i++) {
proxy = [proxies objectAtIndex: i];
if ([proxy isKindOfClass: MAPIStoreAppointmentWrapperK])
{
return proxy;
}
}
return nil;
}
/* getters */
- (int) getPidTagMessageClass: (void **) data
inMemCtx: (TALLOC_CTX *) memCtx
{
SOGoUser *owner;
owner = [[self userContext] sogoUser];
if ([masterEvent userAsAttendee: owner])
*data = talloc_strdup (memCtx, "IPM.Schedule.Meeting.Request");
else
*data = talloc_strdup (memCtx, "IPM.Appointment");
return MAPISTORE_SUCCESS;
}
- (int) getPidLidSideEffects: (void **) data // TODO
inMemCtx: (TALLOC_CTX *) memCtx
{
*data = MAPILongValue (memCtx,
seOpenToDelete | seOpenToCopy | seOpenToMove
| seCoerceToInbox | seOpenForCtxMenu);
return MAPISTORE_SUCCESS;
}
- (int) getPidTagProcessed: (void **) data inMemCtx: (TALLOC_CTX *) memCtx
{
return [self getYes: data inMemCtx: memCtx];
}
- (void) getMessageData: (struct mapistore_message **) dataPtr
inMemCtx: (TALLOC_CTX *) memCtx
{
static NSString *recTypes[] = {@"orig", @"to", @"cc", @"bcc"};
static NSInteger recTypesValue[] = {MAPI_ORIG, MAPI_TO, MAPI_CC, MAPI_BCC};
struct mapistore_message *msgData;
NSDictionary *recipients, *contactInfos;
NSString *email;
NSArray *attendees;
NSDictionary *attendee;
SOGoUserManager *mgr;
struct mapistore_message_recipient *recipient;
NSUInteger nRecType, maxRecTypes, count, max, propCount;
NSObject *currentValue;
[super getMessageData: &msgData inMemCtx: memCtx];
/* The presence of the "recipients" meta-property means that our most recent
list of recipients has not been saved yet and that we must return that
one instead of the one stored in the event. */
recipients = [properties objectForKey: @"recipients"];
if (recipients)
{
mgr = [SOGoUserManager sharedUserManager];
/* TODO: this code might need to be moved into MAPIStoreMessage */
msgData->columns = set_SPropTagArray (msgData, 9,
PR_OBJECT_TYPE,
PR_DISPLAY_TYPE,
PR_7BIT_DISPLAY_NAME_UNICODE,
PR_SMTP_ADDRESS_UNICODE,
PR_SEND_INTERNET_ENCODING,
PR_RECIPIENT_DISPLAY_NAME_UNICODE,
PR_RECIPIENT_FLAGS,
PR_RECIPIENT_ENTRYID,
PR_RECIPIENT_TRACKSTATUS);
maxRecTypes = sizeof(recTypes) / sizeof(recTypes[0]);
for (nRecType = 0; nRecType < maxRecTypes; nRecType++)
{
attendees = [recipients objectForKey: recTypes[nRecType]];
max = [attendees count];
if (max > 0)
{
msgData->recipients_count += max;
msgData->recipients = talloc_realloc(msgData,
msgData->recipients, struct
mapistore_message_recipient, msgData->recipients_count);
recipient = msgData->recipients;
for (count = 0; count < max; count++)
{
attendee = [attendees objectAtIndex: count];
recipient->type = recTypesValue[nRecType];
email = [attendee objectForKey: @"email"];
if (email)
{
contactInfos
= [mgr contactInfosForUserWithUIDorEmail: email];
recipient->username = [[contactInfos
objectForKey: @"c_uid"]
asUnicodeInMemCtx: msgData];
}
else
recipient->username = NULL;
recipient->data = talloc_array(msgData, void *,
msgData->columns->cValues);
for (propCount = 0;
propCount < msgData->columns->cValues;
propCount++)
{
currentValue = [attendee objectForKey: MAPIPropertyKey (msgData->columns->aulPropTag[propCount])];
[currentValue getValue: recipient->data + propCount
forTag: msgData->columns->aulPropTag[propCount]
inMemCtx: msgData];
}
recipient++;
}
}
}
}
else
{
[[self _appointmentWrapper] fillMessageData: msgData
inMemCtx: memCtx];
}
*dataPtr = msgData;
}
- (int) getPidTagResponseRequested: (void **) data
inMemCtx: (TALLOC_CTX *) memCtx
{
return [self getYes: data inMemCtx: memCtx];
}
/* This three methods: getPidTagNormalizedSubject,
getPidTagSensitivity and getPidTagImportance are implemented in
MAPIStoreMessage base class, then the proxy method is not reached
(see MAPIStoreObject).
*/
- (int) getPidTagNormalizedSubject: (void **) data
inMemCtx: (TALLOC_CTX *) memCtx
{
MAPIStoreAppointmentWrapper *appointmentWrapper;
appointmentWrapper = [self _appointmentWrapper];
if (appointmentWrapper)
return [appointmentWrapper getPidTagNormalizedSubject: data inMemCtx: memCtx];
return MAPISTORE_ERR_NOT_FOUND;
}
- (int) getPidTagSensitivity: (void **) data
inMemCtx: (TALLOC_CTX *) memCtx
{
MAPIStoreAppointmentWrapper *appointmentWrapper;
appointmentWrapper = [self _appointmentWrapper];
if (appointmentWrapper)
return [appointmentWrapper getPidTagSensitivity: data inMemCtx: memCtx];
return [self getLongZero: data inMemCtx: memCtx];
}
- (int) getPidTagImportance: (void **) data
inMemCtx: (TALLOC_CTX *) memCtx
{
MAPIStoreAppointmentWrapper *appointmentWrapper;
appointmentWrapper = [self _appointmentWrapper];
if (appointmentWrapper)
return [appointmentWrapper getPidTagImportance: data inMemCtx: memCtx];
*data = MAPILongValue (memCtx, 1);
return MAPISTORE_SUCCESS;
}
- (NSString *) _uidFromGlobalObjectId: (TALLOC_CTX *) memCtx
{
NSData *objectId;
NSString *uid = nil;
char *bytesDup, *uidStart;
NSUInteger length;
/* NOTE: we only handle the generic case at the moment, see
MAPIStoreAppointmentWrapper */
objectId = [properties
objectForKey: MAPIPropertyKey (PidLidGlobalObjectId)];
if (objectId)
{
length = [objectId length];
bytesDup = talloc_array (memCtx, char, length + 1);
memcpy (bytesDup, [objectId bytes], length);
bytesDup[length] = 0;
uidStart = bytesDup + length - 1;
while (uidStart != bytesDup && *(uidStart - 1))
uidStart--;
if (uidStart > bytesDup && *uidStart)
uid = [NSString stringWithUTF8String: uidStart];
talloc_free (bytesDup);
}
return uid;
}
- (SOGoAppointmentObject *) _resurrectRecord: (NSString *) cname
fromFolder: (SOGoAppointmentFolder *) folder
{
NSArray *records;
EOQualifier *qualifier;
EOFetchSpecification *fs;
GCSFolder *ocsFolder;
SOGoAppointmentObject *newObject;
NSMutableDictionary *newRecord;
static NSArray *childRecordFields = nil;
if (!childRecordFields)
{
childRecordFields = [NSArray arrayWithObjects: @"c_name",
@"c_creationdate", @"c_lastmodified",
@"c_content", nil];
[childRecordFields retain];
}
ocsFolder = [folder ocsFolder];
qualifier
= [EOQualifier qualifierWithQualifierFormat:
[NSString stringWithFormat: @"c_name='%@'", cname]];
fs = [EOFetchSpecification fetchSpecificationWithEntityName: [ocsFolder folderName]
qualifier: qualifier
sortOrderings: nil];
records = [ocsFolder fetchFields: childRecordFields
fetchSpecification: fs
ignoreDeleted: NO];
if ([records isKindOfClass: NSArrayK] && [records count])
{
newRecord = [[records objectAtIndex: 0] mutableCopy];
[newRecord setObject: [NSNumber numberWithInt: 0]
forKey: @"c_version"];
newObject = [SOGoAppointmentObject objectWithRecord: newRecord
inContainer: folder];
[newRecord autorelease];
[newObject setIsNew: NO];
}
else
newObject = nil;
return newObject;
}
- (void) _fixupAppointmentObjectWithUID: (NSString *) uid
{
NSString *cname, *url;
MAPIStoreMapping *mapping;
uint64_t objectId;
WOContext *woContext;
SOGoAppointmentFolder *folder;
SOGoAppointmentObject *newObject;
cname = [[container sogoObject] resourceNameForEventUID: uid];
if (cname)
isNew = NO;
else
cname = [NSString stringWithFormat: @"%@.ics", uid];
mapping = [self mapping];
url = [NSString stringWithFormat: @"%@%@", [container url], cname];
folder = [sogoObject container];
/* reinstantiate the old sogo object and attach it to self */
[[self userContext] activate];
woContext = [[self userContext] woContext];
if (isNew)
{
/* event could have been deleted, let's try to resurrect it */
newObject = [self _resurrectRecord: cname fromFolder: folder];
if (!newObject)
{
newObject = [SOGoAppointmentObject objectWithName: cname
inContainer: folder];
[newObject setIsNew: YES];
}
}
else
{
/* dissociate the object url from the old object's id */
objectId = [mapping idFromURL: url];
[mapping unregisterURLWithID: objectId];
newObject = [folder lookupName: cname
inContext: woContext
acquire: NO];
}
/* dissociate the object url associated with this object, as we want to
discard it */
objectId = [self objectId];
[mapping unregisterURLWithID: objectId];
/* associate the new object url with this object id */
[mapping registerURL: url withID: objectId];
[newObject setContext: woContext];
ASSIGN (sogoObject, newObject);
}
- (BOOL) subscriberCanReadMessage
{
NSArray *roles;
roles = [self activeUserRoles];
return ([roles containsObject: SOGoCalendarRole_ComponentViewer]
|| [roles containsObject: SOGoCalendarRole_ComponentDAndTViewer]
|| [self subscriberCanModifyMessage]);
}
- (BOOL) subscriberCanModifyMessage
{
BOOL rc;
NSArray *roles = [self activeUserRoles];
if (isNew)
rc = [roles containsObject: SOGoRole_ObjectCreator];
else
rc = ([roles containsObject: SOGoCalendarRole_ComponentModifier]
|| [roles containsObject: SOGoCalendarRole_ComponentResponder]);
return rc;
}
- (void) _updateAttachedEvents
{
NSArray *allAttachments;
NSUInteger count, max;
NSString *uid, *summary;
iCalEvent *event;
MAPIStoreCalendarAttachment *attachment;
/* ensure that all exception events have the same UID as the master */
uid = [masterEvent uid];
summary = [masterEvent summary];
allAttachments = [attachmentParts allValues];
max = [allAttachments count];
for (count = 0; count < max; count++)
{
attachment = [allAttachments objectAtIndex: count];
event = [attachment event];
if ([[event summary] length] == 0)
[event setSummary: summary];
[event setUid: uid];
}
}
- (void) save: (TALLOC_CTX *) memCtx
{
// iCalCalendar *vCalendar;
// NSCalendarDate *now;
NSString *uid, *nameInContainer;
// iCalEvent *newEvent;
// iCalPerson *userPerson;
SOGoUser *activeUser;
NSRange rangeOfDot;
if (isNew)
{
uid = [self _uidFromGlobalObjectId: memCtx];
if (uid)
{
/* Hack required because of what's explained in oxocal 3.1.4.7.1:
basically, Outlook creates a copy of the event and then removes
the old instance. We perform a trickery to avoid performing those
operations in the backend, in a way that enables us to recover
the initial instance and act solely on it. */
[self _fixupAppointmentObjectWithUID: uid];
}
else
{
/* We create a UID from the nameInContainer, or the reverse if the
latter is already set... */
nameInContainer = [sogoObject nameInContainer];
if (nameInContainer)
{
rangeOfDot = [nameInContainer rangeOfString: @"."
options: NSBackwardsSearch];
if (rangeOfDot.location == NSNotFound)
uid = nameInContainer;
else
uid = [nameInContainer substringToIndex: rangeOfDot.location];
}
else
{
uid = [SOGoObject globallyUniqueObjectId];
nameInContainer = [NSString stringWithFormat: @"%@.ics", uid];
[sogoObject setNameInContainer: nameInContainer];
}
}
[masterEvent setUid: uid];
}
// [self logWithFormat: @"-save, event props:"];
// MAPIStoreDumpMessageProperties (newProperties);
// now = [NSCalendarDate date];
activeUser = [[self context] activeUser];
[masterEvent updateFromMAPIProperties: properties
inUserContext: [self userContext]
withActiveUser: activeUser
inMemCtx: memCtx];
[self _updateAttachedEvents];
[[self userContext] activate];
[sogoObject updateContentWithCalendar: calendar
fromRequest: nil];
[self updateVersions];
}
- (id) lookupAttachment: (NSString *) childKey
{
return [attachmentParts objectForKey: childKey];
}
- (MAPIStoreAttachment *) createAttachment
{
MAPIStoreCalendarAttachment *newAttachment;
uint32_t newAid;
NSString *newKey;
iCalEvent *newEvent;
newAid = [[self attachmentKeys] count];
newAttachment = [MAPIStoreCalendarAttachment
mapiStoreObjectInContainer: self];
[newAttachment setAID: newAid];
newEvent = [iCalEvent groupWithTag: @"vevent"];
[newAttachment setEvent: newEvent];
[calendar addToEvents: newEvent];
newKey = [NSString stringWithFormat: @"%ul", newAid];
[attachmentParts setObject: newAttachment
forKey: newKey];
[attachmentKeys release];
attachmentKeys = nil;
return newAttachment;
}
- (enum mapistore_error) setReadFlag: (uint8_t) flag
{
return MAPISTORE_SUCCESS;
}
@end