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]) {
179 @"ERROR: IMAP4 login failed "
180 @"(host=%@,user=%@,pwd=%s,url=%@/%@/%@): "
182 [_url host], [_url user], [_pwd length] > 0 ? "yes" : "no",
183 [_url absoluteString], [_url baseURL],
184 NSStringFromClass([[_url baseURL] class]),
189 [self debugWithFormat:@"created new IMAP4 connection for URL: %@", _url];
191 /* cache connection in pool */
193 entry = [[SOGoMailConnectionEntry alloc] initWithClient:client
195 [self cacheEntry:entry forURL:_url];
196 [entry release]; entry = nil;
201 - (void)flushCachesForURL:(NSURL *)_url {
202 SOGoMailConnectionEntry *entry;
204 if ((entry = [self entryForURL:_url]) == nil) /* nothing cached */
207 [entry flushFolderHierarchyCache];
208 [entry flushMailCaches];
211 /* folder hierarchy */
213 - (NSArray *)_getDirectChildren:(NSArray *)_array folderName:(NSString *)_fn {
215 Scans string '_array' for strings which start with the string in '_fn'.
219 unsigned i, count, prefixlen;
221 if ((count = [_array count]) < 2)
222 /* one entry is the folder itself, so we need at least two */
223 return [NSArray array];
225 prefixlen = [_fn length] + 1;
226 ma = [NSMutableArray arrayWithCapacity:count];
227 for (i = 0; i < count; i++) {
230 p = [_array objectAtIndex:i];
231 if ([p length] <= prefixlen)
233 if (![p hasPrefix:_fn])
236 /* cut of common part */
237 p = [p substringFromIndex:prefixlen];
239 /* check whether the path is a sub-subfolder path */
240 if ([p rangeOfString:@"/"].length > 0)
246 [ma sortUsingSelector:@selector(compare:)];
250 - (NSString *)imap4Separator {
251 return imap4Separator;
254 - (NSString *)imap4FolderNameForURL:(NSURL *)_url removeFileName:(BOOL)_delfn {
255 /* a bit hackish, but should be OK */
256 NSString *folderName;
262 folderName = [_url path];
263 if ([folderName length] == 0)
265 if ([folderName characterAtIndex:0] == '/')
266 folderName = [folderName substringFromIndex:1];
268 if (_delfn) folderName = [folderName stringByDeletingLastPathComponent];
270 if ([[self imap4Separator] isEqualToString:@"/"])
273 names = [folderName pathComponents];
274 return [names componentsJoinedByString:[self imap4Separator]];
276 - (NSString *)imap4FolderNameForURL:(NSURL *)_url {
277 return [self imap4FolderNameForURL:_url removeFileName:NO];
280 - (NSArray *)extractSubfoldersForURL:(NSURL *)_url
281 fromResultSet:(NSDictionary *)_result
283 NSString *folderName;
284 NSDictionary *result;
288 /* Note: the result is normalized, that is, it contains / as the separator */
289 folderName = [_url path];
291 /* normalized results already have the / in front on libFoundation?! */
292 if ([folderName hasPrefix:@"/"])
293 folderName = [folderName substringFromIndex:1];
296 result = [_result valueForKey:@"list"];
298 /* Cyrus already tells us whether we need to check for children */
299 flags = [result objectForKey:folderName];
300 if ([flags containsObject:@"hasnochildren"]) {
302 [self logWithFormat:@"folder %@ has no children.", folderName];
307 [self logWithFormat:@"all keys %@: %@", folderName, [result allKeys]];
309 names = [self _getDirectChildren:[result allKeys] folderName:folderName];
311 [self debugWithFormat:@"subfolders of %@: %@", folderName,
312 [names componentsJoinedByString:@","]];
317 - (NSArray *)extractFoldersFromResultSet:(NSDictionary *)_result {
318 /* Note: the result is normalized, that is, it contains / as the separator */
319 return [[_result valueForKey:@"list"] allKeys];
322 - (NSArray *)subfoldersForURL:(NSURL *)_url password:(NSString *)_pwd {
323 SOGoMailConnectionEntry *entry;
324 NSDictionary *result;
327 [self debugWithFormat:@"subfolders for URL: %@ ...",[_url absoluteString]];
329 /* check connection cache */
331 if ((entry = [self entryForURL:_url password:_pwd]) == nil)
334 /* check hierarchy cache */
336 if ((result = [entry cachedHierarchyResults]) != nil)
337 return [self extractSubfoldersForURL:_url fromResultSet:result];
339 [self debugWithFormat:@" no folders cached yet .."];
341 /* fetch _all_ folders */
343 result = [[entry client] list:@"INBOX" pattern:@"*"];
344 if (![[result valueForKey:@"result"] boolValue]) {
345 [self logWithFormat:@"ERROR: listing of folder failed!"];
351 if ([result isNotNull]) {
352 if (entry == nil) /* required in case the entry was not setup */
353 entry = [self entryForURL:_url];
355 [entry cacheHierarchyResults:result];
357 [self logWithFormat:@"cached results in entry %@: 0x%08X(%d)",
358 entry, result, [result count]];
364 return [self extractSubfoldersForURL:_url fromResultSet:result];
367 - (NSArray *)allFoldersForURL:(NSURL *)_url password:(NSString *)_pwd {
368 SOGoMailConnectionEntry *entry;
369 NSDictionary *result;
372 [self debugWithFormat:@"folders for URL: %@ ...",[_url absoluteString]];
374 /* check connection cache */
376 if ((entry = [self entryForURL:_url password:_pwd]) == nil)
379 /* check hierarchy cache */
381 if ((result = [entry cachedHierarchyResults]) != nil)
382 return [self extractFoldersFromResultSet:result];
384 [self debugWithFormat:@" no folders cached yet .."];
386 /* fetch _all_ folders */
388 result = [[entry client] list:@"INBOX" pattern:@"*"];
389 if (![[result valueForKey:@"result"] boolValue]) {
390 [self logWithFormat:@"ERROR: listing of folder failed!"];
396 if ([result isNotNull]) {
397 if (entry == nil) /* required in case the entry was not setup */
398 entry = [self entryForURL:_url];
400 [entry cacheHierarchyResults:result];
402 [self logWithFormat:@"cached results in entry %@: 0x%08X(%d)",
403 entry, result, [result count]];
408 return [self extractFoldersFromResultSet:result];
413 - (NSArray *)fetchUIDsInURL:(NSURL *)_url qualifier:(id)_qualifier
414 sortOrdering:(id)_so password:(NSString *)_pwd
417 sortOrdering can be an NSString, an EOSortOrdering or an array of EOS.
419 SOGoMailConnectionEntry *entry;
420 NSDictionary *result;
423 /* check connection cache */
425 if ((entry = [self entryForURL:_url password:_pwd]) == nil)
430 uids = [entry cachedUIDsForURL:_url qualifier:_qualifier sortOrdering:_so];
432 if (debugCache) [self logWithFormat:@"reusing uid cache!"];
433 return [uids isNotNull] ? uids : nil;
436 /* select folder and fetch */
438 result = [[entry client] select:[self imap4FolderNameForURL:_url]];
439 if (![[result valueForKey:@"result"] boolValue]) {
440 [self logWithFormat:@"ERROR: could not select URL: %@: %@", _url, result];
444 result = [[entry client] sort:_so qualifier:_qualifier encoding:@"UTF-8"];
445 if (![[result valueForKey:@"result"] boolValue]) {
446 [self logWithFormat:@"ERROR: could not sort contents of URL: %@", _url];
450 uids = [result valueForKey:@"sort"];
451 if (![uids isNotNull]) {
452 [self logWithFormat:@"ERROR: got no UIDs for URL: %@: %@", _url, result];
458 [entry cacheUIDs:uids forURL:_url qualifier:_qualifier sortOrdering:_so];
462 - (NSArray *)fetchUIDs:(NSArray *)_uids inURL:(NSURL *)_url
463 parts:(NSArray *)_parts password:(NSString *)_pwd
465 // currently returns a dict?!
469 BODY.PEEK[<section>]<<partial>>
470 BODY [this is the bodystructure, supported]
471 BODYSTRUCTURE [not supported yet!]
472 ENVELOPE [this is a parsed header, but does not include type]
480 NGImap4Client *client;
481 NSDictionary *result;
485 if ([_uids count] == 0)
486 return nil; // TODO: might break empty folders?! return a dict!
488 if ((client = [self imap4ClientForURL:_url password:_pwd]) == nil)
493 result = [client select:[self imap4FolderNameForURL:_url]];
494 if (![[result valueForKey:@"result"] boolValue]) {
495 [self logWithFormat:@"ERROR: could not select URL: %@: %@", _url, result];
501 // TODO: split uids into batches, otherwise Cyrus will complain
502 // => not really important because we batch before (in the sort)
503 // if the list is too long, we get a:
504 // "* BYE Fatal error: word too long"
506 result = [client fetchUids:_uids parts:_parts];
507 if (![[result valueForKey:@"result"] boolValue]) {
508 [self logWithFormat:@"ERROR: could not fetch %d uids for url: %@",
513 //[self logWithFormat:@"RESULT: %@", result];
517 - (id)fetchURL:(NSURL *)_url parts:(NSArray *)_parts password:(NSString *)_pwd{
518 // currently returns a dict
519 NGImap4Client *client;
520 NSDictionary *result;
523 if (![_url isNotNull]) return nil;
525 if ((client = [self imap4ClientForURL:_url password:_pwd]) == nil)
530 result = [client select:[self imap4FolderNameForURL:_url
531 removeFileName:YES]];
532 if (![[result valueForKey:@"result"] boolValue]) {
533 [self logWithFormat:@"ERROR: could not select URL: %@: %@", _url, result];
539 uid = [[_url path] lastPathComponent];
541 result = [client fetchUids:[NSArray arrayWithObject:uid] parts:_parts];
542 if (![[result valueForKey:@"result"] boolValue]) {
543 [self logWithFormat:@"ERROR: could not fetch url: %@", _url];
546 //[self logWithFormat:@"RESULT: %@", result];
550 - (NSData *)fetchContentOfBodyPart:(NSString *)_partId
551 atURL:(NSURL *)_url password:(NSString *)_pwd
555 id result, fetch, body;
557 if (_partId == nil) return nil;
559 key = [@"body[" stringByAppendingString:_partId];
560 key = [key stringByAppendingString:@"]"];
561 parts = [NSArray arrayWithObjects:&key count:1];
565 result = [self fetchURL:_url parts:parts password:_pwd];
567 /* process results */
569 result = [result objectForKey:@"fetch"];
570 if ([result count] == 0) { /* did not find part */
571 [self logWithFormat:@"ERROR: did not find part: %@", _partId];
575 fetch = [result objectAtIndex:0];
576 if ((body = [fetch objectForKey:@"body"]) == nil) {
577 [self logWithFormat:@"ERROR: did not find body in response: %@", result];
581 if ((result = [body objectForKey:@"data"]) == nil) {
582 [self logWithFormat:@"ERROR: did not find data in body: %@", fetch];
588 - (NSException *)addOrRemove:(BOOL)_flag flags:(id)_f
589 toURL:(NSURL *)_url password:(NSString *)_p
591 NGImap4Client *client;
594 if (![_url isNotNull]) return nil;
595 if (![_f isNotNull]) return nil;
597 if ((client = [self imap4ClientForURL:_url password:_p]) == nil)
600 if (![_f isKindOfClass:[NSArray class]])
601 _f = [NSArray arrayWithObjects:&_f count:1];
603 result = [client storeUid:[[[_url path] lastPathComponent] intValue]
604 add:[NSNumber numberWithBool:_flag]
606 if (![[result valueForKey:@"result"] boolValue]) {
607 [self logWithFormat:@"DEBUG: fail result %@", result];
608 return [NSException exceptionWithHTTPStatus:500 /* server error */
609 reason:@"failed to add flag to IMAP4 message"];
611 /* result contains 'fetch' key with the current flags */
614 - (NSException *)addFlags:(id)_f toURL:(NSURL *)_u password:(NSString *)_p {
615 return [self addOrRemove:YES flags:_f toURL:_u password:_p];
617 - (NSException *)removeFlags:(id)_f toURL:(NSURL *)_u password:(NSString *)_p {
618 return [self addOrRemove:NO flags:_f toURL:_u password:_p];
621 /* managing folders */
623 - (BOOL)isPermissionDeniedResult:(id)_result {
624 if ([[_result valueForKey:@"result"] intValue] != 0)
627 return [[_result valueForKey:@"reason"]
628 isEqualToString:@"Permission denied"];
631 - (NSException *)createMailbox:(NSString *)_mailbox atURL:(NSURL *)_url
632 password:(NSString *)_pwd
634 SOGoMailConnectionEntry *entry;
638 /* check connection cache */
640 if ((entry = [self entryForURL:_url password:_pwd]) == nil) {
641 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
642 reason:@"did not find IMAP4 folder"];
647 newPath = [self imap4FolderNameForURL:_url];
648 newPath = [newPath stringByAppendingString:[self imap4Separator]];
649 newPath = [newPath stringByAppendingString:_mailbox];
653 result = [[entry client] create:newPath];
654 if ([self isPermissionDeniedResult:result]) {
655 return [NSException exceptionWithHTTPStatus:403 /* forbidden */
656 reason:@"creation of folders not allowed"];
658 else if ([[result valueForKey:@"result"] intValue] == 0) {
659 return [NSException exceptionWithHTTPStatus:500 /* server error */
660 reason:[result valueForKey:@"reason"]];
663 [entry flushFolderHierarchyCache];
664 // [self debugWithFormat:@"created mailbox: %@: %@", newPath, result];
668 - (NSException *)deleteMailboxAtURL:(NSURL *)_url password:(NSString *)_pwd {
669 SOGoMailConnectionEntry *entry;
673 /* check connection cache */
675 if ((entry = [self entryForURL:_url password:_pwd]) == nil) {
676 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
677 reason:@"did not find IMAP4 folder"];
682 path = [self imap4FolderNameForURL:_url];
683 result = [[entry client] delete:path];
685 if ([self isPermissionDeniedResult:result]) {
686 return [NSException exceptionWithHTTPStatus:403 /* forbidden */
687 reason:@"creation of folders not allowed"];
689 else if ([[result valueForKey:@"result"] intValue] == 0) {
690 return [NSException exceptionWithHTTPStatus:500 /* server error */
691 reason:[result valueForKey:@"reason"]];
694 [entry flushFolderHierarchyCache];
696 [self debugWithFormat:@"delete mailbox %@: %@", _url, result];
703 - (BOOL)isDebuggingEnabled {
707 @end /* SOGoMailManager */