/* 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 "MAPIApplication.h" #import "MAPIStoreAppointmentWrapper.h" #import "MAPIStoreContext.h" #import "MAPIStoreDraftsMessage.h" #import "MAPIStoreFAIMessage.h" #import "MAPIStoreMailMessage.h" #import "MAPIStoreMailMessageTable.h" #import "MAPIStoreMapping.h" #import "MAPIStoreTypes.h" #import "NSData+MAPIStore.h" #import "NSString+MAPIStore.h" #import "SOGoMAPIFSMessage.h" #import "MAPIStoreMailFolder.h" static Class MAPIStoreDraftsMessageK; static Class MAPIStoreMailMessageK; static Class SOGoMailFolderK; #undef DEBUG #include #include #include @implementation MAPIStoreMailFolder + (void) initialize { MAPIStoreMailMessageK = [MAPIStoreMailMessage class]; SOGoMailFolderK = [SOGoMailFolder class]; [MAPIStoreAppointmentWrapper class]; } - (id) initWithURL: (NSURL *) newURL inContext: (MAPIStoreContext *) newContext { SOGoUserFolder *userFolder; SOGoMailAccounts *accountsFolder; SOGoMailAccount *accountFolder; SOGoFolder *currentContainer; WOContext *woContext; if ((self = [super initWithURL: newURL inContext: newContext])) { woContext = [newContext woContext]; userFolder = [SOGoUserFolder objectWithName: [newURL user] inContainer: MAPIApp]; [parentContainersBag addObject: userFolder]; [woContext setClientObject: userFolder]; accountsFolder = [userFolder lookupName: @"Mail" inContext: woContext acquire: NO]; [parentContainersBag addObject: accountsFolder]; [woContext setClientObject: accountsFolder]; accountFolder = [accountsFolder lookupName: @"0" inContext: woContext acquire: NO]; [[accountFolder imap4Connection] enableExtension: @"QRESYNC"]; [parentContainersBag addObject: accountFolder]; [woContext setClientObject: accountFolder]; sogoObject = [self specialFolderFromAccount: accountFolder inContext: woContext]; [sogoObject retain]; currentContainer = [sogoObject container]; while (currentContainer != (SOGoFolder *) accountFolder) { [parentContainersBag addObject: currentContainer]; currentContainer = [currentContainer container]; } ASSIGN (versionsMessage, [SOGoMAPIFSMessage objectWithName: @"versions.plist" inContainer: propsFolder]); } return self; } - (id) initWithSOGoObject: (id) newSOGoObject inContainer: (MAPIStoreObject *) newContainer { NSURL *propsURL; NSString *urlString; if ((self = [super initWithSOGoObject: newSOGoObject inContainer: newContainer])) { urlString = [[self url] stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]; propsURL = [NSURL URLWithString: urlString]; ASSIGN (versionsMessage, [SOGoMAPIFSMessage objectWithName: @"versions.plist" inContainer: propsFolder]); } return self; } - (void) dealloc { [versionsMessage release]; [super dealloc]; } - (SOGoMailFolder *) specialFolderFromAccount: (SOGoMailAccount *) accountFolder inContext: (WOContext *) woContext { [self subclassResponsibility: _cmd]; return nil; } - (MAPIStoreMessageTable *) messageTable { [self synchroniseCache]; return [MAPIStoreMailMessageTable tableForContainer: self]; } - (Class) messageClass { return MAPIStoreMailMessageK; } - (NSString *) createFolder: (struct SRow *) aRow withFID: (uint64_t) newFID inContainer: (id) subfolderParent { 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: subfolderParent]; if (![newFolder create]) nameInContainer = nil; } return nameInContainer; } - (NSString *) createFolder: (struct SRow *) aRow withFID: (uint64_t) newFID { return [self createFolder: aRow withFID: newFID inContainer: sogoObject]; } - (int) getPrContentUnread: (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) getPrContainerClass: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { *data = [@"IPF.Note" asUnicodeInMemCtx: memCtx]; return MAPISTORE_SUCCESS; } - (int) getPrMessageClass: (void **) data inMemCtx: (TALLOC_CTX *) memCtx { *data = [@"IPM.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 (!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]; return [uidKeys stringsWithFormat: @"%@.eml"]; } - (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 (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]; return subfolderKeys; } - (id) lookupFolder: (NSString *) childKey { id childObject = nil; SOGoMailFolder *childFolder; [self folderKeys]; if ([folderKeys containsObject: childKey]) { childFolder = [sogoObject lookupName: childKey inContext: nil acquire: NO]; childObject = [MAPIStoreMailFolder mapiStoreObjectWithSOGoObject: childFolder inContainer: self]; } return childObject; } - (NSCalendarDate *) creationTime { return [NSCalendarDate dateWithTimeIntervalSince1970: 0x4dbb2dbe]; /* oc_version_time */ } - (NSDate *) lastMessageModificationTime { NSNumber *ti; NSDate *value = nil; ti = [[versionsMessage properties] objectForKey: @"SyncLastSynchronisationDate"]; if (ti) value = [NSDate dateWithTimeIntervalSince1970: [ti doubleValue]]; else value = [NSDate date]; [self logWithFormat: @"lastMessageModificationTime: %@", value]; return value; } /* 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]; 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); changeKeyDict = [NSDictionary dictionaryWithObjectsAndKeys: guid, @"GUID", globCnt, @"LocalId", nil]; /* 1. set change key association */ [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, *lastModseq, *nextModseq, *uid; EOQualifier *searchQualifier; NSArray *uids; NSUInteger count, max; NSArray *fetchResults; NSDictionary *result; NSData *changeKey; NSMutableDictionary *currentProperties, *messages, *mapping, *messageEntry; NSCalendarDate *now; now = [NSCalendarDate date]; [now setTimeZone: utcTZ]; currentProperties = [[versionsMessage properties] mutableCopy]; if (!currentProperties) currentProperties = [NSMutableDictionary new]; [currentProperties autorelease]; 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"]; if (lastModseq) { nextModseq = [NSNumber numberWithUnsignedLongLong: [lastModseq unsignedLongLongValue] + 1]; searchQualifier = [[EOKeyValueQualifier alloc] initWithKey: @"modseq" operatorSelector: EOQualifierOperatorGreaterThanOrEqualTo value: nextModseq]; [searchQualifier autorelease]; } else searchQualifier = [self nonDeletedQualifier]; 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"]; [self logWithFormat: @"uid '%@' has modseq '%@'", uid, 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"]; 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); ti = [NSNumber numberWithDouble: [now timeIntervalSince1970]]; [currentProperties setObject: ti forKey: @"SyncLastSynchronisationDate"]; [currentProperties setObject: lastModseq forKey: @"SyncLastModseq"]; [versionsMessage appendProperties: currentProperties]; [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 *) _dataFromChangeKeyGUID: (NSString *) guidString andCnt: (NSData *) globCnt { NSMutableData *changeKey; struct GUID guid; changeKey = [NSMutableData dataWithCapacity: 16 + [globCnt length]]; [guidString extractGUID: &guid]; [changeKey appendData: [NSData dataWithGUID: &guid]]; [changeKey appendData: globCnt]; return changeKey; } - (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 = [self _dataFromChangeKeyGUID: guid andCnt: globCnt]; } return changeKey; } - (NSData *) predecessorChangeListForMessageWithKey: (NSString *) messageKey { NSMutableData *changeKeys = nil; NSDictionary *messages, *changeListDict; NSArray *keys; 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) { changeKeys = [NSMutableData data]; keys = [changeListDict allKeys]; max = [keys count]; for (count = 0; count < max; count++) { guid = [keys objectAtIndex: count]; globCnt = [changeListDict objectForKey: guid]; changeKey = [self _dataFromChangeKeyGUID: guid andCnt: globCnt]; [changeKeys appendUInt8: [changeKey length]]; [changeKeys appendData: changeKey]; } } return changeKeys; } - (NSArray *) getDeletedKeysFromChangeNumber: (uint64_t) changeNum andCN: (NSNumber **) cnNbr inTableType: (uint8_t) tableType { NSArray *deletedKeys, *deletedUIDs; NSNumber *changeNumNbr; uint64_t modseq; NSDictionary *versionProperties, *status; NSMutableDictionary *messages, *mapping; NSNumber *newChangeNumNbr, *highestModseq; uint64_t newChangeNum; NSUInteger count, max; if (tableType == MAPISTORE_MESSAGE_TABLE) { changeNumNbr = [NSNumber numberWithUnsignedLongLong: changeNum]; modseq = [[self modseqFromMessageChangeNumber: changeNumNbr] unsignedLongLongValue]; if (modseq > 0) { status = [sogoObject statusForFlags: [NSArray arrayWithObject: @"HIGHESTMODSEQ"]]; highestModseq = [status objectForKey: @"highestmodseq"]; versionProperties = [versionsMessage properties]; messages = [versionProperties objectForKey: @"Messages"]; deletedUIDs = [(SOGoMailFolder *) sogoObject fetchUIDsOfVanishedItems: modseq]; deletedKeys = [deletedUIDs stringsWithFormat: @"%@.eml"]; max = [deletedUIDs count]; if (max > 0) { [messages removeObjectsForKeys: deletedUIDs]; mapping = [versionProperties objectForKey: @"VersionsMapping"]; for (count = 0; count < max; count++) newChangeNum = [[self context] getNewChangeNumber]; newChangeNumNbr = [NSNumber numberWithUnsignedLongLong: newChangeNum]; *cnNbr = newChangeNumNbr; [mapping setObject: newChangeNumNbr forKey: @"SyncLastModseq"]; [versionsMessage save]; } } else deletedKeys = [NSArray array]; } else deletedKeys = [super getDeletedKeysFromChangeNumber: changeNum andCN: cnNbr inTableType: tableType]; return deletedKeys; } // // We need to support creating "fake" emails for RopCopyTo and other // other ROPs to function properly. We don't know what will be the // object's name (as it's the IMAP's UID) until the message really // is created in the mail store. // - (MAPIStoreMessage *) createMessageWithMID: (uint64_t) mid { MAPIStoreMessage *message; SOGoMailObject *mail; mail = [SOGoMailObject objectWithName: [NSString stringWithFormat: @"%llu", mid] inContainer: sogoObject]; message = [MAPIStoreMailMessage mapiStoreObjectWithSOGoObject: mail inContainer: self]; return message; } // // Move (or eventually copy) the mail identified by // "mid" within this current folder. The message is // of course coming from an other folder // - (int) moveCopyMessageWithMID: (uint64_t) mid toFolder: (MAPIStoreFolder *) targetFolder inMessage: (MAPIStoreMessage *) targetMessage wantCopy: (uint8_t) want_copy { NSString *folderName, *messageURL, *uid, *url, *v; MAPIStoreMapping *mapping; NSDictionary *result; unsigned int new_uid; uint64_t target_mid; int rc; // FIXME // We only support IMAP-to-IMAP copy operations for now. // Otherwise we silently fail (for now, at least!) if (![targetFolder isKindOfClass: [MAPIStoreMailFolder class]]) return MAPISTORE_SUCCESS; mapping = [[self context] mapping]; messageURL = [mapping urlFromID: mid]; if (messageURL) { // We get the message UID from that folder by stripping the .eml // This is the message we'll copy in the folder specified by targetURL uid = [messageURL lastPathComponent]; uid = [uid substringToIndex: [uid length] - 4]; folderName = [[targetFolder sogoObject] relativeImap4Name]; if (uid && [folderName length] > 0) { MAPIStoreMessage *message; NSArray *a, *activeTables; NGImap4Client *client; struct mapistore_object_notification_parameters *notif_parameters; struct mapistore_connection_info *connInfo; NSUInteger count, max; // We copy the message, get the new UID and set the old one as deleted client = [[[self sogoObject] imap4Connection] client]; [client select: [[self sogoObject] relativeImap4Name]]; result = [client copyUid: [uid intValue] toFolder: folderName]; // We check if the COPY operation succeeded if (![[result objectForKey: @"result"] boolValue]) return MAPISTORE_ERR_NOT_FOUND; if (!want_copy) [client storeUid: [uid intValue] add: [NSNumber numberWithBool: YES] flags: [NSArray arrayWithObject: @"Deleted"]]; // // We use the UIDPLUS IMAP extension here in order to speedup UID retreival // 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) { unsigned int current_uid, uid_validity; const char *s; char tag[7]; s = [v cStringUsingEncoding: NSASCIIStringEncoding]; sscanf(s, "%s %u %u %u", tag, &uid_validity, ¤t_uid, &new_uid); } else { [client select: folderName]; a = [[client sort: @"ARRIVAL" qualifier: nil encoding: @"UTF-8"] objectForKey: @"sort"]; new_uid = [[[a sortedArrayUsingSelector: @selector(compare:)] lastObject] intValue]; } // We compute the URL of the move message and update our mapping url = [NSString stringWithFormat: @"%@%d.eml", [targetFolder url], new_uid]; if (!want_copy) [mapping unregisterURLWithID: mid]; // We adjust its name within the container with the newly obtained UID. This was previously the // MID of the message and that value was temporary since we created a "fake" message target_mid = strtoull([[[targetMessage sogoObject] nameInContainer] UTF8String], NULL, 10); [[targetMessage sogoObject] setNameInContainer: [NSString stringWithFormat: @"%u.eml", new_uid]]; // We unregister the previously (and temporary) registered mid and register // it again with its new and valid URL [mapping unregisterURLWithID: target_mid]; [mapping registerURL: url withID: target_mid]; // For the "source folder, we ensure the table caches are loaded so // that old and new state can be compared message = [self lookupMessageByURL: messageURL]; activeTables = [self activeMessageTables]; max = [activeTables count]; for (count = 0; count < max; count++) [[activeTables objectAtIndex: count] restrictedChildKeys]; // We notify the client. We start with the source folder. notif_parameters = talloc_zero(NULL, struct mapistore_object_notification_parameters); notif_parameters->object_id = [self objectId]; notif_parameters->tag_count = 5; notif_parameters->tags = talloc_array (notif_parameters, enum MAPITAGS, 5); notif_parameters->tags[0] = PR_CONTENT_COUNT; notif_parameters->tags[1] = PR_DELETED_COUNT_TOTAL; notif_parameters->tags[2] = PR_MESSAGE_SIZE; notif_parameters->tags[3] = PR_NORMAL_MESSAGE_SIZE; notif_parameters->tags[4] = PR_RECIPIENT_ON_NORMAL_MSG_COUNT; notif_parameters->new_message_count = true; notif_parameters->message_count = [[self messageKeys] count] - 1; connInfo = [[self context] connectionInfo]; mapistore_push_notification (connInfo->mstore_ctx, MAPISTORE_FOLDER, MAPISTORE_OBJECT_MODIFIED, notif_parameters); talloc_free(notif_parameters); // move/copy notification of the copied/moved message notif_parameters = talloc_zero(NULL, struct mapistore_object_notification_parameters); notif_parameters->tag_count = 0; notif_parameters->new_message_count = true; notif_parameters->message_count = 0; notif_parameters->object_id = target_mid; notif_parameters->folder_id = [targetFolder objectId]; notif_parameters->old_object_id = mid; notif_parameters->old_folder_id = [self objectId]; mapistore_push_notification (connInfo->mstore_ctx, MAPISTORE_MESSAGE, (want_copy ? MAPISTORE_OBJECT_COPIED : MAPISTORE_OBJECT_MOVED), notif_parameters); talloc_free(notif_parameters); // table notification for (count = 0; count < max; count++) [[activeTables objectAtIndex: count] notifyChangesForChild: message]; // For the "destination folder, we ensure the table caches are loaded so // that old and new state can be compared message = targetMessage; activeTables = [targetFolder activeMessageTables]; max = [activeTables count]; for (count = 0; count < max; count++) [[activeTables objectAtIndex: count] restrictedChildKeys]; notif_parameters = talloc_zero(NULL, struct mapistore_object_notification_parameters); notif_parameters->object_id = [targetFolder objectId]; notif_parameters->tag_count = 5; notif_parameters->tags = talloc_array (notif_parameters, enum MAPITAGS, 5); notif_parameters->tags[0] = PR_CONTENT_COUNT; notif_parameters->tags[1] = PR_DELETED_COUNT_TOTAL; notif_parameters->tags[2] = PR_MESSAGE_SIZE; notif_parameters->tags[3] = PR_NORMAL_MESSAGE_SIZE; notif_parameters->tags[4] = PR_RECIPIENT_ON_NORMAL_MSG_COUNT; notif_parameters->new_message_count = true; notif_parameters->message_count = [[targetFolder messageKeys] count] + 1; connInfo = [[self context] connectionInfo]; mapistore_push_notification (connInfo->mstore_ctx, MAPISTORE_FOLDER, MAPISTORE_OBJECT_MODIFIED, notif_parameters); talloc_free(notif_parameters); // table notification for (count = 0; count < max; count++) [[activeTables objectAtIndex: count] notifyChangesForChild: message]; // We cleanup cache of our source and destination folders [self cleanupCaches]; [targetFolder cleanupCaches]; rc = MAPISTORE_SUCCESS; } else rc = MAPISTORE_ERR_NOT_FOUND; } else rc = MAPISTORE_ERR_NOT_FOUND; return rc; } @end @implementation MAPIStoreInboxFolder : MAPIStoreMailFolder - (id) initWithURL: (NSURL *) newURL inContext: (MAPIStoreContext *) newContext { NSDictionary *list, *response; NGImap4Client *client; if ((self = [super initWithURL: newURL inContext: newContext])) { client = [[(SOGoMailFolder *) sogoObject imap4Connection] client]; list = [client list: @"" pattern: @"INBOX"]; response = [[list objectForKey: @"RawResponse"] objectForKey: @"list"]; usesAltNameSpace = [[response objectForKey: @"flags"] containsObject: @"noinferiors"]; } return self; } - (SOGoMailFolder *) specialFolderFromAccount: (SOGoMailAccount *) accountFolder inContext: (WOContext *) woContext { return [accountFolder inboxFolderInContext: woContext]; } - (NSString *) createFolder: (struct SRow *) aRow withFID: (uint64_t) newFID { id subfolderParent; if (usesAltNameSpace) subfolderParent = [(SOGoMailFolder *) sogoObject mailAccountFolder]; else subfolderParent = sogoObject; return [self createFolder: aRow withFID: newFID inContainer: subfolderParent]; } - (NSMutableString *) _imapFolderNameRepresentation: (NSString *) subfolderName { NSMutableString *representation; if (usesAltNameSpace) { /* with "altnamespace", the subfolders are NEVER subfolders of INBOX... */; if (![subfolderName hasPrefix: @"folder"]) abort (); representation = [NSMutableString stringWithString: [subfolderName substringFromIndex: 6]]; } else representation = [super _imapFolderNameRepresentation: subfolderName]; return representation; } - (NSArray *) folderKeysMatchingQualifier: (EOQualifier *) qualifier andSortOrderings: (NSArray *) sortOrderings { NSMutableArray *subfolderKeys; SOGoMailAccount *account; if (usesAltNameSpace) { if (qualifier) [self errorWithFormat: @"qualifier is not used for folders"]; if (sortOrderings) [self errorWithFormat: @"sort orderings are not used for folders"]; account = [(SOGoMailFolder *) sogoObject mailAccountFolder]; subfolderKeys = [[account toManyRelationshipKeysWithNamespaces: NO] mutableCopy]; [subfolderKeys removeObject: @"folderINBOX"]; [self _cleanupSubfolderKeys: subfolderKeys]; } else subfolderKeys = [[super folderKeysMatchingQualifier: qualifier andSortOrderings: sortOrderings] mutableCopy]; /* TODO: remove special folders */ [subfolderKeys autorelease]; return subfolderKeys; } - (id) lookupFolder: (NSString *) childKey { id childObject = nil; SOGoMailAccount *account; SOGoMailFolder *childFolder; if (usesAltNameSpace) { [self folderKeys]; if ([folderKeys containsObject: childKey]) { account = [(SOGoMailFolder *) sogoObject mailAccountFolder]; childFolder = [account lookupName: childKey inContext: nil acquire: NO]; childObject = [MAPIStoreMailFolder mapiStoreObjectWithSOGoObject: childFolder inContainer: self]; } } else childObject = [super lookupFolder: childKey]; return childObject; } @end @implementation MAPIStoreSentItemsFolder : MAPIStoreMailFolder - (SOGoMailFolder *) specialFolderFromAccount: (SOGoMailAccount *) accountFolder inContext: (WOContext *) woContext { return [accountFolder sentFolderInContext: woContext]; } @end @implementation MAPIStoreDraftsFolder : MAPIStoreMailFolder - (SOGoMailFolder *) specialFolderFromAccount: (SOGoMailAccount *) accountFolder inContext: (WOContext *) woContext { return [accountFolder draftsFolderInContext: woContext]; } @end // @implementation MAPIStoreDeletedItemsFolder : MAPIStoreMailFolder // - (SOGoMailFolder *) specialFolderFromAccount: (SOGoMailAccount *) accountFolder // inContext: (WOContext *) woContext // { // return [accountFolder trashFolderInContext: woContext]; // } // @end // // // @implementation MAPIStoreOutboxFolder : MAPIStoreMailFolder + (void) initialize { MAPIStoreDraftsMessageK = [MAPIStoreDraftsMessage class]; } - (SOGoMailFolder *) specialFolderFromAccount: (SOGoMailAccount *) accountFolder inContext: (WOContext *) woContext { return [accountFolder draftsFolderInContext: woContext]; } - (Class) messageClass { return MAPIStoreDraftsMessageK; } - (MAPIStoreMessage *) createMessageWithMID: (uint64_t) mid { MAPIStoreDraftsMessage *newMessage; SOGoDraftObject *newDraft; newDraft = [sogoObject newDraft]; newMessage = [MAPIStoreDraftsMessage mapiStoreObjectWithSOGoObject: newDraft inContainer: self]; return newMessage; } @end