--- /dev/null
+/*
+ Copyright (C) 2004-2005 SKYRIX Software AG
+
+ This file is part of OpenGroupware.org.
+
+ OGo is free software; you can redistribute it and/or modify it under
+ the terms of the GNU Lesser General Public License as published by the
+ Free Software Foundation; either version 2, or (at your option) any
+ later version.
+
+ OGo 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 Lesser General Public
+ License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with OGo; see the file COPYING. If not, write to the
+ Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
+ 02111-1307, USA.
+*/
+
+#include "NGImap4Connection.h"
+#include "NGImap4MailboxInfo.h"
+#include "NGImap4Client.h"
+#include "imCommon.h"
+
+@implementation NGImap4Connection
+
+static BOOL debugOn = NO;
+static BOOL debugCache = NO;
+static BOOL debugKeys = NO;
+static BOOL alwaysSelect = NO;
+static BOOL onlyFetchInbox = NO;
+static NSString *imap4Separator = nil;
+
++ (void)initialize {
+ NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
+
+ debugOn = [ud boolForKey:@"SOGoEnableIMAP4Debug"];
+ debugCache = [ud boolForKey:@"SOGoEnableIMAP4CacheDebug"];
+ alwaysSelect = [ud boolForKey:@"SOGoAlwaysSelectIMAP4Folder"];
+ if (debugOn) NSLog(@"Note: SOGoEnableIMAP4Debug is enabled!");
+ if (alwaysSelect)
+ NSLog(@"WARNING: 'SOGoAlwaysSelectIMAP4Folder' enabled (slow down)");
+
+ imap4Separator = [[ud stringForKey:@"SOGoIMAP4StringSeparator"] copy];
+ if ([imap4Separator length] == 0)
+ imap4Separator = @"/";
+ NSLog(@"Note(SOGoMailManager): using '%@' as the IMAP4 folder separator.",
+ imap4Separator);
+}
+
+- (id)initWithClient:(NGImap4Client *)_client password:(NSString *)_pwd {
+ if (_client == nil || _pwd == nil) {
+ [self release];
+ return nil;
+ }
+
+ if ((self = [super init])) {
+ self->client = [_client retain];
+ self->password = [_pwd copy];
+
+ self->creationTime = [[NSDate alloc] init];
+ }
+ return self;
+}
+- (id)init {
+ return [self initWithClient:nil password:nil];
+}
+
+- (void)dealloc {
+ [self->urlToRights release];
+ [self->cachedUIDs release];
+ [self->uidFolderURL release];
+ [self->uidSortOrdering release];
+ [self->creationTime release];
+ [self->subfolders release];
+ [self->password release];
+ [self->client release];
+ [super dealloc];
+}
+
+/* accessors */
+
+- (NGImap4Client *)client {
+ return self->client;
+}
+- (BOOL)isValidPassword:(NSString *)_pwd {
+ return [self->password isEqualToString:_pwd];
+}
+
+- (NSDate *)creationTime {
+ return self->creationTime;
+}
+
+- (void)cacheHierarchyResults:(NSDictionary *)_hierarchy {
+ ASSIGNCOPY(self->subfolders, _hierarchy);
+}
+- (NSDictionary *)cachedHierarchyResults {
+ return self->subfolders;
+}
+- (void)flushFolderHierarchyCache {
+ [self->subfolders release]; self->subfolders = nil;
+ [self->urlToRights release]; self->urlToRights = nil;
+}
+
+/* rights */
+
+- (NSString *)cachedMyRightsForURL:(NSURL *)_url {
+ return (_url != nil) ? [self->urlToRights objectForKey:_url] : nil;
+}
+- (void)cacheMyRights:(NSString *)_rights forURL:(NSURL *)_url {
+ if (self->urlToRights == nil)
+ self->urlToRights = [[NSMutableDictionary alloc] initWithCapacity:8];
+ [self->urlToRights setObject:_rights forKey:_url];
+}
+
+/* UIDs */
+
+- (id)cachedUIDsForURL:(NSURL *)_url qualifier:(id)_q sortOrdering:(id)_so {
+ if (_q != nil)
+ return nil;
+ if (![_so isEqual:self->uidSortOrdering])
+ return nil;
+ if (![self->uidFolderURL isEqual:_url])
+ return nil;
+
+ return self->cachedUIDs;
+}
+
+- (void)cacheUIDs:(NSArray *)_uids forURL:(NSURL *)_url
+ qualifier:(id)_q sortOrdering:(id)_so
+{
+ if (_q != nil)
+ return;
+
+ ASSIGNCOPY(self->uidSortOrdering, _so);
+ ASSIGNCOPY(self->uidFolderURL, _url);
+ ASSIGNCOPY(self->cachedUIDs, _uids);
+}
+
+- (void)flushMailCaches {
+ ASSIGN(self->uidSortOrdering, nil);
+ ASSIGN(self->uidFolderURL, nil);
+ ASSIGN(self->cachedUIDs, nil);
+}
+
+
+/* errors */
+
+- (NSException *)errorCouldNotSelectURL:(NSURL *)_url {
+ NSDictionary *ui;
+ NSString *r;
+
+ r = [_url isNotNull]
+ ? [@"Could not select IMAP4 folder: " stringByAppendingString:
+ [_url absoluteString]]
+ : @"Could not select IMAP4 folder!";
+
+ ui = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithInt:404], @"http-status",
+ _url, @"url",
+ nil];
+
+ return [NSException exceptionWithName:@"NGImap4Exception"
+ reason:r userInfo:ui];
+}
+
+- (NSException *)errorForResult:(NSDictionary *)_result text:(NSString *)_txt {
+ NSDictionary *ui;
+ NSString *r;
+ int status;
+
+ if ([[_result valueForKey:@"result"] boolValue])
+ return nil; /* everything went fine! */
+
+ if ((r = [_result valueForKey:@"reason"]) != nil)
+ r = [[_txt stringByAppendingString:@": "] stringByAppendingString:r];
+ else
+ r = _txt;
+
+ if ([r isEqualToString:@"Permission denied"]) {
+ /* different for each server?, no error codes in IMAP4 ... */
+ status = 403 /* Forbidden */;
+ }
+ else
+ status = 500 /* internal server error */;
+
+ ui = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithInt:status], @"http-status",
+ _result, @"rawResult",
+ nil];
+
+ return [NSException exceptionWithName:@"NGImap4Exception"
+ reason:r userInfo:ui];
+}
+
+/* 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 */
+
+ result = [self imap4FolderNameForURL:_url removeFileName:YES];
+ if (![self selectFolder:result])
+ return [self errorCouldNotSelectURL:_url];
+
+ /* store flags */
+
+ result = [[self client] storeUid:[[[_url path] lastPathComponent] intValue]
+ add:[NSNumber numberWithBool:_flag]
+ flags:_f];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ return [self errorForResult:result
+ text:@"Failed to change flags of IMAP4 message"];
+ }
+ /* 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 [self errorCouldNotSelectURL:_url];
+
+ /* fetch all sequence numbers */
+
+ result = [client searchWithQualifier:nil /* means: ALL */];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ return [self errorForResult:result
+ text:@"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 [self errorForResult:result
+ text:@"Failed to change flags of IMAP4 message"];
+ }
+
+ 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])
+ return [self errorForResult:result text:@"Failed to store 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 [self errorCouldNotSelectURL:_url];
+
+ /* 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 [self errorCouldNotSelectURL:_srcurl];
+
+ /* copy */
+
+ result = [[self client] copyUid:uid toFolder:destname];
+ if (![[result valueForKey:@"result"] boolValue])
+ return [self errorForResult:result text:@"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 {
+ NGImap4MailboxInfo *info;
+ NSString *folderName;
+ id result;
+
+ folderName = [self imap4FolderNameForURL:_url];
+ result = [[self client] select:folderName];
+ if (![[result valueForKey:@"result"] boolValue])
+ return [self errorCouldNotSelectURL:_url];
+
+ info = [[NGImap4MailboxInfo 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 (![[result valueForKey:@"result"] boolValue])
+ return [self errorForResult:result text:@"Failed to create folder"];
+
+ [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 (![[result valueForKey:@"result"] boolValue])
+ return [self errorForResult:result text:@"Failed to delete folder"];
+
+ [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 (![[result valueForKey:@"result"] boolValue])
+ return [self errorForResult:result text:@"Failed to move folder"];
+
+ [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]) {
+ return (id)[self errorForResult:result
+ text:@"Failed to get ACL of 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]) {
+ return (id)[self errorForResult:result
+ text:@"Failed to get myrights on folder"];
+ }
+
+ /* cache results */
+
+ if ((result = [result valueForKey:@"myrights"]) != nil)
+ [self cacheMyRights:result forURL:_url];
+ return result;
+}
+
+@end /* NGImap4Connection */
--- /dev/null
+/*
+ Copyright (C) 2004-2005 SKYRIX Software AG
+
+ This file is part of OpenGroupware.org.
+
+ OGo is free software; you can redistribute it and/or modify it under
+ the terms of the GNU Lesser General Public License as published by the
+ Free Software Foundation; either version 2, or (at your option) any
+ later version.
+
+ OGo 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 Lesser General Public
+ License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with OGo; see the file COPYING. If not, write to the
+ Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
+ 02111-1307, USA.
+*/
+
+#include "NGImap4ConnectionManager.h"
+#include "NGImap4Connection.h"
+#include "NGImap4Client.h"
+#include "imCommon.h"
+
+@implementation NGImap4ConnectionManager
+
+static BOOL debugOn = NO;
+static BOOL debugCache = NO;
+static BOOL poolingOff = NO;
+static NSTimeInterval PoolScanInterval = 5 * 60 /* every five minutes */;
+
++ (void)initialize {
+ NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
+
+ debugOn = [ud boolForKey:@"NGImap4EnableIMAP4Debug"];
+ debugCache = [ud boolForKey:@"NGImap4EnableIMAP4CacheDebug"];
+ poolingOff = [ud boolForKey:@"NGImap4DisableIMAP4Pooling"];
+
+ if (debugOn) NSLog(@"Note: NGImap4EnableIMAP4Debug is enabled!");
+ if (poolingOff) NSLog(@"WARNING: IMAP4 connection pooling is disabled!");
+}
+
++ (id)defaultConnectionManager {
+ static NGImap4ConnectionManager *manager = nil; // THREAD
+ if (manager == nil)
+ manager = [[self alloc] init];
+ return manager;
+}
+
+- (id)init {
+ if ((self = [super init])) {
+ if (!poolingOff) {
+ self->urlToEntry = [[NSMutableDictionary alloc] initWithCapacity:256];
+ }
+
+ self->gcTimer = [[NSTimer scheduledTimerWithTimeInterval:
+ PoolScanInterval
+ target:self selector:@selector(_garbageCollect:)
+ userInfo:nil repeats:YES] retain];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ if (self->gcTimer) [self->gcTimer invalidate];
+ [self->urlToEntry release];
+ [self->gcTimer release];
+ [super dealloc];
+}
+
+/* cache */
+
+- (id)cacheKeyForURL:(NSURL *)_url {
+ // protocol, user, host, port
+ return [NSString stringWithFormat:@"%@://%@@%@:%@",
+ [_url scheme], [_url user], [_url host], [_url port]];
+}
+
+- (NGImap4Connection *)entryForURL:(NSURL *)_url {
+ if (_url == nil)
+ return nil;
+
+ return [self->urlToEntry objectForKey:[self cacheKeyForURL:_url]];
+}
+- (void)cacheEntry:(NGImap4Connection *)_entry forURL:(NSURL *)_url {
+ if (_entry == nil) _entry = (id)[NSNull null];
+ [self->urlToEntry setObject:_entry forKey:[self cacheKeyForURL:_url]];
+}
+
+- (void)_garbageCollect:(NSTimer *)_timer {
+ // TODO: scan for old IMAP4 channels
+ [self debugWithFormat:@"should collect IMAP4 channels (%d active)",
+ [self->urlToEntry count]];
+}
+
+- (NGImap4Connection *)connectionForURL:(NSURL *)_url password:(NSString *)_p {
+ /*
+ Three cases:
+ a) not yet connected => create new entry and connect
+ b) connected, correct password => return cached entry
+ c) connected, different password => try to recreate entry
+ */
+ NGImap4Connection *entry;
+ NGImap4Client *client;
+
+ /* check cache */
+
+ if ((entry = [self entryForURL:_url]) != nil) {
+ if ([entry isValidPassword:_p]) {
+ if (debugCache)
+ [self logWithFormat:@"valid password, reusing cache entry ..."];
+ return entry;
+ }
+
+ /* different password, password could have changed! */
+ if (debugCache)
+ [self logWithFormat:@"different password than cached entry: %@", _url];
+ entry = nil;
+ }
+ else
+ [self debugWithFormat:@"no connection cached yet for url: %@", _url];
+
+ /* try to login */
+
+ client = [entry isValidPassword:_p]
+ ? [entry client]
+ : [self imap4ClientForURL:_url password:_p];
+
+ if (client == nil)
+ return nil;
+
+ /* sideeffect of -imap4ClientForURL:password: is to create a cache entry */
+ return [self entryForURL:_url];
+}
+
+/* client object */
+
+- (NGImap4Client *)imap4ClientForURL:(NSURL *)_url password:(NSString *)_pwd {
+ // TODO: move to some global IMAP4 connection pool manager
+ NGImap4Connection *entry;
+ NGImap4Client *client;
+ NSDictionary *result;
+
+ if (_url == nil)
+ return nil;
+
+ /* check connection pool */
+
+ if ((entry = [self entryForURL:_url]) != nil) {
+ if ([entry isValidPassword:_pwd]) {
+ [self debugWithFormat:@"reused IMAP4 connection for URL: %@", _url];
+ return [entry client];
+ }
+
+ /* different password, password could have changed! */
+ entry = nil;
+ }
+
+ /* setup connection and attempt login */
+
+ if ((client = [NGImap4Client clientWithURL:_url]) == nil)
+ return nil;
+
+ result = [client login:[_url user] password:_pwd];
+ if (![[result valueForKey:@"result"] boolValue]) {
+ [self errorWithFormat:
+ @"IMAP4 login failed:\n"
+ @" host=%@, user=%@, pwd=%s\n"
+ @" url=%@\n base=%@\n base-class=%@)\n"
+ @" = %@",
+ [_url host], [_url user], [_pwd length] > 0 ? "yes" : "no",
+ [_url absoluteString],
+ [_url baseURL],
+ NSStringFromClass([[_url baseURL] class]),
+ client];
+ return nil;
+ }
+
+ [self debugWithFormat:@"created new IMAP4 connection for URL: %@", _url];
+
+ /* cache connection in pool */
+
+ entry = [[NGImap4Connection alloc] initWithClient:client
+ password:_pwd];
+ [self cacheEntry:entry forURL:_url];
+ [entry release]; entry = nil;
+
+ return client;
+}
+
+- (void)flushCachesForURL:(NSURL *)_url {
+ NGImap4Connection *entry;
+
+ if ((entry = [self entryForURL:_url]) == nil) /* nothing cached */
+ return;
+
+ [entry flushFolderHierarchyCache];
+ [entry flushMailCaches];
+}
+
+/* debugging */
+
+- (BOOL)isDebuggingEnabled {
+ return debugOn;
+}
+
+@end /* NGImap4ConnectionManager */