2 Copyright (C) 2004 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
6 OGo is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with OGo; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
22 #include "SOGoMailManager.h"
25 @interface SOGoMailConnectionEntry : NSObject
28 NGImap4Client *client;
31 NSDictionary *subfolders;
34 - (id)initWithClient:(NGImap4Client *)_client password:(NSString *)_pwd;
38 - (NGImap4Client *)client;
39 - (BOOL)isValidPassword:(NSString *)_pwd;
41 - (NSDate *)creationTime;
43 - (void)cacheHierarchyResults:(NSDictionary *)_hierarchy;
44 - (NSDictionary *)cachedHierarchyResults;
48 @implementation SOGoMailManager
50 static BOOL debugOn = YES;
51 static BOOL poolingOff = NO;
52 static NSTimeInterval PoolScanInterval = 5 * 60;
55 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
57 debugOn = [ud boolForKey:@"SOGoEnableIMAP4Debug"];
58 poolingOff = [ud boolForKey:@"SOGoDisableIMAP4Pooling"];
61 + (id)defaultMailManager {
62 static SOGoMailManager *manager = nil; // THREAD
64 manager = [[self alloc] init];
69 if ((self = [super init])) {
71 self->urlToEntry = [[NSMutableDictionary alloc] initWithCapacity:256];
74 self->gcTimer = [[NSTimer scheduledTimerWithTimeInterval:
76 target:self selector:@selector(_garbageCollect:)
77 userInfo:nil repeats:YES] retain];
83 if (self->gcTimer) [self->gcTimer invalidate];
84 [self->gcTimer release];
86 [self->urlToEntry release];
92 - (id)cacheKeyForURL:(NSURL *)_url {
93 // protocol, user, host, port
94 return [NSString stringWithFormat:@"%@://%@@%@:%@",
95 [_url scheme], [_url user], [_url host], [_url port]];
98 - (SOGoMailConnectionEntry *)entryForURL:(NSURL *)_url {
102 return [self->urlToEntry objectForKey:[self cacheKeyForURL:_url]];
104 - (void)cacheEntry:(SOGoMailConnectionEntry *)_entry forURL:(NSURL *)_url {
105 if (_entry == nil) _entry = (id)[NSNull null];
106 [self->urlToEntry setObject:_entry forKey:[self cacheKeyForURL:_url]];
109 - (void)_garbageCollect:(NSTimer *)_timer {
110 // TODO: scan for old IMAP4 channels
111 [self debugWithFormat:@"should collect IMAP4 channels (%d active)",
112 [self->urlToEntry count]];
117 - (NGImap4Client *)imap4ClientForURL:(NSURL *)_url password:(NSString *)_pwd {
118 // TODO: move to some global IMAP4 connection pool manager
119 SOGoMailConnectionEntry *entry;
120 NGImap4Client *client;
121 NSDictionary *result;
126 /* check connection pool */
128 if ((entry = [self entryForURL:_url]) != nil) {
129 if ([entry isValidPassword:_pwd]) {
130 [self debugWithFormat:@"reused IMAP4 connection for URL: %@", _url];
131 return [entry client];
134 /* different password, password could have changed! */
138 /* setup connection and attempt login */
140 if ((client = [NGImap4Client clientWithURL:_url]) == nil)
143 result = [client login:[_url user] password:_pwd];
144 if (![[result valueForKey:@"result"] boolValue]) {
145 [self logWithFormat:@"ERROR: IMAP4 login failed!"];
149 [self debugWithFormat:@"created new IMAP4 connection for URL: %@", _url];
151 /* cache connection in pool */
153 entry = [[SOGoMailConnectionEntry alloc] initWithClient:client
155 [self cacheEntry:entry forURL:_url];
156 [entry release]; entry = nil;
161 /* folder hierarchy */
163 - (NSArray *)_getDirectChildren:(NSArray *)_array folderName:(NSString *)_fn {
165 unsigned i, count, prefixlen;
167 if ((count = [_array count]) < 2)
168 /* one entry is the folder itself, so we need at least two */
169 return [NSArray array];
171 prefixlen = [_fn length] + 1;
172 ma = [NSMutableArray arrayWithCapacity:count];
173 for (i = 0; i < count; i++) {
176 p = [_array objectAtIndex:i];
177 if ([p length] <= prefixlen)
179 if (![p hasPrefix:_fn])
182 /* cut of common part */
183 p = [p substringFromIndex:prefixlen];
185 /* check whether the path is a sub-subfolder path */
186 if ([p rangeOfString:@"/"].length > 0)
192 [ma sortUsingSelector:@selector(compare:)];
196 - (NSString *)imap4Separator {
200 - (NSString *)imap4FolderNameForURL:(NSURL *)_url {
201 /* a bit hackish, but should be OK */
202 NSString *folderName;
207 folderName = [_url path];
208 if ([folderName length] == 0)
210 if ([folderName characterAtIndex:0] == '/')
211 folderName = [folderName substringFromIndex:1];
213 return [[folderName pathComponents] componentsJoinedByString:
214 [self imap4Separator]];
217 - (NSArray *)extractSubfoldersForURL:(NSURL *)_url
218 fromResultSet:(NSDictionary *)_result
220 NSString *folderName;
221 NSDictionary *result;
225 /* Note: the result is normalized, that is, it contains / as the separator */
226 folderName = [_url path];
227 if ([folderName hasPrefix:@"/"])
228 folderName = [folderName substringFromIndex:1];
230 result = [_result valueForKey:@"list"];
232 /* Cyrus already tells us whether we need to check for children */
233 flags = [result objectForKey:folderName];
234 if ([flags containsObject:@"hasnochildren"])
237 names = [self _getDirectChildren:[result allKeys] folderName:folderName];
239 [self debugWithFormat:@"subfolders of %@: %@", folderName,
240 [names componentsJoinedByString:@","]];
245 - (NSArray *)subfoldersForURL:(NSURL *)_url password:(NSString *)_pwd {
247 SOGoMailConnectionEntry *entry;
248 NGImap4Client *client;
249 NSDictionary *result;
253 if ((entry = [self entryForURL:_url]) != nil) {
254 if ([entry isValidPassword:_pwd]) {
255 NSDictionary *allFolders;
257 [self debugWithFormat:@"valid password, reusing folder cache .."];
258 if ((allFolders = [entry cachedHierarchyResults]) != nil)
259 return [self extractSubfoldersForURL:_url fromResultSet:allFolders];
261 [self debugWithFormat:@" no folders cached yet .."];
264 /* different password, password could have changed! */
268 [self debugWithFormat:@"no connection cached yet for url: %@", _url];
272 client = [entry isValidPassword:_pwd]
274 : [self imap4ClientForURL:_url password:_pwd];
278 /* fetch _all_ folders */
280 result = [client list:@"INBOX" pattern:@"*"];
281 if (![[result valueForKey:@"result"] boolValue]) {
282 [self logWithFormat:@"ERROR: listing of folder failed!"];
288 if ([result isNotNull]) {
289 if (entry == nil) /* required in case the entry was not setup */
290 entry = [self entryForURL:_url];
292 [entry cacheHierarchyResults:result];
293 [self debugWithFormat:@"cached results in entry %@: %@", entry, result];
298 return [self extractSubfoldersForURL:_url fromResultSet:result];
303 - (BOOL)isDebuggingEnabled {
307 @end /* SOGoMailManager */
309 @implementation SOGoMailConnectionEntry
311 - (id)initWithClient:(NGImap4Client *)_client password:(NSString *)_pwd {
312 if (_client == nil || _pwd == nil) {
317 if ((self = [super init])) {
318 self->client = [_client retain];
319 self->password = [_pwd copy];
321 self->creationTime = [[NSDate alloc] init];
326 return [self initWithClient:nil password:nil];
330 [self->creationTime release];
331 [self->subfolders release];
332 [self->password release];
333 [self->client release];
339 - (NGImap4Client *)client {
342 - (BOOL)isValidPassword:(NSString *)_pwd {
343 return [self->password isEqualToString:_pwd];
346 - (NSDate *)creationTime {
347 return self->creationTime;
350 - (void)cacheHierarchyResults:(NSDictionary *)_hierarchy {
351 ASSIGNCOPY(self->subfolders, _hierarchy);
353 - (NSDictionary *)cachedHierarchyResults {
354 return self->subfolders;
357 @end /* SOGoMailConnectionEntry */