/* MAPIStoreMailFolder.m - this file is part of SOGo * * Copyright (C) 2011 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. */ #include #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "MAPIApplication.h" #import "MAPIStoreAppointmentWrapper.h" #import "MAPIStoreContext.h" #import "MAPIStoreFAIMessage.h" #import "MAPIStoreMailMessageTable.h" #import "MAPIStoreMapping.h" #import "MAPIStoreTypes.h" #import "NSData+MAPIStore.h" #import "NSString+MAPIStore.h" #import "SOGoMAPIDBMessage.h" #import "SOGoMAPIDBFolder.h" #import "MAPIStoreMailVolatileMessage.h" #import "MAPIStoreMailFolder.h" static Class SOGoMailFolderK, MAPIStoreMailFolderK, MAPIStoreOutboxFolderK; #undef DEBUG #include #include #include #include @implementation MAPIStoreMailFolder + (void) initialize { SOGoMailFolderK = [SOGoMailFolder class]; MAPIStoreMailFolderK = [MAPIStoreMailFolder class]; MAPIStoreOutboxFolderK = [MAPIStoreOutboxFolder class]; [MAPIStoreAppointmentWrapper class]; } - (id) init { if ((self = [super init])) { versionsMessage = nil; } return self; } - (void) dealloc { [versionsMessage release]; [super dealloc]; } - (void) setupVersionsMessage { ASSIGN (versionsMessage, [SOGoMAPIDBMessage objectWithName: @"versions.plist" inContainer: dbFolder]); [versionsMessage setObjectType: MAPIDBObjectTypeInternal]; } - (BOOL) ensureFolderExists { return [(SOGoMailFolder *) sogoObject exists] || [sogoObject create]; } - (void) addProperties: (NSDictionary *) newProperties { NSString *newDisplayName; NSMutableDictionary *propsCopy; NSNumber *key; uint64_t fid; key = MAPIPropertyKey (PR_DISPLAY_NAME_UNICODE); newDisplayName = [newProperties objectForKey: key]; if (newDisplayName && ![self isKindOfClass: MAPIStoreOutboxFolderK] && ![[(SOGoMailFolder *) sogoObject displayName] isEqualToString: newDisplayName]) { fid = [self objectId]; [(SOGoMailFolder *) sogoObject renameTo: newDisplayName]; [[self mapping] updateID: fid withURL: [self url]]; [self cleanupCaches]; propsCopy = [newProperties mutableCopy]; [propsCopy removeObjectForKey: key]; [propsCopy autorelease]; newProperties = propsCopy; } [super addProperties: newProperties]; } - (MAPIStoreMessageTable *) messageTable { [self synchroniseCache]; return [MAPIStoreMailMessageTable tableForContainer: self]; } - (enum mapistore_error) createFolder: (struct SRow *) aRow withFID: (uint64_t) newFID andKey: (NSString **) newKeyP { enum mapistore_error rc; NSString *folderName, *nameInContainer; SOGoMailFolder *newFolder; int i; nameInContainer = nil; folderName = nil; for (i = 0; !folderName && i < aRow->cValues; i++) { if (aRow->lpProps[i].ulPropTag == PR_DISPLAY_NAME_UNICODE) folderName = [NSString stringWithUTF8String: aRow->lpProps[i].value.lpszW]; else if (aRow->lpProps[i].ulPropTag == PR_DISPLAY_NAME) folderName = [NSString stringWithUTF8String: aRow->lpProps[i].value.lpszA]; } if (folderName) { nameInContainer = [NSString stringWithFormat: @"folder%@", [folderName asCSSIdentifier]]; newFolder = [SOGoMailFolderK objectWithName: nameInContainer inContainer: sogoObject]; if ([newFolder create]) { *newKeyP = nameInContainer; rc = MAPISTORE_SUCCESS; } else if ([newFolder exists]) rc = MAPISTORE_ERR_EXIST; else rc = MAPISTORE_ERR_DENIED; } return rc; } - (int) deleteFolder { int rc; NSException *error; NSString *name; name = [self nameInContainer]; if ([name isEqualToString: @"folderINBOX"]) rc = MAPISTORE_ERR_DENIED; else { error = [(SOGoMailFolder *) 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; } - (int) getPidTagContentUnreadCount: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { EOQualifier *searchQualifier; uint32_t longValue; searchQualifier = [EOQualifier qualifierWithQualifierFormat: @"flags = %@", @"unseen"]; longValue = [[sogoObject fetchUIDsMatchingQualifier: searchQualifier sortOrdering: nil] count]; *data = MAPILongValue (memCtx, longValue); return MAPISTORE_SUCCESS; } - (int) getPidTagContainerClass: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { *data = [@"IPF.Note" asUnicodeInMemCtx: memCtx]; return MAPISTORE_SUCCESS; } - (EOQualifier *) nonDeletedQualifier { static EOQualifier *nonDeletedQualifier = nil; EOQualifier *deletedQualifier; if (!nonDeletedQualifier) { deletedQualifier = [[EOKeyValueQualifier alloc] initWithKey: @"FLAGS" operatorSelector: EOQualifierOperatorContains value: [NSArray arrayWithObject: @"Deleted"]]; nonDeletedQualifier = [[EONotQualifier alloc] initWithQualifier: deletedQualifier]; [deletedQualifier release]; } return nonDeletedQualifier; } - (NSArray *) messageKeysMatchingQualifier: (EOQualifier *) qualifier andSortOrderings: (NSArray *) sortOrderings { NSArray *uidKeys; EOQualifier *fetchQualifier; if ([self ensureFolderExists]) { if (!sortOrderings) sortOrderings = [NSArray arrayWithObject: @"ARRIVAL"]; if (qualifier) { fetchQualifier = [[EOAndQualifier alloc] initWithQualifiers: [self nonDeletedQualifier], qualifier, nil]; [fetchQualifier autorelease]; } else fetchQualifier = [self nonDeletedQualifier]; uidKeys = [[sogoObject fetchUIDsMatchingQualifier: fetchQualifier sortOrdering: sortOrderings] stringsWithFormat: @"%@.eml"]; } else uidKeys = nil; return uidKeys; } - (NSMutableString *) _imapFolderNameRepresentation: (NSString *) subfolderName { NSMutableString *representation; NSString *nameInContainer, *strippedName; nameInContainer = [self nameInContainer]; if (container) representation = [(MAPIStoreMailFolder *) container _imapFolderNameRepresentation: nameInContainer]; else { if (![nameInContainer hasPrefix: @"folder"]) abort (); strippedName = [nameInContainer substringFromIndex: 6]; representation = [NSMutableString stringWithString: strippedName]; } if (![subfolderName hasPrefix: @"folder"]) abort (); strippedName = [subfolderName substringFromIndex: 6]; [representation appendFormat: @"/%@", strippedName]; return representation; } - (void) _cleanupSubfolderKeys: (NSMutableArray *) subfolderKeys { SOGoMailAccount *account; NSString *draftsFolderName, *sentFolderName, *trashFolderName; NSString *subfolderKey, *cmpString; NSUInteger count, max; NSMutableArray *keysToRemove; account = [(SOGoMailFolder *) sogoObject mailAccountFolder]; draftsFolderName = [account draftsFolderNameInContext: nil]; sentFolderName = [account sentFolderNameInContext: nil]; trashFolderName = [account trashFolderNameInContext: nil]; max = [subfolderKeys count]; keysToRemove = [NSMutableArray arrayWithCapacity: max]; for (count = 0; count < max; count++) { subfolderKey = [subfolderKeys objectAtIndex: count]; cmpString = [self _imapFolderNameRepresentation: subfolderKey]; if ([cmpString isEqualToString: draftsFolderName] || [cmpString isEqualToString: sentFolderName] || [cmpString isEqualToString: trashFolderName]) [keysToRemove addObject: subfolderKey]; } [subfolderKeys removeObjectsInArray: keysToRemove]; } - (NSArray *) folderKeysMatchingQualifier: (EOQualifier *) qualifier andSortOrderings: (NSArray *) sortOrderings { NSMutableArray *subfolderKeys; if ([self ensureFolderExists]) { if (qualifier) [self errorWithFormat: @"qualifier is not used for folders"]; if (sortOrderings) [self errorWithFormat: @"sort orderings are not used for folders"]; subfolderKeys = [[sogoObject toManyRelationshipKeys] mutableCopy]; [subfolderKeys autorelease]; [self _cleanupSubfolderKeys: subfolderKeys]; } else subfolderKeys = nil; return subfolderKeys; } - (NSDate *) creationTime { return [NSCalendarDate dateWithTimeIntervalSince1970: 0x4dbb2dbe]; /* oc_version_time */ } - (NSDate *) lastMessageModificationTime { NSNumber *ti; NSDate *value = nil; [self synchroniseCache]; ti = [[versionsMessage properties] objectForKey: @"SyncLastSynchronisationDate"]; if (ti) value = [NSDate dateWithTimeIntervalSince1970: [ti doubleValue]]; else value = [NSDate date]; [self logWithFormat: @"lastMessageModificationTime: %@", value]; return value; } - (SOGoFolder *) aclFolder { return (SOGoFolder *) sogoObject; } - (NSArray *) permissionEntries { NSArray *permissionEntries; if ([self ensureFolderExists]) permissionEntries = [super permissionEntries]; else permissionEntries = nil; return permissionEntries; } - (BOOL) supportsSubFolders { return YES; } /* synchronisation */ /* Tree: { SyncLastModseq = x; SyncLastSynchronisationDate = x; ** not updated until something changed Messages = { MessageKey = { Version = x; Modseq = x; Deleted = b; }; ... }; VersionMapping = { Version = MessageKey; ... } } */ static NSComparisonResult _compareFetchResultsByMODSEQ (id entry1, id entry2, void *data) { static NSNumber *zeroNumber = nil; NSNumber *modseq1, *modseq2; if (!zeroNumber) { zeroNumber = [NSNumber numberWithUnsignedLongLong: 0]; [zeroNumber retain]; } modseq1 = [entry1 objectForKey: @"modseq"]; if (!modseq1) modseq1 = zeroNumber; modseq2 = [entry2 objectForKey: @"modseq"]; if (!modseq2) modseq2 = zeroNumber; return [modseq1 compare: modseq2]; } - (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->GUID]; globCnt = [NSData dataWithBytes: xid->Data length: xid->Size]; 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]; } - (BOOL) synchroniseCache { BOOL rc = YES; uint64_t newChangeNum; NSNumber *ti, *changeNumber, *modseq, *initialLastModseq, *lastModseq, *nextModseq, *uid; uint64_t lastModseqNbr; EOQualifier *kvQualifier, *searchQualifier; NSArray *uids; NSUInteger count, max; NSArray *fetchResults; NSDictionary *result; NSData *changeKey; NSMutableDictionary *currentProperties, *messages, *mapping, *messageEntry; NSCalendarDate *now; BOOL foundChange = NO; now = [NSCalendarDate date]; [now setTimeZone: utcTZ]; [versionsMessage reloadIfNeeded]; currentProperties = [versionsMessage properties]; 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]; } lastModseq = [currentProperties objectForKey: @"SyncLastModseq"]; initialLastModseq = lastModseq; if (lastModseq) { lastModseqNbr = [lastModseq unsignedLongLongValue]; nextModseq = [NSNumber numberWithUnsignedLongLong: lastModseqNbr + 1]; kvQualifier = [[EOKeyValueQualifier alloc] initWithKey: @"modseq" operatorSelector: EOQualifierOperatorGreaterThanOrEqualTo value: nextModseq]; searchQualifier = [[EOAndQualifier alloc] initWithQualifiers: kvQualifier, [self nonDeletedQualifier], nil]; [kvQualifier release]; [searchQualifier autorelease]; } else { lastModseqNbr = 0; searchQualifier = [self nonDeletedQualifier]; } /* 1. we fetch modified or added uids */ uids = [sogoObject fetchUIDsMatchingQualifier: searchQualifier sortOrdering: nil]; max = [uids count]; if (max > 0) { fetchResults = [(NSDictionary *) [sogoObject fetchUIDs: uids parts: [NSArray arrayWithObject: @"modseq"]] objectForKey: @"fetch"]; /* NOTE: we sort items manually because Cyrus does not properly sort entries with a MODSEQ of 0 */ fetchResults = [fetchResults sortedArrayUsingFunction: _compareFetchResultsByMODSEQ context: NULL]; ldb_transaction_start([[self context] connectionInfo]->oc_ctx); for (count = 0; count < max; count++) { result = [fetchResults objectAtIndex: count]; uid = [result objectForKey: @"uid"]; modseq = [result objectForKey: @"modseq"]; newChangeNum = [[self context] getNewChangeNumber]; changeNumber = [NSNumber numberWithUnsignedLongLong: newChangeNum]; messageEntry = [NSMutableDictionary new]; [messages setObject: messageEntry forKey: uid]; [messageEntry release]; [messageEntry setObject: modseq forKey: @"modseq"]; [messageEntry setObject: changeNumber forKey: @"version"]; [self logWithFormat: @"added message entry for uid %@, modseq %@," @" version %@", uid, modseq, changeNumber]; changeKey = [self getReplicaKeyFromGlobCnt: newChangeNum >> 16]; [self _setChangeKey: changeKey forMessageEntry: messageEntry]; [mapping setObject: modseq forKey: changeNumber]; if (!lastModseq || ([lastModseq compare: modseq] == NSOrderedAscending)) lastModseq = modseq; } ldb_transaction_commit([[self context] connectionInfo]->oc_ctx); [currentProperties setObject: lastModseq forKey: @"SyncLastModseq"]; foundChange = YES; } /* 2. we synchronise deleted UIDs */ if (initialLastModseq) { fetchResults = [(SOGoMailFolder *) sogoObject fetchUIDsOfVanishedItems: lastModseqNbr]; max = [fetchResults count]; changeNumber = nil; for (count = 0; count < max; count++) { uid = [fetchResults objectAtIndex: count]; if ([messages objectForKey: uid]) { newChangeNum = [[self context] getNewChangeNumber]; changeNumber = [NSNumber numberWithUnsignedLongLong: newChangeNum]; [messages removeObjectForKey: uid]; [self logWithFormat: @"removed message entry for uid %@", uid]; } } if (changeNumber) { [currentProperties setObject: changeNumber forKey: @"SyncLastDeleteChangeNumber"]; foundChange = YES; } } if (foundChange) { ti = [NSNumber numberWithDouble: [now timeIntervalSince1970]]; [currentProperties setObject: ti forKey: @"SyncLastSynchronisationDate"]; [versionsMessage save]; } return rc; } - (NSNumber *) modseqFromMessageChangeNumber: (NSNumber *) changeNum { NSDictionary *mapping; NSNumber *modseq; mapping = [[versionsMessage properties] objectForKey: @"VersionMapping"]; modseq = [mapping objectForKey: changeNum]; return modseq; } - (NSNumber *) messageUIDFromMessageKey: (NSString *) messageKey { NSNumber *messageUid; NSString *uidString; NSRange dotRange; dotRange = [messageKey rangeOfString: @".eml"]; if (dotRange.location != NSNotFound) { uidString = [messageKey substringToIndex: dotRange.location]; messageUid = [NSNumber numberWithInt: [uidString intValue]]; } else messageUid = nil; return messageUid; } - (NSNumber *) changeNumberForMessageUID: (NSNumber *) messageUid { NSDictionary *messages; NSNumber *changeNumber; messages = [[versionsMessage properties] objectForKey: @"Messages"]; changeNumber = [[messages objectForKey: messageUid] objectForKey: @"version"]; return changeNumber; } - (void) setChangeKey: (NSData *) changeKey forMessageWithKey: (NSString *) messageKey { NSMutableDictionary *messages; NSMutableDictionary *messageEntry; NSNumber *messageUid; messageUid = [self messageUIDFromMessageKey: messageKey]; messages = [[versionsMessage properties] objectForKey: @"Messages"]; messageEntry = [messages objectForKey: messageUid]; if (!messageEntry) abort (); [self _setChangeKey: changeKey forMessageEntry: messageEntry]; [versionsMessage save]; } - (NSData *) changeKeyForMessageWithKey: (NSString *) messageKey { NSDictionary *messages, *changeKeyDict; NSString *guid; NSNumber *messageUid; NSData *globCnt, *changeKey = nil; messageUid = [self messageUIDFromMessageKey: messageKey]; messages = [[versionsMessage properties] objectForKey: @"Messages"]; changeKeyDict = [[messages objectForKey: messageUid] 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; NSNumber *messageUid; NSData *globCnt; messageUid = [self messageUIDFromMessageKey: messageKey]; messages = [[versionsMessage properties] objectForKey: @"Messages"]; changeListDict = [[messages objectForKey: messageUid] 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, *deletedUIDs; NSNumber *changeNumber; uint64_t modseq; NSDictionary *versionProperties; if (tableType == MAPISTORE_MESSAGE_TABLE) { changeNumber = [NSNumber numberWithUnsignedLongLong: changeNum]; modseq = [[self modseqFromMessageChangeNumber: changeNumber] unsignedLongLongValue]; if (modseq > 0) { deletedUIDs = [(SOGoMailFolder *) sogoObject fetchUIDsOfVanishedItems: modseq]; deletedKeys = [deletedUIDs stringsWithFormat: @"%@.eml"]; if ([deletedUIDs count] > 0) { versionProperties = [versionsMessage properties]; changeNumber = [versionProperties objectForKey: @"SyncLastDeleteChangeNumber"]; *cnNbr = changeNumber; [versionsMessage save]; } } else deletedKeys = [NSArray array]; } else deletedKeys = [super getDeletedKeysFromChangeNumber: changeNum andCN: cnNbr inTableType: tableType]; return deletedKeys; } static void _appendIMAPRange (NSMutableArray *UIDs, uint32_t low, uint32_t high) { uint32_t count; for (count = low; count < high + 1; count++) [UIDs addObject: [NSNumber numberWithUnsignedLong: count]]; } static NSUInteger _parseUID (const unichar *uniString, uint32_t *newUidP) { NSUInteger count = 0; uint32_t newUid = 0; while (uniString[count] >= '0' && uniString[count] <= '9') { newUid = newUid * 10 + (uniString[count] - 48); count++; } *newUidP = newUid; return count; } static NSUInteger _parseIMAPRange (const unichar *uniString, NSArray **UIDsP) { NSMutableArray *UIDs; NSUInteger count = 0; uint32_t currentUid, rangeMin; BOOL done = NO, inRange = NO; UIDs = [NSMutableArray array]; while (!done) { count += _parseUID (uniString + count, ¤tUid); switch (uniString[count]) { case ':': inRange = YES; rangeMin = currentUid; break; case ' ': case 0: done = YES; case ',': if (inRange) { _appendIMAPRange (UIDs, rangeMin, currentUid); inRange = NO; } else [UIDs addObject: [NSNumber numberWithUnsignedLong: currentUid]]; break; default: abort (); } count++; } *UIDsP = UIDs; return count; } static void _parseCOPYUID (NSString *line, NSArray **destUIDsP) { unichar *uniString; NSUInteger count = 0, max; // char state = 'i'; /* i = init, v = validity, s = source range, d = dest range */ /* sample: 1 OK [COPYUID 1311899334 1:3 11:13] Completed */ max = [line length]; uniString = NSZoneMalloc (NULL, sizeof (unichar) * (max + 1)); [line getCharacters: uniString]; uniString[max] = 0; while (count < max && uniString[count] != ' ') count++; count++; while (count < max && uniString[count] != ' ') count++; count++; while (count < max && uniString[count] != ' ') count++; count++; if (count < max) count += _parseIMAPRange (uniString + count, destUIDsP); NSZoneFree (NULL, uniString); } // // Move (or eventually copy) the mails identified by // "srcMids" from the source folder into this folder. // - (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 { NGImap4Connection *connection; NGImap4Client *client; NSString *sourceFolderName, *targetFolderName, *messageURL, *messageKey, *v; NSMutableArray *uids, *oldMessageURLs; NSNumber *uid; NSArray *destUIDs; MAPIStoreMapping *mapping; NSDictionary *result; NSUInteger count; NSArray *a; NSData *changeKey; if (![sourceFolder isKindOfClass: [MAPIStoreMailFolder class]]) return [super moveCopyMessagesWithMIDs: srcMids andCount: midCount fromFolder: sourceFolder withMIDs: targetMids andChangeKeys: targetChangeKeys wantCopy: wantCopy]; /* Conversion of mids to IMAP uids */ mapping = [self mapping]; uids = [NSMutableArray arrayWithCapacity: midCount]; oldMessageURLs = [NSMutableArray arrayWithCapacity: midCount]; for (count = 0; count < midCount; count++) { messageURL = [mapping urlFromID: srcMids[count]]; if (messageURL) { uid = [self messageUIDFromMessageKey: [messageURL lastPathComponent]]; [uids addObject: uid]; [oldMessageURLs addObject: messageURL]; } else return MAPISTORE_ERROR; } /* IMAP COPY */ connection = [sogoObject imap4Connection]; sourceFolderName = [connection imap4FolderNameForURL: [[sourceFolder sogoObject] imap4URL]]; targetFolderName = [connection imap4FolderNameForURL: [sogoObject imap4URL]]; client = [connection client]; [client select: sourceFolderName]; result = [client copyUids: uids toFolder: targetFolderName]; if (![[result objectForKey: @"result"] boolValue]) return MAPISTORE_ERROR; /* "Move" treatment: Store \Deleted and unregister urls */ if (!wantCopy) { [client storeFlags: [NSArray arrayWithObject: @"Deleted"] forUIDs: uids addOrRemove: YES]; for (count = 0; count < midCount; count++) [mapping unregisterURLWithID: srcMids[count]]; } /* Registration of target messages */ // // We use the UIDPLUS IMAP extension here in order to speedup UID retrieval // If supported by the server, we'll get something like: COPYUID 1315425789 1 8 // // Sometimes COPYUID isn't returned at all by Cyrus or in case the server doesn't // support the UIDPLUS IMAP extension, we fallback to a simple UID search. // v = [[[result objectForKey: @"RawResponse"] objectForKey: @"ResponseResult"] objectForKey: @"flag"]; if (v) { destUIDs = nil; _parseCOPYUID (v, &destUIDs); } else { /* FIXME: this may fail if new messages are appended to the folder between the COPY and SORT operations */ [client select: targetFolderName]; a = [[client sort: @"ARRIVAL" qualifier: nil encoding: @"UTF-8"] objectForKey: @"sort"]; destUIDs = [[a sortedArrayUsingSelector: @selector (compare:)] subarrayWithRange: NSMakeRange ([a count] - midCount, midCount)]; } for (count = 0; count < midCount; count++) { messageURL = [NSString stringWithFormat: @"%@%@.eml", [self url], [destUIDs objectAtIndex: count]]; [mapping registerURL: messageURL withID: targetMids[count]]; } /* Update the change keys */ if (targetChangeKeys) { [self synchroniseCache]; for (count = 0; count < midCount; count++) { changeKey = [NSData dataWithBinary: targetChangeKeys[count]]; messageKey = [NSString stringWithFormat: @"%@.eml", [destUIDs objectAtIndex: count]]; [self setChangeKey: changeKey forMessageWithKey: messageKey]; } } [self postNotificationsForMoveCopyMessagesWithMIDs: srcMids andMessageURLs: oldMessageURLs andCount: midCount fromFolder: sourceFolder withMIDs: targetMids wantCopy: wantCopy]; // We cleanup cache of our source and destination folders [self cleanupCaches]; [sourceFolder cleanupCaches]; return MAPISTORE_SUCCESS; } - (enum mapistore_error) moveCopyToFolder: (MAPIStoreFolder *) targetFolder withNewName: (NSString *) newFolderName isMove: (BOOL) isMove isRecursive: (BOOL) isRecursive { enum mapistore_error rc; NSURL *folderURL, *newFolderURL; struct SRow folderRow; struct SPropValue nameProperty; MAPIStoreMailFolder *newFolder; SOGoMailFolder *targetSOGoFolder; NSMutableArray *uids; NSArray *childKeys; NSUInteger count, max; NGImap4Connection *connection; NGImap4Client *client; NSString *newURL, *parentDBFolderPath, *childKey, *folderIMAPName, *newFolderIMAPName; NSException *error; MAPIStoreMapping *mapping; NSDictionary *result; if ([targetFolder isKindOfClass: MAPIStoreMailFolderK]) { folderURL = [sogoObject imap4URL]; if (!newFolderName) newFolderName = [sogoObject displayName]; newFolderName = [newFolderName stringByEscapingURL]; targetSOGoFolder = [targetFolder sogoObject]; newFolderURL = [NSURL URLWithString: newFolderName relativeToURL: [targetSOGoFolder imap4URL]]; if (isMove) { error = [[sogoObject imap4Connection] moveMailboxAtURL: folderURL toURL: newFolderURL]; if (error) rc = MAPISTORE_ERR_DENIED; else { rc = MAPISTORE_SUCCESS; mapping = [self mapping]; newURL = [NSString stringWithFormat: @"%@folder%@/", [targetFolder url], newFolderName]; [mapping updateID: [self objectId] withURL: newURL]; parentDBFolderPath = [[targetFolder dbFolder] path]; if (!parentDBFolderPath) parentDBFolderPath = @""; [dbFolder changePathTo: [NSString stringWithFormat: @"%@/folder%@", parentDBFolderPath, newFolderName]]; } } else { nameProperty.ulPropTag = PidTagDisplayName; nameProperty.value.lpszW = [newFolderName UTF8String]; folderRow.lpProps = &nameProperty; folderRow.cValues = 1; rc = [targetFolder createFolder: &folderRow withFID: -1 andKey: &childKey]; if (rc == MAPISTORE_SUCCESS) { newFolder = [targetFolder lookupFolder: childKey]; connection = [sogoObject imap4Connection]; folderIMAPName = [connection imap4FolderNameForURL: [sogoObject imap4URL]]; newFolderIMAPName = [connection imap4FolderNameForURL: [[newFolder sogoObject] imap4URL]]; client = [connection client]; [client select: folderIMAPName]; childKeys = [self messageKeys]; max = [childKeys count]; uids = [NSMutableArray arrayWithCapacity: max]; for (count = 0; count < max; count++) { childKey = [childKeys objectAtIndex: count]; [uids addObject: [self messageUIDFromMessageKey: childKey]]; } result = [client copyUids: uids toFolder: newFolderIMAPName]; if ([[result objectForKey: @"result"] boolValue]) { if (isRecursive) { childKeys = [self folderKeys]; max = [childKeys count]; for (count = 0; count < max; count++) { childKey = [childKeys objectAtIndex: count]; [[self lookupFolder: childKey] moveCopyToFolder: newFolder withNewName: nil isMove: NO isRecursive: YES]; } } } else rc = MAPISTORE_ERROR; } } [targetFolder cleanupCaches]; } else rc = [super moveCopyToFolder: targetFolder withNewName: newFolderName isMove: isMove isRecursive: isRecursive]; return rc; } - (MAPIStoreMessage *) createMessage { SOGoMAPIObject *childObject; childObject = [SOGoMAPIObject objectWithName: [SOGoMAPIObject globallyUniqueObjectId] inContainer: sogoObject]; return [MAPIStoreMailVolatileMessage mapiStoreObjectWithSOGoObject: childObject inContainer: self]; } - (NSArray *) rolesForExchangeRights: (uint32_t) rights { NSMutableArray *roles; roles = [NSMutableArray arrayWithCapacity: 6]; if (rights & RoleOwner) [roles addObject: SOGoMailRole_Administrator]; if (rights & RightsCreateItems) { [roles addObject: SOGoRole_ObjectCreator]; [roles addObject: SOGoMailRole_Writer]; [roles addObject: SOGoMailRole_Poster]; } if (rights & RightsDeleteAll) { [roles addObject: SOGoRole_ObjectEraser]; [roles addObject: SOGoRole_FolderEraser]; [roles addObject: SOGoMailRole_Expunger]; } if (rights & RightsEditAll) [roles addObject: SOGoRole_ObjectEditor]; if (rights & RightsReadItems) [roles addObject: SOGoRole_ObjectViewer]; if (rights & RightsCreateSubfolders) [roles addObject: SOGoRole_FolderCreator]; // [self logWithFormat: @"roles for rights %.8x = (%@)", rights, roles]; return roles; } - (uint32_t) exchangeRightsForRoles: (NSArray *) roles { uint32_t rights = 0; if ([roles containsObject: SOGoMailRole_Administrator]) rights |= (RoleOwner ^ RightsAll); if ([roles containsObject: SOGoRole_ObjectCreator]) rights |= RightsCreateItems; if ([roles containsObject: SOGoRole_ObjectEraser] && [roles containsObject: SOGoRole_FolderEraser]) rights |= RightsDeleteAll; if ([roles containsObject: SOGoRole_ObjectEditor]) rights |= RightsEditAll; if ([roles containsObject: SOGoRole_ObjectViewer]) rights |= RightsReadItems; if ([roles containsObject: SOGoRole_FolderCreator]) rights |= RightsCreateSubfolders; if (rights != 0) rights |= RoleNone; /* actually "folder visible" */ // [self logWithFormat: @"rights for roles (%@) = %.8x", roles, rights]; return rights; } @end @implementation MAPIStoreOutboxFolder - (int) getPidTagDisplayName: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { *data = [@"Outbox" asUnicodeInMemCtx: memCtx]; return MAPISTORE_SUCCESS; } @end