+/* IMAP4 path/url processing methods */
+
+NSArray *SOGoMailGetDirectChildren(NSArray *_array, NSString *_fn) {
+ /*
+ Scans string '_array' for strings which start with the string in '_fn'.
+ Then split on '/'.
+ */
+ NSMutableArray *ma;
+ unsigned i, count, prefixlen;
+
+ if ((count = [_array count]) < 2)
+ /* one entry is the folder itself, so we need at least two */
+ return [NSArray array];
+
+#if __APPLE__
+ // TODO: somehow results are different on OSX
+ prefixlen = [_fn isEqualToString:@""] ? 0 : [_fn length] + 1;
+#else
+ prefixlen = [_fn isEqualToString:@"/"] ? 1 : [_fn length] + 1;
+#endif
+ ma = [NSMutableArray arrayWithCapacity:count];
+ for (i = 0; i < count; i++) {
+ NSString *p;
+
+ p = [_array objectAtIndex:i];
+ if ([p length] <= prefixlen)
+ continue;
+ if (prefixlen != 0 && ![p hasPrefix:_fn])
+ continue;
+
+ /* cut of common part */
+ p = [p substringFromIndex:prefixlen];
+
+ /* check whether the path is a sub-subfolder path */
+ if ([p rangeOfString:@"/"].length > 0)
+ continue;
+
+ [ma addObject:p];
+ }
+
+ [ma sortUsingSelector:@selector(compare:)];
+ return ma;
+}
+
+NSArray *SOGoMailExtractSubfolders(NSURL *_url, NSDictionary *_result) {
+ NSString *folderName;
+ NSDictionary *result;
+ NSArray *names;
+ NSArray *flags;
+
+ /* Note: the result is normalized, that is, it contains / as the separator */
+ folderName = [_url path];
+#if __APPLE__
+ /* normalized results already have the / in front on libFoundation?! */
+ if ([folderName hasPrefix:@"/"])
+ folderName = [folderName substringFromIndex:1];
+#endif
+
+ result = [_result valueForKey:@"list"];
+
+ /* Cyrus already tells us whether we need to check for children */
+ flags = [result objectForKey:folderName];
+ if ([flags containsObject:@"hasnochildren"]) {
+ if (debugKeys)
+ NSLog(@"%s: folder %@ has no children.", __PRETTY_FUNCTION__,folderName);
+ return nil;
+ }
+
+ if (debugKeys) {
+ NSLog(@"%s: all keys %@: %@", __PRETTY_FUNCTION__, folderName,
+ [[result allKeys] componentsJoinedByString:@", "]);
+ }
+
+ names = SOGoMailGetDirectChildren([result allKeys], folderName);
+ if (debugKeys) {
+ NSLog(@"%s: subfolders of '%@': %@", __PRETTY_FUNCTION__, folderName,
+ [names componentsJoinedByString:@","]);
+ }
+ return names;
+}
+
+- (NSArray *)_getDirectChildren:(NSArray *)_array folderName:(NSString *)_fn {
+ return SOGoMailGetDirectChildren(_array, _fn);
+}
+- (NSArray *)extractSubfoldersForURL:(NSURL *)_url
+ fromResultSet:(NSDictionary *)_result
+{
+ return SOGoMailExtractSubfolders(_url, _result);
+}
+
+- (NSString *)imap4Separator {
+ // TODO: make server specific ivar!
+ return imap4Separator;
+}
+
+- (NSString *)imap4FolderNameForURL:(NSURL *)_url removeFileName:(BOOL)_delfn {
+ /* a bit hackish, but should be OK */
+ NSString *folderName;
+ NSArray *names;
+
+ if (_url == nil)
+ return nil;
+
+ folderName = [_url path];
+ if ([folderName length] == 0)
+ return nil;
+ if ([folderName characterAtIndex:0] == '/')
+ folderName = [folderName substringFromIndex:1];
+
+ if (_delfn) folderName = [folderName stringByDeletingLastPathComponent];
+
+ if ([[self imap4Separator] isEqualToString:@"/"])
+ return folderName;
+
+ names = [folderName pathComponents];
+ return [names componentsJoinedByString:[self imap4Separator]];
+}
+- (NSString *)imap4FolderNameForURL:(NSURL *)_url {
+ return [self imap4FolderNameForURL:_url removeFileName:NO];
+}
+
+- (NSArray *)extractFoldersFromResultSet:(NSDictionary *)_result {
+ /* Note: the result is normalized, that is, it contains / as the separator */
+ return [[_result valueForKey:@"list"] allKeys];
+}
+
+/* folder selections */
+
+- (BOOL)selectFolder:(id)_url {
+ NSDictionary *result;
+ NSString *newFolder;
+
+ newFolder = [_url isKindOfClass:[NSURL class]]
+ ? [self imap4FolderNameForURL:_url]
+ : _url;
+
+ if (!alwaysSelect) {
+ if ([[[self client] selectedFolderName] isEqualToString:newFolder])
+ return YES;
+ }
+
+ result = [[self client] select:newFolder];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ [self errorWithFormat:@"could not select URL: %@: %@", _url, result];
+ return NO;
+ }
+
+ return YES;
+}
+
+- (BOOL)isPermissionDeniedResult:(id)_result {
+ if ([[_result valueForKey:@"result"] intValue] != 0)
+ return NO;
+
+ return [[_result valueForKey:@"reason"]
+ isEqualToString:@"Permission denied"];
+}
+
+/* folder operations */
+
+- (NSArray *)subfoldersForURL:(NSURL *)_url {
+ NSDictionary *result;
+
+ /* check hierarchy cache */
+
+ if ((result = [self cachedHierarchyResults]) != nil)
+ return [self extractSubfoldersForURL:_url fromResultSet:result];
+
+ [self debugWithFormat:@" no folders cached yet .."];
+
+ /* fetch _all_ folders */
+
+ result = [[self client] list:(onlyFetchInbox ? @"INBOX" : @"*")
+ pattern:@"*"];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ [self errorWithFormat:@"listing of folder failed!"];
+ return nil;
+ }
+
+ /* cache results */
+
+ if ([result isNotNull]) {
+ [self cacheHierarchyResults:result];
+ if (debugCache) {
+ [self logWithFormat:@"cached results in entry %@: 0x%08X(%d)",
+ self, result, [result count]];
+ }
+ }
+
+ /* extract list */
+
+ return [self extractSubfoldersForURL:_url fromResultSet:result];
+}
+
+- (NSArray *)allFoldersForURL:(NSURL *)_url {
+ NSDictionary *result;
+
+ /* check hierarchy cache */
+
+ if ((result = [self cachedHierarchyResults]) != nil)
+ return [self extractFoldersFromResultSet:result];
+
+ [self debugWithFormat:@" no folders cached yet .."];
+
+ /* fetch _all_ folders */
+
+ result = [[self client] list:@"INBOX" pattern:@"*"];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ [self logWithFormat:@"ERROR: listing of folder failed!"];
+ return nil;
+ }
+
+ /* cache results */
+
+ if ([result isNotNull]) {
+ [self cacheHierarchyResults:result];
+ if (debugCache) {
+ [self logWithFormat:@"cached results in entry %@: 0x%08X(%d)",
+ self, result, [result count]];
+ }
+ }
+
+ /* extract list */
+ return [self extractFoldersFromResultSet:result];
+}
+
+/* message operations */
+
+- (NSArray *)fetchUIDsInURL:(NSURL *)_url qualifier:(id)_qualifier
+ sortOrdering:(id)_so
+{
+ /*
+ sortOrdering can be an NSString, an EOSortOrdering or an array of EOS.
+ */
+ NSDictionary *result;
+ NSArray *uids;
+
+ /* check cache */
+
+ uids = [self cachedUIDsForURL:_url qualifier:_qualifier sortOrdering:_so];
+ if (uids != nil) {
+ if (debugCache) [self logWithFormat:@"reusing uid cache!"];
+ return [uids isNotNull] ? uids : nil;
+ }
+
+ /* select folder and fetch */
+
+ if (![self selectFolder:_url])
+ return nil;
+
+ result = [[self client] sort:_so qualifier:_qualifier encoding:@"UTF-8"];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ [self errorWithFormat:@"could not sort contents of URL: %@", _url];
+ return nil;
+ }
+
+ uids = [result valueForKey:@"sort"];
+ if (![uids isNotNull]) {
+ [self errorWithFormat:@"got no UIDs for URL: %@: %@", _url, result];
+ return nil;
+ }
+
+ /* cache */
+
+ [self cacheUIDs:uids forURL:_url qualifier:_qualifier sortOrdering:_so];
+ return uids;
+}
+
+- (NSArray *)fetchUIDs:(NSArray *)_uids inURL:(NSURL *)_url
+ parts:(NSArray *)_parts
+{
+ // currently returns a dict?!
+ /*
+ Allowed fetch keys:
+ UID
+ BODY.PEEK[<section>]<<partial>>
+ BODY [this is the bodystructure, supported]
+ BODYSTRUCTURE [not supported yet!]
+ ENVELOPE [this is a parsed header, but does not include type]
+ FLAGS
+ INTERNALDATE
+ RFC822
+ RFC822.HEADER
+ RFC822.SIZE
+ RFC822.TEXT
+ */
+ NSDictionary *result;
+
+ if (_uids == nil)
+ return nil;
+ if ([_uids count] == 0)
+ return nil; // TODO: might break empty folders?! return a dict!
+
+ /* select folder */
+
+ if (![self selectFolder:_url])
+ return nil;
+
+ /* fetch parts */
+
+ // TODO: split uids into batches, otherwise Cyrus will complain
+ // => not really important because we batch before (in the sort)
+ // if the list is too long, we get a:
+ // "* BYE Fatal error: word too long"
+
+ result = [[self client] fetchUids:_uids parts:_parts];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ [self errorWithFormat:@"could not fetch %d uids for url: %@",
+ [_uids count],_url];
+ return nil;
+ }
+
+ //[self logWithFormat:@"RESULT: %@", result];
+ return (id)result;
+}
+
+- (id)fetchURL:(NSURL *)_url parts:(NSArray *)_parts {
+ // currently returns a dict
+ NSDictionary *result;
+ NSString *uid;
+
+ if (![_url isNotNull]) return nil;
+
+ /* select folder */
+
+ uid = [self imap4FolderNameForURL:_url removeFileName:YES];
+ if (![self selectFolder:uid])
+ return nil;
+
+ /* fetch parts */
+
+ uid = [[_url path] lastPathComponent];
+
+ result = [client fetchUids:[NSArray arrayWithObject:uid] parts:_parts];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ [self errorWithFormat:@"could not fetch url: %@", _url];
+ return nil;
+ }
+ //[self logWithFormat:@"RESULT: %@", result];
+ return (id)result;
+}
+
+- (NSData *)fetchContentOfBodyPart:(NSString *)_partId atURL:(NSURL *)_url {
+ NSString *key;
+ NSArray *parts;
+ id result, fetch, body;
+
+ if (_partId == nil) return nil;
+
+ key = [@"body[" stringByAppendingString:_partId];
+ key = [key stringByAppendingString:@"]"];
+ parts = [NSArray arrayWithObjects:&key count:1];
+
+ /* fetch */
+
+ result = [self fetchURL:_url parts:parts];
+
+ /* process results */
+
+ result = [result objectForKey:@"fetch"];
+ if ([result count] == 0) { /* did not find part */
+ [self errorWithFormat:@"did not find part: %@", _partId];
+ return nil;
+ }
+
+ fetch = [result objectAtIndex:0];
+ if ((body = [fetch objectForKey:@"body"]) == nil) {
+ [self errorWithFormat:@"did not find body in response: %@", result];
+ return nil;
+ }
+
+ if ((result = [body objectForKey:@"data"]) == nil) {
+ [self errorWithFormat:@"did not find data in body: %@", fetch];
+ return nil;
+ }
+ return result;
+}
+
+/* message flags */
+
+- (NSException *)addOrRemove:(BOOL)_flag flags:(id)_f toURL:(NSURL *)_url {
+ id result;
+
+ if (![_url isNotNull]) return nil;
+ if (![_f isNotNull]) return nil;
+
+ if (![_f isKindOfClass:[NSArray class]])
+ _f = [NSArray arrayWithObjects:&_f count:1];
+
+ /* select folder */
+
+ if (![self selectFolder:
+ [self imap4FolderNameForURL:_url removeFileName:YES]]) {
+ return [NSException exceptionWithHTTPStatus:404 /* server error */
+ reason:@"could not select IMAP4 folder"];
+ }
+
+ /* store flags */
+
+ result = [[self client] storeUid:[[[_url path] lastPathComponent] intValue]
+ add:[NSNumber numberWithBool:_flag]
+ flags:_f];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ unsigned int status;
+ NSString *r;
+
+ r = [result valueForKey:@"reason"];
+ if ([r isEqualToString:@"Permission denied"]) {
+ /* different for each server?, no error codes in IMAP4 ... */
+ status = 403 /* Forbidden */;
+ }
+ else
+ status = 500 /* internal server error */;
+
+ [self logWithFormat:@"DEBUG: fail result %@", result];
+
+ r = [@"Failed to add flag to IMAP4 message: " stringByAppendingString:r];
+
+ return [NSException exceptionWithHTTPStatus:status /* server error */
+ reason:r];
+ }
+ /* result contains 'fetch' key with the current flags */
+ return nil;
+}
+- (NSException *)addFlags:(id)_f toURL:(NSURL *)_u {
+ return [self addOrRemove:YES flags:_f toURL:_u];
+}
+- (NSException *)removeFlags:(id)_f toURL:(NSURL *)_u {
+ return [self addOrRemove:NO flags:_f toURL:_u];
+}
+
+- (NSException *)markURLDeleted:(NSURL *)_url {
+ return [self addOrRemove:YES flags:@"Deleted" toURL:_url];
+}
+
+- (NSException *)addFlags:(id)_f toAllMessagesInURL:(NSURL *)_url {
+ id result;
+
+ if (![_url isNotNull]) return nil;
+ if (![_f isNotNull]) return nil;
+
+ if (![_f isKindOfClass:[NSArray class]])
+ _f = [NSArray arrayWithObjects:&_f count:1];
+
+ /* select folder */
+
+ if (![self selectFolder:[self imap4FolderNameForURL:_url]]) {
+ return [NSException exceptionWithHTTPStatus:404 /* not found */
+ reason:@"could not select IMAP4 folder"];
+ }
+
+ /* fetch all sequence numbers */
+
+ result = [client searchWithQualifier:nil /* means: ALL */];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ return [NSException exceptionWithHTTPStatus:500 /* server error */
+ reason:@"could not search in IMAP4 folder"];
+ }
+
+ result = [result valueForKey:@"search"];
+ if ([result count] == 0) /* no messages in there, nothin' to be done */
+ return nil;
+
+ /* store flags */
+
+ result = [[self client] storeFlags:_f forMSNs:result addOrRemove:YES];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ return [NSException exceptionWithHTTPStatus:500 /* server error */
+ reason:@"could not change flags in IMAP4 folder"];
+ }
+
+ return nil;
+}
+
+/* posting new data */
+
+- (NSException *)postData:(NSData *)_data flags:(id)_f
+ toFolderURL:(NSURL *)_url
+{
+ id result;
+
+ if (![_url isNotNull]) return nil;
+ if (![_f isNotNull]) _f = [NSArray array];
+
+ if (![_f isKindOfClass:[NSArray class]])
+ _f = [NSArray arrayWithObjects:&_f count:1];
+
+ result = [[self client] append:_data
+ toFolder:[self imap4FolderNameForURL:_url]
+ withFlags:_f];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ [self logWithFormat:@"DEBUG: fail result %@", result];
+ return [NSException exceptionWithHTTPStatus:500 /* server error */
+ reason:@"failed to store message to IMAP4 message"];
+ }
+ /* result contains 'fetch' key with the current flags */
+
+ // TODO: need to flush any caches?
+ return nil;
+}
+
+/* operations */
+
+- (NSException *)expungeAtURL:(NSURL *)_url {
+ NSString *p;
+ id result;
+
+ /* select folder */
+
+ p = [self imap4FolderNameForURL:_url removeFileName:NO];
+ if (![self selectFolder:p]) {
+ return [NSException exceptionWithHTTPStatus:501 /* Server Error */
+ reason:@"could not select IMAP4 folder!"];
+ }
+
+ /* expunge */
+
+ result = [[self client] expunge];
+
+ if (![[result valueForKey:@"result"] boolValue]) {
+ [self errorWithFormat:@"could not expunge url: %@", _url];
+ return nil;
+ }
+ //[self logWithFormat:@"RESULT: %@", result];
+ return nil;
+}
+
+/* copying and moving */
+
+- (NSException *)copyMailURL:(NSURL *)_srcurl toFolderURL:(NSURL *)_desturl {
+ NSString *srcname, *destname;
+ unsigned uid;
+ id result;
+
+ /* names */
+
+ srcname = [self imap4FolderNameForURL:_srcurl removeFileName:YES];
+ uid = [[[_srcurl path] lastPathComponent] unsignedIntValue];
+ destname = [self imap4FolderNameForURL:_desturl];
+
+ /* select source folder */
+
+ if (![self selectFolder:srcname]) {
+ return [NSException exceptionWithHTTPStatus:404 /* Not Found */
+ reason:@"did not find source folder"];
+ }
+
+ /* copy */
+
+ result = [[self client] copyUid:uid toFolder:destname];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ return [NSException exceptionWithHTTPStatus:500 /* Server Error */
+ reason:@"copy operation failed"];
+ }
+
+ // TODO: need to flush some caches?
+
+ return nil;
+}
+
+/* managing folders */
+
+- (BOOL)doesMailboxExistAtURL:(NSURL *)_url {
+ NSString *folderName;
+ id result;
+
+ /* check in hierarchy cache */
+
+ if ((result = [self cachedHierarchyResults]) != nil) {
+ result = [result objectForKey:@"list"];
+ return ([result objectForKey:[_url path]] != nil) ? YES : NO;
+ }
+
+ /* check using IMAP4 select */
+ // TODO: we should probably just fetch the whole hierarchy?
+
+ folderName = [self imap4FolderNameForURL:_url];
+ result = [[self client] select:folderName];
+ if (![[result valueForKey:@"result"] boolValue])
+ return NO;
+
+ return YES;
+}
+
+- (id)infoForMailboxAtURL:(NSURL *)_url {
+ SOGoMailboxInfo *info;
+ NSString *folderName;
+ id result;
+
+ folderName = [self imap4FolderNameForURL:_url];
+ result = [[self client] select:folderName];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ return [NSException exceptionWithHTTPStatus:404 /* Not Found */
+ reason:@"did not find IMAP4 folder (select failed)"];
+ }
+
+ info = [[SOGoMailboxInfo alloc] initWithURL:_url folderName:folderName
+ selectDictionary:result];
+ return [info autorelease];
+}
+
+- (NSException *)createMailbox:(NSString *)_mailbox atURL:(NSURL *)_url {
+ NSString *newPath;
+ id result;
+
+ /* construct path */
+
+ newPath = [self imap4FolderNameForURL:_url];
+ newPath = [newPath stringByAppendingString:[self imap4Separator]];
+ newPath = [newPath stringByAppendingString:_mailbox];
+
+ /* create */
+
+ result = [[self client] create:newPath];
+ if ([self isPermissionDeniedResult:result]) {
+ return [NSException exceptionWithHTTPStatus:403 /* forbidden */
+ reason:@"creation of folders not allowed"];
+ }
+ else if ([[result valueForKey:@"result"] intValue] == 0) {
+ return [NSException exceptionWithHTTPStatus:500 /* server error */
+ reason:[result valueForKey:@"reason"]];
+ }
+
+ [self flushFolderHierarchyCache];
+ // [self debugWithFormat:@"created mailbox: %@: %@", newPath, result];
+ return nil;
+}
+
+- (NSException *)deleteMailboxAtURL:(NSURL *)_url {
+ NSString *path;
+ id result;
+
+ /* delete */
+
+ path = [self imap4FolderNameForURL:_url];
+ result = [[self client] delete:path];
+
+ if ([self isPermissionDeniedResult:result]) {
+ return [NSException exceptionWithHTTPStatus:403 /* forbidden */
+ reason:@"creation of folders not allowed"];
+ }
+ else if ([[result valueForKey:@"result"] intValue] == 0) {
+ return [NSException exceptionWithHTTPStatus:500 /* server error */
+ reason:[result valueForKey:@"reason"]];
+ }
+
+ [self flushFolderHierarchyCache];
+#if 0
+ [self debugWithFormat:@"delete mailbox %@: %@", _url, result];
+#endif
+ return nil;
+}
+
+- (NSException *)moveMailboxAtURL:(NSURL *)_srcurl toURL:(NSURL *)_desturl {
+ NSString *srcname, *destname;
+ id result;
+
+ /* rename */
+
+ srcname = [self imap4FolderNameForURL:_srcurl];
+ destname = [self imap4FolderNameForURL:_desturl];
+
+ result = [[self client] rename:srcname to:destname];
+
+ if ([self isPermissionDeniedResult:result]) {
+ return [NSException exceptionWithHTTPStatus:403 /* forbidden */
+ reason:@"creation of folders not allowed"];
+ }
+ else if ([[result valueForKey:@"result"] intValue] == 0) {
+ return [NSException exceptionWithHTTPStatus:500 /* server error */
+ reason:[result valueForKey:@"reason"]];
+ }
+
+ [self flushFolderHierarchyCache];
+#if 0
+ [self debugWithFormat:@"renamed mailbox %@: %@", _srcurl, result];
+#endif
+ return nil;
+}
+
+/* ACLs */
+
+- (NSDictionary *)aclForMailboxAtURL:(NSURL *)_url {
+ /*
+ Returns a mapping of uid => permission strings, eg:
+ guizmo.g = lrs;
+ root = lrswipcda;
+ */
+ NSString *folderName;
+ id result;
+
+ folderName = [self imap4FolderNameForURL:_url];
+ result = [[self client] getACL:folderName];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ [self logWithFormat:@"ERROR: getacl failed: %@", result];
+ return [NSException exceptionWithHTTPStatus:404 /* Not Found */
+ reason:@"did not find ACL for IMAP4 folder"];
+ }
+
+ return [result valueForKey:@"acl"];
+}
+
+- (NSString *)myRightsForMailboxAtURL:(NSURL *)_url {
+ NSString *folderName;
+ id result;
+
+ /* check cache */
+
+ if ((result = [self cachedMyRightsForURL:_url]) != nil)
+ return result;
+
+ /* run IMAP4 op */
+
+ folderName = [self imap4FolderNameForURL:_url];
+ result = [[self client] myRights:folderName];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ [self logWithFormat:@"ERROR: myrights failed: %@", result];
+ return [NSException exceptionWithHTTPStatus:404 /* Not Found */
+ reason:@"did not find myrights for IMAP4 folder"];
+ }
+
+ /* cache results */
+
+ if ((result = [result valueForKey:@"myrights"]) != nil)
+ [self cacheMyRights:result forURL:_url];
+ return result;
+}
+