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"
23 #include "SOGoMailConnectionEntry.h"
26 // TODO: need a way to refresh caches on get mail!
28 @implementation SOGoMailManager
30 static BOOL debugOn = NO;
31 static BOOL debugCache = NO;
32 static BOOL debugKeys = NO;
33 static BOOL poolingOff = NO;
34 static NSTimeInterval PoolScanInterval = 5 * 60;
35 static NSString *imap4Separator = nil;
38 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
40 debugOn = [ud boolForKey:@"SOGoEnableIMAP4Debug"];
41 debugCache = [ud boolForKey:@"SOGoEnableIMAP4CacheDebug"];
42 poolingOff = [ud boolForKey:@"SOGoDisableIMAP4Pooling"];
44 if (debugOn) NSLog(@"Note: SOGoEnableIMAP4Debug is enabled!");
45 if (poolingOff) NSLog(@"WARNING: IMAP4 connection pooling is disabled!");
47 imap4Separator = [[ud stringForKey:@"SOGoIMAP4StringSeparator"] copy];
48 if ([imap4Separator length] == 0)
49 imap4Separator = @"/";
50 NSLog(@"Note(SOGoMailManager): using '%@' as the IMAP4 folder separator.",
54 + (id)defaultMailManager {
55 static SOGoMailManager *manager = nil; // THREAD
57 manager = [[self alloc] init];
62 if ((self = [super init])) {
64 self->urlToEntry = [[NSMutableDictionary alloc] initWithCapacity:256];
67 self->gcTimer = [[NSTimer scheduledTimerWithTimeInterval:
69 target:self selector:@selector(_garbageCollect:)
70 userInfo:nil repeats:YES] retain];
76 if (self->gcTimer) [self->gcTimer invalidate];
77 [self->gcTimer release];
79 [self->urlToEntry release];
85 - (id)cacheKeyForURL:(NSURL *)_url {
86 // protocol, user, host, port
87 return [NSString stringWithFormat:@"%@://%@@%@:%@",
88 [_url scheme], [_url user], [_url host], [_url port]];
91 - (SOGoMailConnectionEntry *)entryForURL:(NSURL *)_url {
95 return [self->urlToEntry objectForKey:[self cacheKeyForURL:_url]];
97 - (void)cacheEntry:(SOGoMailConnectionEntry *)_entry forURL:(NSURL *)_url {
98 if (_entry == nil) _entry = (id)[NSNull null];
99 [self->urlToEntry setObject:_entry forKey:[self cacheKeyForURL:_url]];
102 - (void)_garbageCollect:(NSTimer *)_timer {
103 // TODO: scan for old IMAP4 channels
104 [self debugWithFormat:@"should collect IMAP4 channels (%d active)",
105 [self->urlToEntry count]];
108 - (id)entryForURL:(NSURL *)_url password:(NSString *)_pwd {
111 a) not yet connected => create new entry and connect
112 b) connected, correct password => return cached entry
113 c) connected, different password => try to recreate entry
115 SOGoMailConnectionEntry *entry;
116 NGImap4Client *client;
120 if ((entry = [self entryForURL:_url]) != nil) {
121 if ([entry isValidPassword:_pwd]) {
123 [self logWithFormat:@"valid password, reusing cache entry ..."];
127 /* different password, password could have changed! */
129 [self logWithFormat:@"different password than cached entry: %@", _url];
133 [self debugWithFormat:@"no connection cached yet for url: %@", _url];
137 client = [entry isValidPassword:_pwd]
139 : [self imap4ClientForURL:_url password:_pwd];
144 /* sideeffect of -imap4ClientForURL:password: is to create a cache entry */
145 return [self entryForURL:_url];
150 - (NGImap4Client *)imap4ClientForURL:(NSURL *)_url password:(NSString *)_pwd {
151 // TODO: move to some global IMAP4 connection pool manager
152 SOGoMailConnectionEntry *entry;
153 NGImap4Client *client;
154 NSDictionary *result;
159 /* check connection pool */
161 if ((entry = [self entryForURL:_url]) != nil) {
162 if ([entry isValidPassword:_pwd]) {
163 [self debugWithFormat:@"reused IMAP4 connection for URL: %@", _url];
164 return [entry client];
167 /* different password, password could have changed! */
171 /* setup connection and attempt login */
173 if ((client = [NGImap4Client clientWithURL:_url]) == nil)
176 result = [client login:[_url user] password:_pwd];
177 if (![[result valueForKey:@"result"] boolValue]) {
178 [self errorWithFormat:
179 @"IMAP4 login failed (host=%@,user=%@,pwd=%s,url=%@/%@/%@): %@",
180 [_url host], [_url user], [_pwd length] > 0 ? "yes" : "no",
181 [_url absoluteString], [_url baseURL],
182 NSStringFromClass([[_url baseURL] class]),
187 [self debugWithFormat:@"created new IMAP4 connection for URL: %@", _url];
189 /* cache connection in pool */
191 entry = [[SOGoMailConnectionEntry alloc] initWithClient:client
193 [self cacheEntry:entry forURL:_url];
194 [entry release]; entry = nil;
199 - (void)flushCachesForURL:(NSURL *)_url {
200 SOGoMailConnectionEntry *entry;
202 if ((entry = [self entryForURL:_url]) == nil) /* nothing cached */
205 [entry flushFolderHierarchyCache];
206 [entry flushMailCaches];
209 /* folder hierarchy */
211 - (NSArray *)_getDirectChildren:(NSArray *)_array folderName:(NSString *)_fn {
213 Scans string '_array' for strings which start with the string in '_fn'.
217 unsigned i, count, prefixlen;
219 if ((count = [_array count]) < 2)
220 /* one entry is the folder itself, so we need at least two */
221 return [NSArray array];
223 prefixlen = [_fn length] + 1;
224 ma = [NSMutableArray arrayWithCapacity:count];
225 for (i = 0; i < count; i++) {
228 p = [_array objectAtIndex:i];
229 if ([p length] <= prefixlen)
231 if (![p hasPrefix:_fn])
234 /* cut of common part */
235 p = [p substringFromIndex:prefixlen];
237 /* check whether the path is a sub-subfolder path */
238 if ([p rangeOfString:@"/"].length > 0)
244 [ma sortUsingSelector:@selector(compare:)];
248 - (NSString *)imap4Separator {
249 return imap4Separator;
252 - (NSString *)imap4FolderNameForURL:(NSURL *)_url removeFileName:(BOOL)_delfn {
253 /* a bit hackish, but should be OK */
254 NSString *folderName;
260 folderName = [_url path];
261 if ([folderName length] == 0)
263 if ([folderName characterAtIndex:0] == '/')
264 folderName = [folderName substringFromIndex:1];
266 if (_delfn) folderName = [folderName stringByDeletingLastPathComponent];
268 if ([[self imap4Separator] isEqualToString:@"/"])
271 names = [folderName pathComponents];
272 return [names componentsJoinedByString:[self imap4Separator]];
274 - (NSString *)imap4FolderNameForURL:(NSURL *)_url {
275 return [self imap4FolderNameForURL:_url removeFileName:NO];
278 - (NSArray *)extractSubfoldersForURL:(NSURL *)_url
279 fromResultSet:(NSDictionary *)_result
281 NSString *folderName;
282 NSDictionary *result;
286 /* Note: the result is normalized, that is, it contains / as the separator */
287 folderName = [_url path];
289 /* normalized results already have the / in front on libFoundation?! */
290 if ([folderName hasPrefix:@"/"])
291 folderName = [folderName substringFromIndex:1];
294 result = [_result valueForKey:@"list"];
296 /* Cyrus already tells us whether we need to check for children */
297 flags = [result objectForKey:folderName];
298 if ([flags containsObject:@"hasnochildren"]) {
300 [self logWithFormat:@"folder %@ has no children.", folderName];
305 [self logWithFormat:@"all keys %@: %@", folderName, [result allKeys]];
307 names = [self _getDirectChildren:[result allKeys] folderName:folderName];
309 [self debugWithFormat:@"subfolders of %@: %@", folderName,
310 [names componentsJoinedByString:@","]];
315 - (NSArray *)extractFoldersFromResultSet:(NSDictionary *)_result {
316 /* Note: the result is normalized, that is, it contains / as the separator */
317 return [[_result valueForKey:@"list"] allKeys];
320 - (NSArray *)subfoldersForURL:(NSURL *)_url password:(NSString *)_pwd {
321 SOGoMailConnectionEntry *entry;
322 NSDictionary *result;
325 [self debugWithFormat:@"subfolders for URL: %@ ...",[_url absoluteString]];
327 /* check connection cache */
329 if ((entry = [self entryForURL:_url password:_pwd]) == nil)
332 /* check hierarchy cache */
334 if ((result = [entry cachedHierarchyResults]) != nil)
335 return [self extractSubfoldersForURL:_url fromResultSet:result];
337 [self debugWithFormat:@" no folders cached yet .."];
339 /* fetch _all_ folders */
341 result = [[entry client] list:@"INBOX" pattern:@"*"];
342 if (![[result valueForKey:@"result"] boolValue]) {
343 [self errorWithFormat:@"listing of folder failed!"];
349 if ([result isNotNull]) {
350 if (entry == nil) /* required in case the entry was not setup */
351 entry = [self entryForURL:_url];
353 [entry cacheHierarchyResults:result];
355 [self logWithFormat:@"cached results in entry %@: 0x%08X(%d)",
356 entry, result, [result count]];
362 return [self extractSubfoldersForURL:_url fromResultSet:result];
365 - (NSArray *)allFoldersForURL:(NSURL *)_url password:(NSString *)_pwd {
366 SOGoMailConnectionEntry *entry;
367 NSDictionary *result;
370 [self debugWithFormat:@"folders for URL: %@ ...",[_url absoluteString]];
372 /* check connection cache */
374 if ((entry = [self entryForURL:_url password:_pwd]) == nil)
377 /* check hierarchy cache */
379 if ((result = [entry cachedHierarchyResults]) != nil)
380 return [self extractFoldersFromResultSet:result];
382 [self debugWithFormat:@" no folders cached yet .."];
384 /* fetch _all_ folders */
386 result = [[entry client] list:@"INBOX" pattern:@"*"];
387 if (![[result valueForKey:@"result"] boolValue]) {
388 [self logWithFormat:@"ERROR: listing of folder failed!"];
394 if ([result isNotNull]) {
395 if (entry == nil) /* required in case the entry was not setup */
396 entry = [self entryForURL:_url];
398 [entry cacheHierarchyResults:result];
400 [self logWithFormat:@"cached results in entry %@: 0x%08X(%d)",
401 entry, result, [result count]];
406 return [self extractFoldersFromResultSet:result];
411 - (NSArray *)fetchUIDsInURL:(NSURL *)_url qualifier:(id)_qualifier
412 sortOrdering:(id)_so password:(NSString *)_pwd
415 sortOrdering can be an NSString, an EOSortOrdering or an array of EOS.
417 SOGoMailConnectionEntry *entry;
418 NSDictionary *result;
421 /* check connection cache */
423 if ((entry = [self entryForURL:_url password:_pwd]) == nil)
428 uids = [entry cachedUIDsForURL:_url qualifier:_qualifier sortOrdering:_so];
430 if (debugCache) [self logWithFormat:@"reusing uid cache!"];
431 return [uids isNotNull] ? uids : nil;
434 /* select folder and fetch */
436 result = [[entry client] select:[self imap4FolderNameForURL:_url]];
437 if (![[result valueForKey:@"result"] boolValue]) {
438 [self errorWithFormat:@"could not select URL: %@: %@", _url, result];
442 result = [[entry client] sort:_so qualifier:_qualifier encoding:@"UTF-8"];
443 if (![[result valueForKey:@"result"] boolValue]) {
444 [self errorWithFormat:@"could not sort contents of URL: %@", _url];
448 uids = [result valueForKey:@"sort"];
449 if (![uids isNotNull]) {
450 [self errorWithFormat:@"got no UIDs for URL: %@: %@", _url, result];
456 [entry cacheUIDs:uids forURL:_url qualifier:_qualifier sortOrdering:_so];
460 - (NSArray *)fetchUIDs:(NSArray *)_uids inURL:(NSURL *)_url
461 parts:(NSArray *)_parts password:(NSString *)_pwd
463 // currently returns a dict?!
467 BODY.PEEK[<section>]<<partial>>
468 BODY [this is the bodystructure, supported]
469 BODYSTRUCTURE [not supported yet!]
470 ENVELOPE [this is a parsed header, but does not include type]
478 NGImap4Client *client;
479 NSDictionary *result;
483 if ([_uids count] == 0)
484 return nil; // TODO: might break empty folders?! return a dict!
486 if ((client = [self imap4ClientForURL:_url password:_pwd]) == nil)
491 result = [client select:[self imap4FolderNameForURL:_url]];
492 if (![[result valueForKey:@"result"] boolValue]) {
493 [self errorWithFormat:@"could not select URL: %@: %@", _url, result];
499 // TODO: split uids into batches, otherwise Cyrus will complain
500 // => not really important because we batch before (in the sort)
501 // if the list is too long, we get a:
502 // "* BYE Fatal error: word too long"
504 result = [client fetchUids:_uids parts:_parts];
505 if (![[result valueForKey:@"result"] boolValue]) {
506 [self errorWithFormat:@"could not fetch %d uids for url: %@",
511 //[self logWithFormat:@"RESULT: %@", result];
515 - (id)fetchURL:(NSURL *)_url parts:(NSArray *)_parts password:(NSString *)_pwd{
516 // currently returns a dict
517 NGImap4Client *client;
518 NSDictionary *result;
521 if (![_url isNotNull]) return nil;
523 if ((client = [self imap4ClientForURL:_url password:_pwd]) == nil)
528 result = [client select:[self imap4FolderNameForURL:_url
529 removeFileName:YES]];
530 if (![[result valueForKey:@"result"] boolValue]) {
531 [self errorWithFormat:@"could not select URL: %@: %@", _url, result];
537 uid = [[_url path] lastPathComponent];
539 result = [client fetchUids:[NSArray arrayWithObject:uid] parts:_parts];
540 if (![[result valueForKey:@"result"] boolValue]) {
541 [self errorWithFormat:@"could not fetch url: %@", _url];
544 //[self logWithFormat:@"RESULT: %@", result];
548 - (NSData *)fetchContentOfBodyPart:(NSString *)_partId
549 atURL:(NSURL *)_url password:(NSString *)_pwd
553 id result, fetch, body;
555 if (_partId == nil) return nil;
557 key = [@"body[" stringByAppendingString:_partId];
558 key = [key stringByAppendingString:@"]"];
559 parts = [NSArray arrayWithObjects:&key count:1];
563 result = [self fetchURL:_url parts:parts password:_pwd];
565 /* process results */
567 result = [result objectForKey:@"fetch"];
568 if ([result count] == 0) { /* did not find part */
569 [self errorWithFormat:@"did not find part: %@", _partId];
573 fetch = [result objectAtIndex:0];
574 if ((body = [fetch objectForKey:@"body"]) == nil) {
575 [self errorWithFormat:@"did not find body in response: %@", result];
579 if ((result = [body objectForKey:@"data"]) == nil) {
580 [self errorWithFormat:@"did not find data in body: %@", fetch];
586 - (NSException *)addOrRemove:(BOOL)_flag flags:(id)_f
587 toURL:(NSURL *)_url password:(NSString *)_p
589 NGImap4Client *client;
592 if (![_url isNotNull]) return nil;
593 if (![_f isNotNull]) return nil;
595 if ((client = [self imap4ClientForURL:_url password:_p]) == nil)
598 if (![_f isKindOfClass:[NSArray class]])
599 _f = [NSArray arrayWithObjects:&_f count:1];
601 result = [client storeUid:[[[_url path] lastPathComponent] intValue]
602 add:[NSNumber numberWithBool:_flag]
604 if (![[result valueForKey:@"result"] boolValue]) {
605 [self logWithFormat:@"DEBUG: fail result %@", result];
606 return [NSException exceptionWithHTTPStatus:500 /* server error */
607 reason:@"failed to add flag to IMAP4 message"];
609 /* result contains 'fetch' key with the current flags */
612 - (NSException *)addFlags:(id)_f toURL:(NSURL *)_u password:(NSString *)_p {
613 return [self addOrRemove:YES flags:_f toURL:_u password:_p];
615 - (NSException *)removeFlags:(id)_f toURL:(NSURL *)_u password:(NSString *)_p {
616 return [self addOrRemove:NO flags:_f toURL:_u password:_p];
619 /* managing folders */
621 - (BOOL)isPermissionDeniedResult:(id)_result {
622 if ([[_result valueForKey:@"result"] intValue] != 0)
625 return [[_result valueForKey:@"reason"]
626 isEqualToString:@"Permission denied"];
629 - (NSException *)createMailbox:(NSString *)_mailbox atURL:(NSURL *)_url
630 password:(NSString *)_pwd
632 SOGoMailConnectionEntry *entry;
636 /* check connection cache */
638 if ((entry = [self entryForURL:_url password:_pwd]) == nil) {
639 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
640 reason:@"did not find IMAP4 folder"];
645 newPath = [self imap4FolderNameForURL:_url];
646 newPath = [newPath stringByAppendingString:[self imap4Separator]];
647 newPath = [newPath stringByAppendingString:_mailbox];
651 result = [[entry client] create:newPath];
652 if ([self isPermissionDeniedResult:result]) {
653 return [NSException exceptionWithHTTPStatus:403 /* forbidden */
654 reason:@"creation of folders not allowed"];
656 else if ([[result valueForKey:@"result"] intValue] == 0) {
657 return [NSException exceptionWithHTTPStatus:500 /* server error */
658 reason:[result valueForKey:@"reason"]];
661 [entry flushFolderHierarchyCache];
662 // [self debugWithFormat:@"created mailbox: %@: %@", newPath, result];
666 - (NSException *)deleteMailboxAtURL:(NSURL *)_url password:(NSString *)_pwd {
667 SOGoMailConnectionEntry *entry;
671 /* check connection cache */
673 if ((entry = [self entryForURL:_url password:_pwd]) == nil) {
674 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
675 reason:@"did not find IMAP4 folder"];
680 path = [self imap4FolderNameForURL:_url];
681 result = [[entry client] delete:path];
683 if ([self isPermissionDeniedResult:result]) {
684 return [NSException exceptionWithHTTPStatus:403 /* forbidden */
685 reason:@"creation of folders not allowed"];
687 else if ([[result valueForKey:@"result"] intValue] == 0) {
688 return [NSException exceptionWithHTTPStatus:500 /* server error */
689 reason:[result valueForKey:@"reason"]];
692 [entry flushFolderHierarchyCache];
694 [self debugWithFormat:@"delete mailbox %@: %@", _url, result];
701 - (BOOL)isDebuggingEnabled {
705 @end /* SOGoMailManager */