/* MAPIStoreGCSFolder.m - this file is part of SOGo * * Copyright (C) 2011-2012 Inverse inc * * Author: Wolfgang Sourdeau * * 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. */ #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "MAPIStoreGCSBaseContext.h" #import "MAPIStoreTypes.h" #import "MAPIStoreUserContext.h" #import "NSData+MAPIStore.h" #import "NSDate+MAPIStore.h" #import "NSString+MAPIStore.h" #import "SOGoMAPIDBMessage.h" #import "MAPIStoreGCSFolder.h" #undef DEBUG #include #include static Class NSNumberK; @implementation MAPIStoreGCSFolder + (void) initialize { NSNumberK = [NSNumber class]; } - (id) initWithSOGoObject: (id) newSOGoObject inContainer: (MAPIStoreObject *) newContainer { if ((self = [super initWithSOGoObject: newSOGoObject inContainer: newContainer])) { activeUserRoles = nil; } return self; } - (void) setupVersionsMessage { ASSIGN (versionsMessage, [SOGoMAPIDBMessage objectWithName: @"versions.plist" inContainer: dbFolder]); [versionsMessage setObjectType: MAPIInternalCacheObject]; [versionsMessage reloadIfNeeded]; } - (void) dealloc { [versionsMessage release]; [activeUserRoles release]; [componentQualifier release]; [super dealloc]; } - (int) deleteFolder { int rc; NSException *error; NSString *name; name = [self nameInContainer]; if ([name isEqualToString: @"personal"]) rc = MAPISTORE_ERR_DENIED; else { [[sogoObject container] removeSubFolder: name]; error = [(SOGoGCSFolder *) sogoObject delete]; if (error) rc = MAPISTORE_ERROR; else { if (![versionsMessage delete]) rc = MAPISTORE_SUCCESS; else rc = MAPISTORE_ERROR; } } return (rc == MAPISTORE_SUCCESS) ? [super deleteFolder] : rc; } - (void) setDisplayName: (NSString *) newDisplayName { NSString *suffix, *fullSuffix; Class cClass; cClass = [(MAPIStoreGCSBaseContext *) [self context] class]; /* if a suffix exists, we strip it from the final name */ suffix = [cClass folderNameSuffix]; if ([suffix length] > 0) { fullSuffix = [NSString stringWithFormat: @"(%@)", suffix]; if ([newDisplayName hasSuffix: fullSuffix]) { newDisplayName = [newDisplayName substringToIndex: [newDisplayName length] - [fullSuffix length]]; newDisplayName = [newDisplayName stringByTrimmingSpaces]; } } if (![[sogoObject displayName] isEqualToString: newDisplayName]) [sogoObject renameTo: newDisplayName]; } - (int) getPidTagDisplayName: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { NSString *displayName; Class cClass; cClass = [(MAPIStoreGCSBaseContext *) [self context] class]; displayName = [cClass getFolderDisplayName: [sogoObject displayName]]; *data = [displayName asUnicodeInMemCtx: memCtx]; return MAPISTORE_SUCCESS; } - (void) addProperties: (NSDictionary *) newProperties { NSString *newDisplayName; NSMutableDictionary *propsCopy; NSNumber *key; key = MAPIPropertyKey (PR_DISPLAY_NAME_UNICODE); newDisplayName = [newProperties objectForKey: key]; if (newDisplayName) { [self setDisplayName: newDisplayName]; propsCopy = [newProperties mutableCopy]; [propsCopy removeObjectForKey: key]; [propsCopy autorelease]; newProperties = propsCopy; } [super addProperties: newProperties]; } - (NSArray *) messageKeysMatchingQualifier: (EOQualifier *) qualifier andSortOrderings: (NSArray *) sortOrderings { static NSArray *fields = nil; SOGoUser *ownerUser; NSArray *records; NSMutableArray *qualifierArray; EOQualifier *fetchQualifier, *aclQualifier; GCSFolder *ocsFolder; EOFetchSpecification *fs; NSArray *keys; if (!fields) fields = [[NSArray alloc] initWithObjects: @"c_name", @"c_version", nil]; qualifierArray = [NSMutableArray new]; ownerUser = [[self userContext] sogoUser]; if (![[context activeUser] isEqual: ownerUser]) { aclQualifier = [self aclQualifier]; if (aclQualifier) [qualifierArray addObject: aclQualifier]; } [qualifierArray addObject: [self componentQualifier]]; if (qualifier) [qualifierArray addObject: qualifier]; fetchQualifier = [[EOAndQualifier alloc] initWithQualifierArray: qualifierArray]; ocsFolder = [sogoObject ocsFolder]; fs = [EOFetchSpecification fetchSpecificationWithEntityName: [ocsFolder folderName] qualifier: fetchQualifier sortOrderings: sortOrderings]; [fetchQualifier release]; [qualifierArray release]; records = [ocsFolder fetchFields: fields fetchSpecification: fs]; keys = [records objectsForKey: @"c_name" notFoundMarker: nil]; return keys; } - (NSDate *) lastMessageModificationTime { NSDate *value; NSNumber *ti; [self synchroniseCache]; ti = [[versionsMessage properties] objectForKey: @"SyncLastModificationDate"]; if (ti) value = [NSDate dateWithTimeIntervalSince1970: [ti doubleValue]]; else value = nil; return value; } - (SOGoFolder *) aclFolder { return (SOGoFolder *) sogoObject; } /* synchronisation */ /* Tree { SyncLastModseq = x; SyncLastSynchronisationDate = x; ** not updated until something changed Messages = { MessageKey = { Version = x; Modseq = x; Deleted = b; ChangeKey = d; PredecessorChangeList = { guid1 = globcnt1, guid2 ... }; }; ... }; VersionMapping = { Version = last-modified; ... } } */ - (void) _setChangeKey: (NSData *) changeKey forMessageEntry: (NSMutableDictionary *) messageEntry { struct XID *xid; NSString *guid; NSData *globCnt; NSDictionary *changeKeyDict; NSMutableDictionary *changeList; xid = [changeKey asXIDInMemCtx: NULL]; guid = [NSString stringWithGUID: &xid->NameSpaceGuid]; globCnt = [NSData dataWithBytes: xid->LocalId.data length: xid->LocalId.length]; talloc_free (xid); /* 1. set change key association */ changeKeyDict = [NSDictionary dictionaryWithObjectsAndKeys: guid, @"GUID", globCnt, @"LocalId", nil]; [messageEntry setObject: changeKeyDict forKey: @"ChangeKey"]; /* 2. append/update predecessor change list */ changeList = [messageEntry objectForKey: @"PredecessorChangeList"]; if (!changeList) { changeList = [NSMutableDictionary new]; [messageEntry setObject: changeList forKey: @"PredecessorChangeList"]; [changeList release]; } [changeList setObject: globCnt forKey: guid]; } - (void) _updatePredecessorChangeList: (NSData *) predecessorChangeList forMessageEntry: (NSMutableDictionary *) messageEntry withOldChangeKey: (NSData *) oldChangeKey { NSData *globCnt, *oldGlobCnt; NSDictionary *changeKeyDict; NSString *guid; NSMutableDictionary *changeList; struct SizedXid *sizedXIDList; struct XID xid, *givenChangeKey; TALLOC_CTX *localMemCtx; uint32_t i, length; localMemCtx = talloc_new (NULL); if (!localMemCtx) { [self errorWithFormat: @"No more memory"]; return; } if (predecessorChangeList) { sizedXIDList = [predecessorChangeList asSizedXidArrayInMemCtx: localMemCtx with: &length]; changeList = [messageEntry objectForKey: @"PredecessorChangeList"]; if (!changeList) { changeList = [NSMutableDictionary new]; [messageEntry setObject: changeList forKey: @"PredecessorChangeList"]; [changeList release]; } if (sizedXIDList) { for (i = 0; i < length; i++) { xid = sizedXIDList[i].XID; guid = [NSString stringWithGUID: &xid.NameSpaceGuid]; globCnt = [NSData dataWithBytes: xid.LocalId.data length: xid.LocalId.length]; oldGlobCnt = [changeList objectForKey: guid]; if (!oldGlobCnt || ([globCnt compare: oldGlobCnt] == NSOrderedDescending)) [changeList setObject: globCnt forKey: guid]; } } } if (oldChangeKey) { givenChangeKey = [oldChangeKey asXIDInMemCtx: localMemCtx]; if (givenChangeKey) { guid = [NSString stringWithGUID: &givenChangeKey->NameSpaceGuid]; globCnt = [NSData dataWithBytes: givenChangeKey->LocalId.data length: givenChangeKey->LocalId.length]; changeKeyDict = [messageEntry objectForKey: @"ChangeKey"]; if (!changeKeyDict || ([guid isEqualToString: [changeKeyDict objectForKey: @"GUID"]] && ([globCnt compare: [changeKeyDict objectForKey: @"LocalId"]] == NSOrderedDescending))) { /* The given change key is greater than current one stored in metadata or it does not exist */ [messageEntry setObject: [NSDictionary dictionaryWithObjectsAndKeys: guid, @"GUID", globCnt, @"LocalId", nil] forKey: @"ChangeKey"]; } } } talloc_free (localMemCtx); } - (EOQualifier *) componentQualifier { if (!componentQualifier) componentQualifier = [[EOKeyValueQualifier alloc] initWithKey: @"c_component" operatorSelector: EOQualifierOperatorEqual value: [self component]]; return componentQualifier; } - (EOQualifier *) contentComponentQualifier { EOQualifier *contentComponentQualifier; NSString *likeString; likeString = [NSString stringWithFormat: @"%%BEGIN:%@%%", [[self component] uppercaseString]]; contentComponentQualifier = [[EOKeyValueQualifier alloc] initWithKey: @"c_content" operatorSelector: EOQualifierOperatorLike value: likeString]; [contentComponentQualifier autorelease]; return contentComponentQualifier; } - (BOOL) synchroniseCache { BOOL rc = YES; uint64_t newChangeNum; NSData *changeKey; NSString *cName, *changeNumber; NSNumber *ti, *lastModificationDate, *cVersion, *cLastModified, *cDeleted; EOFetchSpecification *fs; EOQualifier *searchQualifier, *fetchQualifier; NSUInteger count, max; NSArray *fetchResults, *changeNumbers; NSMutableArray *keys, *modifiedEntries; NSDictionary *result; NSMutableDictionary *currentProperties, *messages, *mapping, *messageEntry; NSCalendarDate *now; GCSFolder *ocsFolder; static NSArray *fields = nil; static EOSortOrdering *sortOrdering = nil; /* NOTE: we are using NSString instance for "changeNumber" because NSNumber proved to give very bad performances when used as NSDictionary keys with GNUstep 1.22.1. The bug seems to be solved with 1.24 but many distros still ship an older version. */ if (!fields) fields = [[NSArray alloc] initWithObjects: @"c_name", @"c_version", @"c_lastmodified", @"c_deleted", nil]; if (!sortOrdering) { sortOrdering = [EOSortOrdering sortOrderingWithKey: @"c_lastmodified" selector: EOCompareAscending]; [sortOrdering retain]; } [versionsMessage reloadIfNeeded]; currentProperties = [versionsMessage properties]; lastModificationDate = [currentProperties objectForKey: @"SyncLastModificationDate"]; if (lastModificationDate) { searchQualifier = [[EOKeyValueQualifier alloc] initWithKey: @"c_lastmodified" operatorSelector: EOQualifierOperatorGreaterThanOrEqualTo value: lastModificationDate]; fetchQualifier = [[EOAndQualifier alloc] initWithQualifiers: searchQualifier, [self contentComponentQualifier], nil]; [fetchQualifier autorelease]; [searchQualifier release]; } else fetchQualifier = [self componentQualifier]; ocsFolder = [sogoObject ocsFolder]; fs = [EOFetchSpecification fetchSpecificationWithEntityName: [ocsFolder folderName] qualifier: fetchQualifier sortOrderings: [NSArray arrayWithObject: sortOrdering]]; fetchResults = [ocsFolder fetchFields: fields fetchSpecification: fs ignoreDeleted: NO]; max = [fetchResults count]; if (max > 0) { messages = [currentProperties objectForKey: @"Messages"]; if (!messages) { messages = [NSMutableDictionary new]; [currentProperties setObject: messages forKey: @"Messages"]; [messages release]; } mapping = [currentProperties objectForKey: @"VersionMapping"]; if (!mapping) { mapping = [NSMutableDictionary new]; [currentProperties setObject: mapping forKey: @"VersionMapping"]; [mapping release]; } keys = [NSMutableArray arrayWithCapacity: max]; modifiedEntries = [NSMutableArray arrayWithCapacity: max]; for (count = 0; count < max; count++) { result = [fetchResults objectAtIndex: count]; cName = [result objectForKey: @"c_name"]; [keys addObject: cName]; cDeleted = [result objectForKey: @"c_deleted"]; if ([cDeleted isKindOfClass: NSNumberK] && [cDeleted intValue]) cVersion = [NSNumber numberWithInt: -1]; else cVersion = [result objectForKey: @"c_version"]; cLastModified = [result objectForKey: @"c_lastmodified"]; messageEntry = [messages objectForKey: cName]; if (!messageEntry) { messageEntry = [NSMutableDictionary new]; [messages setObject: messageEntry forKey: cName]; [messageEntry release]; } if (![[messageEntry objectForKey: @"c_version"] isEqual: cVersion]) { [sogoObject removeChildRecordWithName: cName]; [modifiedEntries addObject: messageEntry]; [messageEntry setObject: cLastModified forKey: @"c_lastmodified"]; [messageEntry setObject: cVersion forKey: @"c_version"]; if (!lastModificationDate || ([lastModificationDate compare: cLastModified] == NSOrderedAscending)) lastModificationDate = cLastModified; } } /* make sure all returned objects have a corresponding mid */ [self ensureIDsForChildKeys: keys]; max = [modifiedEntries count]; if (max > 0) { changeNumbers = [[self context] getNewChangeNumbers: max]; for (count = 0; count < max; count++) { messageEntry = [modifiedEntries objectAtIndex: count]; changeNumber = [changeNumbers objectAtIndex: count]; cLastModified = [messageEntry objectForKey: @"c_lastmodified"]; [mapping setObject: cLastModified forKey: changeNumber]; [messageEntry setObject: changeNumber forKey: @"version"]; newChangeNum = [changeNumber unsignedLongLongValue]; // A GLOBCNT structure is a 6-byte global namespace counter, // we strip the first 2 bytes. The first two bytes is the ReplicaId changeKey = [self getReplicaKeyFromGlobCnt: newChangeNum >> 16]; [self _setChangeKey: changeKey forMessageEntry: messageEntry]; } now = [NSCalendarDate date]; ti = [NSNumber numberWithDouble: [now timeIntervalSince1970]]; [currentProperties setObject: ti forKey: @"SyncLastSynchronisationDate"]; [currentProperties setObject: lastModificationDate forKey: @"SyncLastModificationDate"]; [versionsMessage save]; } } return rc; } - (BOOL) synchroniseCacheFor: (NSString *) nameInContainer { /* Try to synchronise old messages in versions.plist cache using an specific c_name. It returns a boolean indicating if the synchronisation was carried out succesfully. It should be used as last resort, keeping synchroniseCache as the main sync entry point. */ uint64_t changeNumber; NSString *changeNumberStr; NSData *changeKey; NSNumber *cLastModified, *cDeleted, *cVersion; EOFetchSpecification *fs; EOQualifier *searchQualifier, *fetchQualifier; NSArray *fetchResults; NSDictionary *result; NSMutableDictionary *currentProperties, *messages, *mapping, *messageEntry; GCSFolder *ocsFolder; static NSArray *fields; [versionsMessage reloadIfNeeded]; currentProperties = [versionsMessage properties]; messages = [currentProperties objectForKey: @"Messages"]; if (!messages) { messages = [NSMutableDictionary new]; [currentProperties setObject: messages forKey: @"Messages"]; [messages release]; } messageEntry = [messages objectForKey: nameInContainer]; if (!messageEntry) { /* Fetch the message by its name */ if (!fields) fields = [[NSArray alloc] initWithObjects: @"c_name", @"c_version", @"c_lastmodified", @"c_deleted", nil]; searchQualifier = [[EOKeyValueQualifier alloc] initWithKey: @"c_name" operatorSelector: EOQualifierOperatorEqual value: nameInContainer]; fetchQualifier = [[EOAndQualifier alloc] initWithQualifiers: searchQualifier, [self contentComponentQualifier], nil]; [fetchQualifier autorelease]; [searchQualifier release]; ocsFolder = [sogoObject ocsFolder]; fs = [EOFetchSpecification fetchSpecificationWithEntityName: [ocsFolder folderName] qualifier: fetchQualifier sortOrderings: nil]; fetchResults = [ocsFolder fetchFields: fields fetchSpecification: fs ignoreDeleted: NO]; if ([fetchResults count] == 1) { result = [fetchResults objectAtIndex: 0]; cLastModified = [result objectForKey: @"c_lastmodified"]; cDeleted = [result objectForKey: @"c_deleted"]; if ([cDeleted isKindOfClass: NSNumberK] && [cDeleted intValue]) cVersion = [NSNumber numberWithInt: -1]; else cVersion = [result objectForKey: @"c_version"]; changeNumber = [[self context] getNewChangeNumber]; changeNumberStr = [NSString stringWithUnsignedLongLong: changeNumber]; /* Create new message entry in Messages dict */ messageEntry = [NSMutableDictionary new]; [messages setObject: messageEntry forKey: nameInContainer]; [messageEntry release]; /* Store cLastModified, cVersion and the change number */ [messageEntry setObject: cLastModified forKey: @"c_lastmodified"]; [messageEntry setObject: cVersion forKey: @"c_version"]; [messageEntry setObject: changeNumberStr forKey: @"version"]; /* Store the change key */ changeKey = [self getReplicaKeyFromGlobCnt: changeNumber >> 16]; [self _setChangeKey: changeKey forMessageEntry: messageEntry]; /* Store the changeNumber -> cLastModified mapping */ mapping = [currentProperties objectForKey: @"VersionMapping"]; if (!mapping) { mapping = [NSMutableDictionary new]; [currentProperties setObject: mapping forKey: @"VersionMapping"]; [mapping release]; } [mapping setObject: cLastModified forKey: changeNumberStr]; /* Save the message */ [versionsMessage save]; return YES; } else return NO; } /* If message entry exists, then synchroniseCache did its job */ return YES; } - (void) updateVersionsForMessageWithKey: (NSString *) messageKey withChangeKey: (NSData *) oldChangeKey andPredecessorChangeList: (NSData *) pcl { NSMutableDictionary *messages, *messageEntry; [self synchroniseCache]; if (oldChangeKey || pcl) { messages = [[versionsMessage properties] objectForKey: @"Messages"]; messageEntry = [messages objectForKey: messageKey]; if (!messageEntry) [NSException raise: @"MAPIStoreIOException" format: @"no version record found for message '%@'", messageKey]; [self _updatePredecessorChangeList: pcl forMessageEntry: messageEntry withOldChangeKey: oldChangeKey]; [versionsMessage save]; } } - (NSNumber *) lastModifiedFromMessageChangeNumber: (NSString *) changeNumber { NSDictionary *mapping; NSNumber *lastModified; mapping = [[versionsMessage properties] objectForKey: @"VersionMapping"]; lastModified = [mapping objectForKey: changeNumber]; return lastModified; } - (NSString *) changeNumberForMessageWithKey: (NSString *) messageKey { NSDictionary *messages; NSString *changeNumber; messages = [[versionsMessage properties] objectForKey: @"Messages"]; changeNumber = [[messages objectForKey: messageKey] objectForKey: @"version"]; return changeNumber; } - (NSData *) changeKeyForMessageWithKey: (NSString *) messageKey { NSDictionary *messages, *changeKeyDict; NSString *guid; NSData *globCnt, *changeKey = nil; messages = [[versionsMessage properties] objectForKey: @"Messages"]; changeKeyDict = [[messages objectForKey: messageKey] objectForKey: @"ChangeKey"]; if (changeKeyDict) { guid = [changeKeyDict objectForKey: @"GUID"]; globCnt = [changeKeyDict objectForKey: @"LocalId"]; changeKey = [NSData dataWithChangeKeyGUID: guid andCnt: globCnt]; } return changeKey; } - (NSData *) predecessorChangeListForMessageWithKey: (NSString *) messageKey { NSMutableData *list = nil; NSDictionary *messages, *changeListDict; NSArray *keys; NSMutableArray *changeKeys; NSUInteger count, max; NSData *changeKey; NSString *guid; NSData *globCnt; messages = [[versionsMessage properties] objectForKey: @"Messages"]; changeListDict = [[messages objectForKey: messageKey] objectForKey: @"PredecessorChangeList"]; if (changeListDict) { keys = [changeListDict allKeys]; max = [keys count]; changeKeys = [NSMutableArray arrayWithCapacity: max]; for (count = 0; count < max; count++) { guid = [keys objectAtIndex: count]; globCnt = [changeListDict objectForKey: guid]; changeKey = [NSData dataWithChangeKeyGUID: guid andCnt: globCnt]; [changeKeys addObject: changeKey]; } [changeKeys sortUsingFunction: MAPIChangeKeyGUIDCompare context: nil]; list = [NSMutableData data]; for (count = 0; count < max; count++) { changeKey = [changeKeys objectAtIndex: count]; [list appendUInt8: [changeKey length]]; [list appendData: changeKey]; } } return list; } - (NSArray *) getDeletedKeysFromChangeNumber: (uint64_t) changeNum andCN: (NSNumber **) cnNbr inTableType: (uint8_t) tableType { NSArray *deletedKeys, *deletedCNames, *records; NSNumber *lastModified; NSString *cName, *changeNumber; NSDictionary *versionProperties, *messageEntry; NSMutableDictionary *messages; uint64_t maxChangeNum = changeNum, currentChangeNum; EOAndQualifier *fetchQualifier; EOKeyValueQualifier *cDeletedQualifier, *cLastModifiedQualifier; EOFetchSpecification *fs; GCSFolder *ocsFolder; NSUInteger count, max; if (tableType == MAPISTORE_MESSAGE_TABLE) { deletedKeys = [NSMutableArray array]; changeNumber = [NSString stringWithUnsignedLongLong: changeNum]; lastModified = [self lastModifiedFromMessageChangeNumber: changeNumber]; if (lastModified) { versionProperties = [versionsMessage properties]; messages = [versionProperties objectForKey: @"Messages"]; ocsFolder = [sogoObject ocsFolder]; cLastModifiedQualifier = [[EOKeyValueQualifier alloc] initWithKey: @"c_lastmodified" operatorSelector: EOQualifierOperatorGreaterThanOrEqualTo value: lastModified]; cDeletedQualifier = [[EOKeyValueQualifier alloc] initWithKey: @"c_deleted" operatorSelector: EOQualifierOperatorEqual value: [NSNumber numberWithInt: 1]]; fetchQualifier = [[EOAndQualifier alloc] initWithQualifiers: cLastModifiedQualifier, cDeletedQualifier, nil]; [fetchQualifier autorelease]; [cLastModifiedQualifier release]; [cDeletedQualifier release]; fs = [EOFetchSpecification fetchSpecificationWithEntityName: [ocsFolder folderName] qualifier: fetchQualifier sortOrderings: nil]; records = [ocsFolder fetchFields: [NSArray arrayWithObject: @"c_name"] fetchSpecification: fs ignoreDeleted: NO]; deletedCNames = [records objectsForKey: @"c_name" notFoundMarker: nil]; max = [deletedCNames count]; for (count = 0; count < max; count++) { cName = [deletedCNames objectAtIndex: count]; [sogoObject removeChildRecordWithName: cName]; messageEntry = [messages objectForKey: cName]; if (messageEntry) { currentChangeNum = [[messageEntry objectForKey: @"version"] unsignedLongLongValue]; if (MAPICNCompare (changeNum, currentChangeNum, NULL) == NSOrderedAscending) { [(NSMutableArray *) deletedKeys addObject: cName]; if (MAPICNCompare (maxChangeNum, currentChangeNum, NULL) == NSOrderedAscending) maxChangeNum = currentChangeNum; } } } if (maxChangeNum != changeNum) *cnNbr = [NSNumber numberWithUnsignedLongLong: maxChangeNum]; } } else deletedKeys = [super getDeletedKeysFromChangeNumber: changeNum andCN: cnNbr inTableType: tableType]; return deletedKeys; } - (NSArray *) activeUserRoles { SOGoUser *activeUser; WOContext *woContext; if (!activeUserRoles) { activeUser = [[self context] activeUser]; woContext = [[self userContext] woContext]; activeUserRoles = [activeUser rolesForObject: sogoObject inContext: woContext]; [activeUserRoles retain]; } return activeUserRoles; } - (BOOL) subscriberCanCreateMessages { return [[self activeUserRoles] containsObject: SOGoRole_ObjectCreator]; } - (BOOL) subscriberCanDeleteMessages { return [[self activeUserRoles] containsObject: SOGoRole_ObjectEraser]; } /* subclasses */ - (EOQualifier *) aclQualifier { return nil; } - (NSString *) component { [self subclassResponsibility: _cmd]; return nil; } @end