/* MAPIStoreFolder.m - this file is part of SOGo * * Copyright (C) 2011-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 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: main key arrays must be initialized */ #import #import #import #import #import #import #import #import #import #import #import #import #import #import "MAPIStoreActiveTables.h" #import "MAPIStoreContext.h" #import "MAPIStoreFAIMessage.h" #import "MAPIStoreFAIMessageTable.h" #import "MAPIStoreFolder.h" #import "MAPIStoreFolderTable.h" #import "MAPIStoreMapping.h" #import "MAPIStoreMessage.h" #import "MAPIStorePermissionsTable.h" #import "MAPIStoreSamDBUtils.h" #import "MAPIStoreTypes.h" #import "MAPIStoreUserContext.h" #import "NSData+MAPIStore.h" #import "NSDate+MAPIStore.h" #import "NSString+MAPIStore.h" #import "NSObject+MAPIStore.h" #import #import "SOGoMAPIDBMessage.h" #import "SOGoCacheGCSObject+MAPIStore.h" #import #include #undef DEBUG #include #include #include #include Class NSExceptionK, MAPIStoreFAIMessageK, MAPIStoreMessageTableK, MAPIStoreFAIMessageTableK, MAPIStoreFolderTableK; @implementation MAPIStoreFolder + (void) initialize { NSExceptionK = [NSException class]; MAPIStoreFAIMessageK = [MAPIStoreFAIMessage class]; MAPIStoreMessageTableK = [MAPIStoreMessageTable class]; MAPIStoreFAIMessageTableK = [MAPIStoreFAIMessageTable class]; MAPIStoreFolderTableK = [MAPIStoreFolderTable class]; } - (id) init { if ((self = [super init])) { // messageKeys = nil; // faiMessageKeys = nil; // folderKeys = nil; dbFolder = nil; context = nil; // propsFolder = nil; // propsMessage = nil; } return self; } - (void) setupAuxiliaryObjects { NSURL *folderURL; NSMutableString *pathPrefix; NSString *path, *folderName; NSArray *parts; NSUInteger lastPartIdx; MAPIStoreUserContext *userContext; parts = 0; lastPartIdx = 0; folderURL = [NSURL URLWithString: [self url]]; /* note: -[NSURL path] returns an unescaped representation */ path = [folderURL path]; path = [path substringFromIndex: 1]; if ([path length] > 0) { parts = [path componentsSeparatedByString: @"/"]; lastPartIdx = [parts count] - 1; if ([path hasSuffix: @"/"]) lastPartIdx--; folderName = [parts objectAtIndex: lastPartIdx]; } else folderName = [folderURL host]; userContext = [self userContext]; [userContext ensureFolderTableExists]; ASSIGN (dbFolder, [SOGoCacheGCSFolder objectWithName: folderName inContainer: [container dbFolder]]); [dbFolder setTableUrl: [userContext folderTableURL]]; if (!container && [path length] > 0) { pathPrefix = [NSMutableString stringWithCapacity: 64]; [pathPrefix appendFormat: @"/%@", [folderURL host]]; parts = [parts subarrayWithRange: NSMakeRange (0, lastPartIdx)]; if ([parts count] > 0) [pathPrefix appendFormat: @"/%@", [parts componentsJoinedByString: @"/"]]; [dbFolder setPathPrefix: pathPrefix]; } [dbFolder reloadIfNeeded]; /* propsMessage and self share the same properties dictionary */ // ASSIGN (propsMessage, // [SOGoMAPIDBMessage objectWithName: @"properties.plist" // inContainer: dbFolder]); // [propsMessage setObjectType: MAPIInternalCacheObject]; // [propsMessage reloadIfNeeded]; [properties release]; properties = [dbFolder properties]; [properties retain]; [self setupVersionsMessage]; } - (id) initWithSOGoObject: (id) newSOGoObject inContainer: (MAPIStoreObject *) newContainer { /* The instantiation of auxiliary folders is postponed when newContainer is nil since there is no way to deduce the parent url. When setContext: is invoked, it becomes possible again. */ if ((self = [super initWithSOGoObject: newSOGoObject inContainer: newContainer]) && newContainer) { [self setupAuxiliaryObjects]; } return self; } - (void) setContext: (MAPIStoreContext *) newContext { ASSIGN (context, newContext); if (newContext) [self setupAuxiliaryObjects]; } - (MAPIStoreContext *) context { if (!context) [self setContext: (MAPIStoreContext *) [container context]]; return context; } - (void) dealloc { //[self logWithFormat: @"METHOD '%s' (%d)", __FUNCTION__, __LINE__]; // [messageKeys release]; // [faiMessageKeys release]; // [folderKeys release]; // [propsMessage release]; [dbFolder release]; [context release]; [super dealloc]; } - (SOGoCacheGCSFolder *) dbFolder { return dbFolder; } /* backend interface */ // - (SOGoMAPIDBMessage *) propertiesMessage // { // return propsMessage; // } - (uint64_t) objectVersion { NSNumber *value; uint64_t cn; value = [properties objectForKey: MAPIPropertyKey (PidTagChangeNumber)]; if (value) cn = [value unsignedLongLongValue]; else { [self logWithFormat: @"no value for PidTagChangeNumber, adding one now"]; cn = [[self context] getNewChangeNumber]; value = [NSNumber numberWithUnsignedLongLong: cn]; [properties setObject: value forKey: MAPIPropertyKey (PidTagChangeNumber)]; [dbFolder save]; } return cn >> 16; } - (id) lookupFolder: (NSString *) folderKey { MAPIStoreFolder *childFolder; SOGoFolder *sogoFolder; WOContext *woContext; childFolder = nil; if ([[self folderKeys] containsObject: folderKey]) { woContext = [[self userContext] woContext]; /* We activate the user for the context using the root folder context as there are times where the active user is not matching with the one stored in the application context and SOGo object is storing cached data with the wrong user */ [[self userContext] activateWithUser: [woContext activeUser]]; sogoFolder = [sogoObject lookupName: folderKey inContext: woContext acquire: NO]; if (sogoFolder && ![sogoFolder isKindOfClass: NSExceptionK]) { [sogoFolder setContext: woContext]; childFolder = [isa mapiStoreObjectWithSOGoObject: sogoFolder inContainer: self]; } } else childFolder = nil; return childFolder; } - (id) lookupFolderByURL: (NSString *) childURL { MAPIStoreObject *foundObject; NSString *key, *slashLessURL; if ([childURL hasSuffix: @"/"]) slashLessURL = [childURL substringToIndex: [childURL length] - 1]; else slashLessURL = childURL; key = [self childKeyFromURL: slashLessURL]; if (key) foundObject = [self lookupFolder: key]; else foundObject = nil; return foundObject; } - (id) lookupMessage: (NSString *) messageKey { MAPIStoreObject *childMessage = nil; Class messageClass; SOGoObject *msgObject; if (messageKey) { msgObject = [sogoObject lookupName: messageKey inContext: nil acquire: NO]; /* If the lookup in the indexing table works, but the IMAP does not have the message, then the message does not exist in this folder */ if (msgObject && [msgObject isKindOfClass: [SOGoMailObject class]] && ! [(SOGoMailObject *)msgObject doesMailExist]) return nil; if (msgObject && ![msgObject isKindOfClass: NSExceptionK]) { [msgObject setContext: [[self userContext] woContext]]; messageClass = [msgObject mapistoreMessageClass]; childMessage = [messageClass mapiStoreObjectWithSOGoObject: msgObject inContainer: self]; } } return childMessage; } - (id) lookupFAIMessage: (NSString *) messageKey { MAPIStoreObject *childMessage = nil; SOGoObject *msgObject; if (messageKey) { if ([[self faiMessageKeys] containsObject: messageKey]) { msgObject = [dbFolder lookupName: messageKey inContext: nil acquire: NO]; childMessage = [MAPIStoreFAIMessageK mapiStoreObjectWithSOGoObject: msgObject inContainer: self]; } } return childMessage; } - (NSString *) childKeyFromURL: (NSString *) childURL { NSString *baseURL, *subURL, *key = nil; NSArray *parts; NSUInteger partsCount; baseURL = [self url]; if (![baseURL hasSuffix: @"/"]) baseURL = [NSString stringWithFormat: @"%@/", baseURL]; if ([childURL hasPrefix: baseURL]) { subURL = [childURL substringFromIndex: [baseURL length]]; if ([subURL length] > 0) { parts = [subURL componentsSeparatedByString: @"/"]; partsCount = [parts count]; if (partsCount == 1) { key = [[parts objectAtIndex: 0] stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding]; } } } return key; } - (id) lookupMessageByURL: (NSString *) childURL { MAPIStoreObject *foundObject; NSString *key; key = [self childKeyFromURL: childURL]; if (key) { foundObject = [self lookupFAIMessage: key]; if (!foundObject) foundObject = [self lookupMessage: key]; } else foundObject = nil; return foundObject; } - (int) openFolder: (MAPIStoreFolder **) childFolderPtr withFID: (uint64_t) fid { int rc = MAPISTORE_ERR_NOT_FOUND; MAPIStoreFolder *childFolder; MAPIStoreMapping *mapping; NSString *childURL; //[self logWithFormat: @"METHOD '%s' (%d)", __FUNCTION__, __LINE__]; mapping = [self mapping]; childURL = [mapping urlFromID: fid]; if (childURL) { childFolder = [self lookupFolderByURL: childURL]; if (childFolder) { *childFolderPtr = childFolder; rc = MAPISTORE_SUCCESS; } } return rc; } - (int) createFolder: (MAPIStoreFolder **) childFolderPtr withRow: (struct SRow *) aRow andFID: (uint64_t) fid { BOOL mapped; enum mapistore_error rc = MAPISTORE_SUCCESS; MAPIStoreMapping *mapping; NSString *baseURL, *childURL, *folderKey; MAPIStoreFolder *childFolder; SOGoUser *ownerUser; //[self logWithFormat: @"METHOD '%s' (%d)", __FUNCTION__, __LINE__]; ownerUser = [[self userContext] sogoUser]; if ([[context activeUser] isEqual: ownerUser] || [self subscriberCanCreateSubFolders]) { mapping = [self mapping]; childURL = [mapping urlFromID: fid]; if (childURL) rc = MAPISTORE_ERR_EXIST; else { rc = [self createFolder: aRow withFID: fid andKey: &folderKey]; if (rc == MAPISTORE_SUCCESS) { [self cleanupCaches]; baseURL = [self url]; if (![baseURL hasSuffix: @"/"]) baseURL = [NSString stringWithFormat: @"%@/", baseURL]; childURL = [NSString stringWithFormat: @"%@%@/", baseURL, [folderKey stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]]; mapped = [mapping registerURL: childURL withID: fid]; if (!mapped) /* Enforce the creation if the backend does know the fid */ [mapping updateURL: childURL withID: fid]; childFolder = [self lookupFolder: folderKey]; if (childFolder) { [childFolder addPropertiesFromRow: aRow]; *childFolderPtr = childFolder; } else [NSException raise: @"MAPIStoreIOException" format: @"unable to fetch created folder"]; } } } else rc = MAPISTORE_ERR_DENIED; return rc; } - (int) deleteFolder { // TODO: raise exception in case underlying delete fails? // [propsMessage delete]; [dbFolder delete]; [self cleanupCaches]; return MAPISTORE_SUCCESS; } - (int) getChildCount: (uint32_t *) rowCount ofTableType: (enum mapistore_table_type) tableType { NSArray *keys; int rc = MAPISTORE_SUCCESS; //[self logWithFormat: @"METHOD '%s' (%d) -- tableType: %d", //__FUNCTION__, __LINE__, tableType]; if (tableType == MAPISTORE_MESSAGE_TABLE) keys = [self messageKeys]; else if (tableType == MAPISTORE_FOLDER_TABLE) keys = [self folderKeys]; else if (tableType == MAPISTORE_FAI_TABLE) keys = [self faiMessageKeys]; else { keys = nil; rc = MAPISTORE_ERR_NOT_FOUND; } *rowCount = [keys count]; return rc; } - (int) openMessage: (MAPIStoreMessage **) messagePtr withMID: (uint64_t) mid forWriting: (BOOL) readWrite inMemCtx: (TALLOC_CTX *) memCtx; { NSString *messageURL; MAPIStoreMapping *mapping; MAPIStoreMessage *message; SOGoUser *ownerUser; int rc = MAPISTORE_ERR_NOT_FOUND; mapping = [self mapping]; messageURL = [mapping urlFromID: mid]; if (messageURL) { message = [self lookupMessageByURL: messageURL]; if (message) { ownerUser = [[self userContext] sogoUser]; if ([[context activeUser] isEqual: ownerUser] || (readWrite && [message subscriberCanModifyMessage]) || (!readWrite && [message subscriberCanReadMessage])) { *messagePtr = message; rc = MAPISTORE_SUCCESS; } else rc = MAPISTORE_ERR_DENIED; } else { /* Unregistering from indexing table as the backend says the object was not found */ [mapping unregisterURLWithID: mid]; } } return rc; } - (int) createMessage: (MAPIStoreMessage **) messagePtr withMID: (uint64_t) mid isAssociated: (BOOL) isAssociated { enum mapistore_error rc; MAPIStoreMessage *message; NSString *baseURL, *childURL; MAPIStoreMapping *mapping; SOGoUser *ownerUser; //[self logWithFormat: @"METHOD '%s' -- mid: 0x%.16llx associated: %d", // __FUNCTION__, mid, isAssociated]; context = [self context]; ownerUser = [[self userContext] sogoUser]; if ([[context activeUser] isEqual: ownerUser] || (!isAssociated && [self subscriberCanCreateMessages])) { mapping = [self mapping]; if ([mapping urlFromID: mid]) rc = MAPISTORE_ERR_EXIST; else { message = [self createMessage: isAssociated]; if (message) { baseURL = [self url]; if (![baseURL hasSuffix: @"/"]) baseURL = [NSString stringWithFormat: @"%@/", baseURL]; childURL = [NSString stringWithFormat: @"%@%@", baseURL, [message nameInContainer]]; [mapping registerURL: childURL withID: mid]; *messagePtr = message; rc = MAPISTORE_SUCCESS; } else rc = MAPISTORE_ERROR; } } else rc = MAPISTORE_ERR_DENIED; return rc; } - (int) deleteMessageWithMID: (uint64_t) mid andFlags: (uint8_t) flags { NSString *childURL; MAPIStoreMapping *mapping; MAPIStoreMessage *message; NSArray *activeTables; NSUInteger count, max; id msgObject; SOGoUser *ownerUser; int rc; /* flags that control the behaviour of the operation (MAPISTORE_SOFT_DELETE or MAPISTORE_PERMANENT_DELETE) */ [self logWithFormat: @"-deleteMessageWithMID: mid: 0x%.16llx flags: %d", mid, flags]; mapping = [self mapping]; childURL = [mapping urlFromID: mid]; if (childURL) { message = [self lookupMessageByURL: childURL]; if (message) { ownerUser = [[self userContext] sogoUser]; if ([[context activeUser] isEqual: ownerUser] || (![message isKindOfClass: MAPIStoreFAIMessageK] && [self subscriberCanDeleteMessages])) { /* we ensure the table caches are loaded so that old and new state can be compared */ activeTables = ([message isKindOfClass: MAPIStoreFAIMessageK] ? [self activeFAIMessageTables] : [self activeMessageTables]); max = [activeTables count]; for (count = 0; count < max; count++) [[activeTables objectAtIndex: count] restrictedChildKeys]; msgObject = [message sogoObject]; if (([msgObject respondsToSelector: @selector (prepareDelete)] && [msgObject prepareDelete]) || [msgObject delete]) { rc = MAPISTORE_ERROR; [self logWithFormat: @"ERROR deleting object at URL: %@", childURL]; } else { [self logWithFormat: @"successfully deleted object at URL: %@", childURL]; /* Ensure we are respecting flags parameter */ [mapping unregisterURLWithID: mid andFlags: flags]; [self cleanupCaches]; rc = MAPISTORE_SUCCESS; } } else rc = MAPISTORE_ERR_DENIED; } else rc = MAPISTORE_ERR_NOT_FOUND; } else rc = MAPISTORE_ERR_NOT_FOUND; return rc; } // private method - (int) _moveCopyMessageWithMID: (uint64_t) srcMid fromFolder: (MAPIStoreFolder *) sourceFolder withMID: (uint64_t) targetMid andChangeKey: (struct Binary_r *) targetChangeKey wantCopy: (uint8_t) wantCopy inMemCtx: (TALLOC_CTX *) memCtx { int rc; MAPIStoreMessage *sourceMsg, *destMsg; //TALLOC_CTX *memCtx; struct SRow aRow; struct SPropValue property; uint8_t deleteFlags; [self logWithFormat: @"-moveCopyMessageWithMID: 0x%.16llx .. withMID: 0x%.16llx .. wantCopy: %d", srcMid, targetMid, wantCopy]; //memCtx = talloc_zero (NULL, TALLOC_CTX); rc = [sourceFolder openMessage: &sourceMsg withMID: srcMid forWriting: NO inMemCtx: memCtx]; if (rc != MAPISTORE_SUCCESS) goto end; rc = [self createMessage: &destMsg withMID: targetMid isAssociated: [sourceMsg isKindOfClass: MAPIStoreFAIMessageK]]; if (rc != MAPISTORE_SUCCESS) goto end; [sourceMsg copyToMessage: destMsg inMemCtx: memCtx]; if (targetChangeKey) { property.ulPropTag = PidTagChangeKey; property.value.bin = *targetChangeKey; aRow.cValues = 1; aRow.lpProps = &property; rc = [destMsg addPropertiesFromRow: &aRow]; if (rc != MAPISTORE_SUCCESS) goto end; } [destMsg save: memCtx]; if (!wantCopy) /* We want to keep mid for restoring/shared data to work if mids are different. */ deleteFlags = (srcMid == targetMid) ? MAPISTORE_PERMANENT_DELETE : MAPISTORE_SOFT_DELETE; rc = [sourceFolder deleteMessageWithMID: srcMid andFlags: deleteFlags]; end: //talloc_free (memCtx); return rc; } - (int) moveCopyMessagesWithMIDs: (uint64_t *) srcMids andCount: (uint32_t) midCount fromFolder: (MAPIStoreFolder *) sourceFolder withMIDs: (uint64_t *) targetMids andChangeKeys: (struct Binary_r **) targetChangeKeys wantCopy: (uint8_t) wantCopy inMemCtx: (TALLOC_CTX *) memCtx { int rc = MAPISTORE_SUCCESS; NSUInteger count; NSMutableArray *oldMessageURLs; NSString *oldMessageURL; MAPIStoreMapping *mapping; SOGoUser *ownerUser; struct Binary_r *targetChangeKey; //TALLOC_CTX *memCtx; //memCtx = talloc_zero (NULL, TALLOC_CTX); ownerUser = [[self userContext] sogoUser]; if (wantCopy || [[context activeUser] isEqual: ownerUser]) { if ([sourceFolder isKindOfClass: isa] || [self isKindOfClass: [sourceFolder class]]) [self logWithFormat: @"%s: this class could probably implement" @" a specialized/optimized version", __FUNCTION__]; oldMessageURLs = [NSMutableArray arrayWithCapacity: midCount]; mapping = [self mapping]; for (count = 0; rc == MAPISTORE_SUCCESS && count < midCount; count++) { oldMessageURL = [mapping urlFromID: srcMids[count]]; if (oldMessageURL) { [oldMessageURLs addObject: oldMessageURL]; if (targetChangeKeys) targetChangeKey = targetChangeKeys[count]; else targetChangeKey = NULL; rc = [self _moveCopyMessageWithMID: srcMids[count] fromFolder: sourceFolder withMID: targetMids[count] andChangeKey: targetChangeKey wantCopy: wantCopy inMemCtx: memCtx]; } else rc = MAPISTORE_ERR_NOT_FOUND; } /* Notifications */ if (rc == MAPISTORE_SUCCESS) { // We cleanup cache of our source and destination folders [self cleanupCaches]; [sourceFolder cleanupCaches]; } } else rc = MAPISTORE_ERR_DENIED; //talloc_free (memCtx); return rc; } - (enum mapistore_error) moveCopyToFolder: (MAPIStoreFolder *) targetFolder withNewName: (NSString *) newFolderName isMove: (BOOL) isMove isRecursive: (BOOL) isRecursive inMemCtx: (TALLOC_CTX *) memCtx { enum mapistore_error rc; NSAutoreleasePool *pool; struct SRow folderRow; struct SPropValue nameProperty; MAPIStoreFolder *subFolder, *newFolder; NSArray *children; MAPIStoreMapping *mapping; MAPIStoreMessage *message, *targetMessage; NSUInteger count, max; NSString *childKey; uint64_t fmid; //TALLOC_CTX *memCtx; //memCtx = talloc_zero (NULL, TALLOC_CTX); /* TODO: one possible issue with this algorithm is that moved messages will lack a version number and will all be assigned a new one, even though they have not changed. This also means that they will be transferred again to the client during a sync operation. */ if ([targetFolder supportsSubFolders]) { mapping = [self mapping]; if (!newFolderName) newFolderName = [sogoObject displayName]; nameProperty.ulPropTag = PidTagDisplayName; nameProperty.value.lpszW = [newFolderName UTF8String]; folderRow.lpProps = &nameProperty; folderRow.cValues = 1; rc = [targetFolder createFolder: &folderRow withFID: [self objectId] andKey: &childKey]; if (rc == MAPISTORE_SUCCESS) { newFolder = [targetFolder lookupFolder: childKey]; [self copyPropertiesToObject: newFolder inMemCtx: memCtx]; pool = [NSAutoreleasePool new]; children = [self messageKeys]; max = [children count]; for (count = 0; count < max; count++) { childKey = [children objectAtIndex: count]; message = [self lookupMessage: childKey]; targetMessage = [newFolder createMessage: NO]; [targetMessage setIsNew: YES]; [message copyToMessage: targetMessage inMemCtx: memCtx]; if (isMove) { fmid = [mapping idFromURL: [message url]]; [self deleteMessageWithMID: fmid andFlags: MAPISTORE_PERMANENT_DELETE]; [mapping registerURL: [targetMessage url] withID: fmid]; } [targetMessage save: memCtx]; } [pool release]; pool = [NSAutoreleasePool new]; children = [self faiMessageKeys]; max = [children count]; for (count = 0; count < max; count++) { childKey = [children objectAtIndex: count]; message = [self lookupFAIMessage: childKey]; targetMessage = [newFolder createMessage: YES]; [targetMessage setIsNew: YES]; [message copyToMessage: targetMessage inMemCtx: memCtx]; if (isMove) { fmid = [mapping idFromURL: [message url]]; [self deleteMessageWithMID: fmid andFlags: MAPISTORE_PERMANENT_DELETE]; [mapping registerURL: [targetMessage url] withID: fmid]; } [targetMessage save: memCtx]; } [pool release]; if (isRecursive) { pool = [NSAutoreleasePool new]; children = [self folderKeys]; max = [children count]; for (count = 0; count < max; count++) { childKey = [children objectAtIndex: count]; subFolder = [self lookupFolder: childKey]; [subFolder moveCopyToFolder: newFolder withNewName: nil isMove: isMove isRecursive: isRecursive inMemCtx: memCtx]; } [pool release]; } if (isMove) [self deleteFolder]; [targetFolder cleanupCaches]; } [self cleanupCaches]; /* We perform the mapping operations at the end as objectId is required to be available until the caches are cleaned up */ if (isMove && rc == MAPISTORE_SUCCESS) { fmid = [mapping idFromURL: [self url]]; [mapping unregisterURLWithID: fmid]; [mapping registerURL: [newFolder url] withID: fmid]; } } else rc = MAPISTORE_ERR_DENIED; //talloc_free (memCtx); return rc; } - (SOGoFolder *) aclFolder { [self subclassResponsibility: _cmd]; return nil; } - (void) _modifyPermissionEntryForUser: (NSString *) user withRoles: (NSArray *) roles isAddition: (BOOL) isAddition withACLFolder: (SOGoFolder *) aclFolder { if (user) { if (isAddition) [aclFolder addUserInAcls: user]; [aclFolder setRoles: roles forUser: user]; } else [self logWithFormat: @"user is nil, keeping intended entry intact"]; } - (void) setupVersionsMessage { } - (void) ensureIDsForChildKeys: (NSArray *) keys { NSMutableArray *missingURLs; MAPIStoreMapping *mapping; NSUInteger count, max; NSString *baseURL, *URL, *key; NSArray *newIDs; uint64_t idNbr; bool softDeleted; baseURL = [self url]; mapping = [self mapping]; max = [keys count]; missingURLs = [NSMutableArray arrayWithCapacity: max]; for (count = 0; count < max; count++) { key = [keys objectAtIndex: count]; URL = [NSString stringWithFormat: @"%@%@", baseURL, key]; idNbr = [mapping idFromURL: URL isSoftDeleted: &softDeleted]; if (idNbr == NSNotFound && !softDeleted) [missingURLs addObject: URL]; } max = [missingURLs count]; newIDs = [[self context] getNewFMIDs: max]; [mapping registerURLs: missingURLs withIDs: newIDs]; } - (int) getDeletedFMIDs: (struct UI8Array_r **) fmidsPtr andCN: (uint64_t *) cnPtr fromChangeNumber: (uint64_t) changeNum inTableType: (enum mapistore_table_type) tableType inMemCtx: (TALLOC_CTX *) memCtx { int rc; NSString *baseURL, *format, *url; NSArray *keys; NSNumber *cnNbr; NSUInteger count, max; MAPIStoreMapping *mapping; struct UI8Array_r *fmids; uint64_t fmid; bool softDeleted; keys = [self getDeletedKeysFromChangeNumber: changeNum andCN: &cnNbr inTableType: tableType]; if (keys) { mapping = [self mapping]; max = [keys count]; fmids = talloc_zero (memCtx, struct UI8Array_r); fmids->cValues = 0; fmids->lpui8 = talloc_array (fmids, uint64_t, max); *fmidsPtr = fmids; if (max > 0) *cnPtr = [cnNbr unsignedLongLongValue]; baseURL = [self url]; if ([baseURL hasSuffix: @"/"]) format = @"%@%@"; else format = @"%@/%@"; for (count = 0; count < max; count++) { url = [NSString stringWithFormat: format, baseURL, [keys objectAtIndex: count]]; fmid = [mapping idFromURL: url isSoftDeleted: &softDeleted]; if (fmid != NSNotFound) /* if no fmid is returned, then the object "never existed" in the OpenChange databases. Soft-deleted messages are returned back */ { fmids->lpui8[fmids->cValues] = fmid; fmids->cValues++; } } rc = MAPISTORE_SUCCESS; } else rc = MAPISTORE_ERR_NOT_FOUND; return rc; } - (int) getTable: (MAPIStoreTable **) tablePtr andRowCount: (uint32_t *) countPtr tableType: (enum mapistore_table_type) tableType andHandleId: (uint32_t) handleId { int rc = MAPISTORE_SUCCESS; MAPIStoreTable *table; SOGoUser *ownerUser; if (tableType == MAPISTORE_MESSAGE_TABLE) table = [self messageTable]; else if (tableType == MAPISTORE_FAI_TABLE) table = [self faiMessageTable]; else if (tableType == MAPISTORE_FOLDER_TABLE) table = [self folderTable]; else if (tableType == MAPISTORE_PERMISSIONS_TABLE) { ownerUser = [[self userContext] sogoUser]; if ([[context activeUser] isEqual: ownerUser]) table = [self permissionsTable]; else rc = MAPISTORE_ERR_DENIED; } else { table = nil; [NSException raise: @"MAPIStoreIOException" format: @"unsupported table type: %d", tableType]; } if (rc == MAPISTORE_SUCCESS) { if (table) { [table setHandleId: handleId]; *tablePtr = table; *countPtr = [[table childKeys] count]; } else rc = MAPISTORE_ERR_NOT_FOUND; } return rc; } - (void) addProperties: (NSDictionary *) newProperties { static enum MAPITAGS bannedProps[] = { PR_MID, PR_FID, PR_PARENT_FID, PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_CHANGE_KEY, 0x00000000 }; enum MAPITAGS *currentProp; NSMutableDictionary *propsCopy; /* TODO: this should no longer be required once mapistore v2 API is in place, when we can then do this from -dealloc below */ [dbFolder reloadIfNeeded]; propsCopy = [newProperties mutableCopy]; [propsCopy autorelease]; currentProp = bannedProps; while (*currentProp) { [propsCopy removeObjectForKey: MAPIPropertyKey (*currentProp)]; currentProp++; } [properties addEntriesFromDictionary: propsCopy]; [dbFolder save]; } - (NSArray *) messageKeys { return [self messageKeysMatchingQualifier: nil andSortOrderings: nil]; // if (!messageKeys) // { // messageKeys = [self messageKeysMatchingQualifier: nil // andSortOrderings: nil]; // [messageKeys retain]; // } // return messageKeys; } - (MAPIStoreFAIMessageTable *) faiMessageTable { return [MAPIStoreFAIMessageTable tableForContainer: self]; } - (NSArray *) faiMessageKeysMatchingQualifier: (EOQualifier *) qualifier andSortOrderings: (NSArray *) sortOrderings { return [dbFolder childKeysOfType: MAPIFAICacheObject includeDeleted: NO matchingQualifier: qualifier andSortOrderings: sortOrderings]; } - (NSArray *) faiMessageKeys { return [self faiMessageKeysMatchingQualifier: nil andSortOrderings: nil]; // if (!faiMessageKeys) // { // faiMessageKeys = [self faiMessageKeysMatchingQualifier: nil // andSortOrderings: nil]; // [faiMessageKeys retain]; // } // return faiMessageKeys; } - (MAPIStoreFolderTable *) folderTable { return [MAPIStoreFolderTable tableForContainer: self]; } - (NSArray *) folderKeys { return [self folderKeysMatchingQualifier: nil andSortOrderings: nil]; // if (!folderKeys) // { // folderKeys = [self folderKeysMatchingQualifier: nil // andSortOrderings: nil]; // [folderKeys retain]; // } // return folderKeys; } - (NSArray *) folderKeysMatchingQualifier: (EOQualifier *) qualifier andSortOrderings: (NSArray *) sortOrderings { if (qualifier) [self errorWithFormat: @"qualifier is not used for folders"]; if (sortOrderings) [self errorWithFormat: @"sort orderings are not used for folders"]; return [sogoObject toManyRelationshipKeys]; } - (NSArray *) activeMessageTables { return [[MAPIStoreActiveTables activeTables] activeTablesForFMID: [self objectId] andType: MAPISTORE_MESSAGE_TABLE]; } - (NSArray *) activeFAIMessageTables { return [[MAPIStoreActiveTables activeTables] activeTablesForFMID: [self objectId] andType: MAPISTORE_FAI_TABLE]; } - (void) _cleanupTableCaches: (enum mapistore_table_type) tableType { NSArray *tables; NSUInteger count, max; tables = [[MAPIStoreActiveTables activeTables] activeTablesForFMID: [self objectId] andType: tableType]; max = [tables count]; for (count = 0; count < max; count++) [[tables objectAtIndex: count] cleanupCaches]; } - (void) cleanupCaches { [self _cleanupTableCaches: MAPISTORE_MESSAGE_TABLE]; [self _cleanupTableCaches: MAPISTORE_FAI_TABLE]; [self _cleanupTableCaches: MAPISTORE_FOLDER_TABLE]; // [faiMessageKeys release]; // faiMessageKeys = nil; // [messageKeys release]; // messageKeys = nil; // [folderKeys release]; // folderKeys = nil; } - (int) getPidTagParentFolderId: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { *data = MAPILongLongValue (memCtx, [container objectId]); return MAPISTORE_SUCCESS; } - (int) getPidTagFolderId: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { *data = MAPILongLongValue (memCtx, [self objectId]); return MAPISTORE_SUCCESS; } /* Possible values are: 0x00000001 Modify 0x00000002 Read 0x00000004 Delete 0x00000008 Create Hierarchy Table 0x00000010 Create Contents Table 0x00000020 Create Associated Contents Table */ - (int) getPidTagAccess: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { uint32_t access = 0; SOGoUser *ownerUser; BOOL userIsOwner; ownerUser = [[self userContext] sogoUser]; userIsOwner = [[context activeUser] isEqual: ownerUser]; if (userIsOwner || [self subscriberCanModifyMessages]) access |= 0x01; if (userIsOwner || [self subscriberCanReadMessages]) access |= 0x02; if (userIsOwner || [self subscriberCanDeleteMessages]) access |= 0x04; if ((userIsOwner || [self subscriberCanCreateSubFolders]) && [self supportsSubFolders]) access |= 0x08; if (userIsOwner || [self subscriberCanCreateMessages]) access |= 0x10; if (userIsOwner) access |= 0x20; *data = MAPILongValue (memCtx, access); return MAPISTORE_SUCCESS; } - (int) getPidTagRights: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { uint32_t rights = 0; SOGoUser *ownerUser; BOOL userIsOwner; ownerUser = [[self userContext] sogoUser]; userIsOwner = [[context activeUser] isEqual: ownerUser]; if (userIsOwner || [self subscriberCanReadMessages]) rights |= RightsReadItems; if (userIsOwner || [self subscriberCanCreateMessages]) rights |= RightsCreateItems; if (userIsOwner || [self subscriberCanModifyMessages]) rights |= RightsEditOwn | RightsEditAll; if (userIsOwner || [self subscriberCanDeleteMessages]) rights |= RightsDeleteOwn | RightsDeleteAll; if ((userIsOwner || [self subscriberCanCreateSubFolders]) && [self supportsSubFolders]) rights |= RightsCreateSubfolders; if (userIsOwner) rights |= RightsFolderOwner | RightsFolderContact; *data = MAPILongValue (memCtx, rights); return MAPISTORE_SUCCESS; } - (int) getPidTagAccessControlListData: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { *data = [[NSData data] asBinaryInMemCtx: memCtx]; return MAPISTORE_SUCCESS; } - (int) getPidTagAttributeHidden: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { return [self getNo: data inMemCtx: memCtx]; } - (int) getPidTagAttributeSystem: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { return [self getNo: data inMemCtx: memCtx]; } - (int) getPidTagAttributeReadOnly: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { return [self getNo: data inMemCtx: memCtx]; } - (int) getPidTagSubfolders: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { *data = MAPIBoolValue (memCtx, [self supportsSubFolders] && [[self folderKeys] count] > 0); return MAPISTORE_SUCCESS; } - (int) getPidTagFolderChildCount: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { *data = MAPILongValue (memCtx, [[self folderKeys] count]); return MAPISTORE_SUCCESS; } - (int) getPidTagContentCount: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { *data = MAPILongValue (memCtx, [[self messageKeys] count]); return MAPISTORE_SUCCESS; } - (int) getPidTagContentUnreadCount: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { *data = MAPILongValue (memCtx, 0); return MAPISTORE_SUCCESS; } - (int) getPidTagAssociatedContentCount: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { *data = MAPILongValue (memCtx, [[self faiMessageKeys] count]); return MAPISTORE_SUCCESS; } - (int) getPidTagDeletedCountTotal: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { /* TODO */ *data = MAPILongValue (memCtx, 0); return MAPISTORE_SUCCESS; } - (int) getPidTagLocalCommitTimeMax: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { int rc = MAPISTORE_SUCCESS; NSDate *date; date = [self lastMessageModificationTime]; if (date) *data = [date asFileTimeInMemCtx: memCtx]; else rc = MAPISTORE_ERR_NOT_FOUND; return rc; } - (int) getPidTagDefaultPostMessageClass: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { *data = [@"IPM.Note" asUnicodeInMemCtx: memCtx]; return MAPISTORE_SUCCESS; } - (int) getProperties: (struct mapistore_property_data *) data withTags: (enum MAPITAGS *) tags andCount: (uint16_t) columnCount inMemCtx: (TALLOC_CTX *) memCtx { [dbFolder reloadIfNeeded]; return [super getProperties: data withTags: tags andCount: columnCount inMemCtx: memCtx]; } - (int) getProperty: (void **) data withTag: (enum MAPITAGS) propTag inMemCtx: (TALLOC_CTX *) memCtx { int rc; id value; value = [properties objectForKey: MAPIPropertyKey (propTag)]; if (value) rc = [value getValue: data forTag: propTag inMemCtx: memCtx]; else rc = [super getProperty: data withTag: propTag inMemCtx: memCtx]; return rc; } - (MAPIStoreMessage *) _createAssociatedMessage { MAPIStoreMessage *newMessage; SOGoMAPIDBMessage *dbObject; NSString *newKey; newKey = [NSString stringWithFormat: @"%@.plist", [SOGoObject globallyUniqueObjectId]]; dbObject = [SOGoMAPIDBMessage objectWithName: newKey inContainer: dbFolder]; [dbObject setObjectType: MAPIFAICacheObject]; [dbObject setIsNew: YES]; newMessage = [MAPIStoreFAIMessageK mapiStoreObjectWithSOGoObject: dbObject inContainer: self]; return newMessage; } - (MAPIStoreMessage *) createMessage: (BOOL) isAssociated { MAPIStoreMessage *newMessage; WOContext *woContext; if (isAssociated) newMessage = [self _createAssociatedMessage]; else newMessage = [self createMessage]; /* FIXME: this is ugly as the specifics of message creation should all be delegated to subclasses */ if ([newMessage respondsToSelector: @selector (setIsNew:)]) [newMessage setIsNew: YES]; woContext = [[self userContext] woContext]; /* FIXME: this is ugly too as the specifics of message creation should all be delegated to subclasses */ if ([newMessage respondsToSelector: @selector (sogoObject:)]) [[newMessage sogoObject] setContext: woContext]; return newMessage; } - (enum mapistore_error) createFolder: (struct SRow *) aRow withFID: (uint64_t) newFID andKey: (NSString **) newKeyP { [self errorWithFormat: @"new folders cannot be created in this context"]; return MAPISTORE_ERR_DENIED; } /* helpers */ - (NSString *) url { NSString *url; if (container) url = [NSString stringWithFormat: @"%@/", [super url]]; else { url = [[context url] absoluteString]; if (![url hasSuffix: @"/"]) url = [NSString stringWithFormat: @"%@/", url]; } return url; } - (MAPIStorePermissionsTable *) permissionsTable { return [MAPIStorePermissionsTable tableForContainer: self]; } - (NSArray *) permissionEntries { NSMutableArray *permissionEntries; MAPIStorePermissionEntry *entry; NSArray *aclUsers; uint64_t memberId, regularMemberId = 1; NSUInteger count, max; NSString *username, *defaultUserId; SOGoFolder *aclFolder; aclFolder = [self aclFolder]; defaultUserId = [aclFolder defaultUserID]; aclUsers = [aclFolder aclUsers]; max = [aclUsers count]; permissionEntries = [NSMutableArray arrayWithCapacity: max]; for (count = 0; count < max; count++) { username = [aclUsers objectAtIndex: count]; if (![username hasPrefix: @"@"]) { if ([username isEqualToString: defaultUserId]) memberId = 0; else if ([username isEqualToString: @"anonymous"]) memberId = ULLONG_MAX; else { memberId = regularMemberId; regularMemberId++; } entry = [MAPIStorePermissionEntry entryWithUserId: username andMemberId: memberId forFolder: self]; [permissionEntries addObject: entry]; } } return permissionEntries; } - (NSArray *) rolesForExchangeRights: (uint32_t) rights { [self subclassResponsibility: _cmd]; return nil; } - (uint32_t) exchangeRightsForRoles: (NSArray *) roles { [self subclassResponsibility: _cmd]; return 0; } - (NSString *) _usernameFromEntryId: (struct SBinary_short *) bin { struct Binary_r bin32; struct AddressBookEntryId *entryId; NSString *username; struct ldb_context *samCtx; if (bin && bin->cb) { bin32.cb = bin->cb; bin32.lpb = bin->lpb; entryId = get_AddressBookEntryId (NULL, &bin32); if (entryId) { samCtx = [[self context] connectionInfo]->sam_ctx; username = MAPIStoreSamDBUserAttribute (samCtx, @"legacyExchangeDN", [NSString stringWithUTF8String: entryId->X500DN], @"sAMAccountName"); } else username = nil; talloc_free (entryId); } else username = nil; return username; } - (NSString *) _usernameFromMemberId: (uint64_t) memberId inEntries: (NSArray *) entries { NSString *username = nil; NSUInteger count, max; MAPIStorePermissionEntry *entry; if (memberId == 0) username = [[self aclFolder] defaultUserID]; else if (memberId == ULLONG_MAX) username = @"anonymous"; else { max = [entries count]; for (count = 0; !username && count < max; count++) { entry = [entries objectAtIndex: count]; if ([entry memberId] == memberId) username = [entry userId]; } } return username; } - (void) _emptyACL { NSUInteger count, max; NSArray *users; SOGoFolder *aclFolder; aclFolder = [self aclFolder]; users = [[aclFolder aclUsers] copy]; max = [users count]; for (count = 0; count < max; count++) [aclFolder removeUserFromAcls: [users objectAtIndex: count]]; [users release]; } - (int) modifyPermissions: (struct PermissionData *) permissions withCount: (uint16_t) pcount andFlags: (int8_t) flags { NSUInteger count, propCount; struct PermissionData *currentPermission; struct mapi_SPropValue *mapiValue; NSString *permissionUser; NSArray *entries; NSArray *permissionRoles; BOOL reset, isAdd = NO, isDelete = NO, isModify = NO; SOGoFolder *aclFolder; aclFolder = [self aclFolder]; reset = ((flags & ModifyPerms_ReplaceRows) != 0); if (reset) [self _emptyACL]; entries = [self permissionEntries]; for (count = 0; count < pcount; count++) { currentPermission = permissions + count; permissionUser = nil; permissionRoles = nil; if (currentPermission->PermissionDataFlags == ROW_ADD) isAdd = YES; else if (currentPermission->PermissionDataFlags == ROW_MODIFY) isModify = YES; else isDelete = YES; for (propCount = 0; propCount < currentPermission->lpProps.cValues; propCount++) { mapiValue = currentPermission->lpProps.lpProps + propCount; switch (mapiValue->ulPropTag) { case PR_ENTRYID: if (isAdd) permissionUser = [self _usernameFromEntryId: &mapiValue->value.bin]; break; case PR_MEMBER_ID: if (isModify || isDelete) permissionUser = [self _usernameFromMemberId: mapiValue->value.d inEntries: entries]; break; case PR_MEMBER_RIGHTS: if (isAdd || isModify) permissionRoles = [self rolesForExchangeRights: mapiValue->value.l]; break; default: if (mapiValue->ulPropTag != PR_MEMBER_NAME) [self warnWithFormat: @"unhandled permission property: %.8x", mapiValue->ulPropTag]; } } if (reset) { if (isAdd) [self _modifyPermissionEntryForUser: permissionUser withRoles: permissionRoles isAddition: YES withACLFolder: aclFolder]; } else { if (isAdd || currentPermission->PermissionDataFlags == ROW_MODIFY) [self _modifyPermissionEntryForUser: permissionUser withRoles: permissionRoles isAddition: isAdd withACLFolder: aclFolder]; else if (currentPermission->PermissionDataFlags == ROW_REMOVE) [aclFolder removeUserFromAcls: permissionUser]; else [self errorWithFormat: @"unhandled permission action flag: %d", currentPermission->PermissionDataFlags]; } } return MAPISTORE_SUCCESS; } - (enum mapistore_error) preloadMessageBodiesWithMIDs: (const struct UI8Array_r *) mids ofTableType: (enum mapistore_table_type) tableType { uint32_t count; NSMutableArray *messageKeys; MAPIStoreMapping *mapping; NSString *messageURL, *messageKey; messageKeys = [NSMutableArray arrayWithCapacity: mids->cValues]; mapping = [self mapping]; for (count = 0; count < mids->cValues; count++) { messageURL = [mapping urlFromID: mids->lpui8[count]]; if (messageURL) { messageKey = [self childKeyFromURL: messageURL]; if (messageKey) [messageKeys addObject: messageKey]; } } return [self preloadMessageBodiesWithKeys: messageKeys ofTableType: tableType]; } - (enum mapistore_error) preloadMessageBodiesWithKeys: (NSArray *) keys ofTableType: (enum mapistore_table_type) tableType { return MAPISTORE_SUCCESS; } - (uint64_t) objectId { uint64_t objectId; NSString *folderKey; if (container) { folderKey = [NSString stringWithFormat: @"%@/", [sogoObject nameInContainer]]; objectId = [container idForObjectWithKey: folderKey]; } else objectId = [self idForObjectWithKey: nil]; return objectId; } - (uint64_t) idForObjectWithKey: (NSString *) childKey { return [[self context] idForObjectWithKey: childKey inFolderURL: [self url]]; } - (MAPIStoreFolder *) rootContainer { /* Return the oldest ancestor, which does not have container. If there is not container, it returns itself. */ if (container) return [container rootContainer]; else return self; } - (NSDate *) creationTime { return [dbFolder creationDate]; } - (NSDate *) lastModificationTime { return [dbFolder lastModified]; } /* subclasses */ - (MAPIStoreMessageTable *) messageTable { return nil; } - (NSArray *) messageKeysMatchingQualifier: (EOQualifier *) qualifier andSortOrderings: (NSArray *) sortOrderings { [self subclassResponsibility: _cmd]; return nil; } - (NSArray *) getDeletedKeysFromChangeNumber: (uint64_t) changeNum andCN: (NSNumber **) cnNbrs inTableType: (enum mapistore_table_type) tableType { return nil; } - (MAPIStoreMessage *) createMessage { [self logWithFormat: @"ignored method: %s", __PRETTY_FUNCTION__]; return nil; } - (NSCalendarDate *) lastMessageModificationTime { [self subclassResponsibility: _cmd]; return nil; } - (BOOL) subscriberCanCreateMessages { return NO; } - (BOOL) subscriberCanModifyMessages { return NO; } - (BOOL) subscriberCanReadMessages { return NO; } - (BOOL) subscriberCanDeleteMessages { return NO; } - (BOOL) subscriberCanCreateSubFolders { return NO; } - (BOOL) supportsSubFolders { return NO; } @end