2 Copyright (C) 2004-2005 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"
27 Could check read-write state:
28 dict = [[self->context client] select:[self absoluteName]];
30 [[dict objectForKey:@"access"] isEqualToString:@"READ-WRITE"]
31 ? NoNumber : YesNumber;
34 @implementation SOGoMailManager
36 static BOOL debugOn = NO;
37 static BOOL debugCache = NO;
38 static BOOL debugKeys = NO;
39 static BOOL poolingOff = NO;
40 static NSTimeInterval PoolScanInterval = 5 * 60;
41 static NSString *imap4Separator = nil;
44 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
46 debugOn = [ud boolForKey:@"SOGoEnableIMAP4Debug"];
47 debugCache = [ud boolForKey:@"SOGoEnableIMAP4CacheDebug"];
48 poolingOff = [ud boolForKey:@"SOGoDisableIMAP4Pooling"];
50 if (debugOn) NSLog(@"Note: SOGoEnableIMAP4Debug is enabled!");
51 if (poolingOff) NSLog(@"WARNING: IMAP4 connection pooling is disabled!");
53 imap4Separator = [[ud stringForKey:@"SOGoIMAP4StringSeparator"] copy];
54 if ([imap4Separator length] == 0)
55 imap4Separator = @"/";
56 NSLog(@"Note(SOGoMailManager): using '%@' as the IMAP4 folder separator.",
60 + (id)defaultMailManager {
61 static SOGoMailManager *manager = nil; // THREAD
63 manager = [[self alloc] init];
68 if ((self = [super init])) {
70 self->urlToEntry = [[NSMutableDictionary alloc] initWithCapacity:256];
73 self->gcTimer = [[NSTimer scheduledTimerWithTimeInterval:
75 target:self selector:@selector(_garbageCollect:)
76 userInfo:nil repeats:YES] retain];
82 if (self->gcTimer) [self->gcTimer invalidate];
83 [self->gcTimer release];
85 [self->urlToEntry release];
91 - (id)cacheKeyForURL:(NSURL *)_url {
92 // protocol, user, host, port
93 return [NSString stringWithFormat:@"%@://%@@%@:%@",
94 [_url scheme], [_url user], [_url host], [_url port]];
97 - (SOGoMailConnectionEntry *)entryForURL:(NSURL *)_url {
101 return [self->urlToEntry objectForKey:[self cacheKeyForURL:_url]];
103 - (void)cacheEntry:(SOGoMailConnectionEntry *)_entry forURL:(NSURL *)_url {
104 if (_entry == nil) _entry = (id)[NSNull null];
105 [self->urlToEntry setObject:_entry forKey:[self cacheKeyForURL:_url]];
108 - (void)_garbageCollect:(NSTimer *)_timer {
109 // TODO: scan for old IMAP4 channels
110 [self debugWithFormat:@"should collect IMAP4 channels (%d active)",
111 [self->urlToEntry count]];
114 - (id)entryForURL:(NSURL *)_url password:(NSString *)_pwd {
117 a) not yet connected => create new entry and connect
118 b) connected, correct password => return cached entry
119 c) connected, different password => try to recreate entry
121 SOGoMailConnectionEntry *entry;
122 NGImap4Client *client;
126 if ((entry = [self entryForURL:_url]) != nil) {
127 if ([entry isValidPassword:_pwd]) {
129 [self logWithFormat:@"valid password, reusing cache entry ..."];
133 /* different password, password could have changed! */
135 [self logWithFormat:@"different password than cached entry: %@", _url];
139 [self debugWithFormat:@"no connection cached yet for url: %@", _url];
143 client = [entry isValidPassword:_pwd]
145 : [self imap4ClientForURL:_url password:_pwd];
150 /* sideeffect of -imap4ClientForURL:password: is to create a cache entry */
151 return [self entryForURL:_url];
156 - (NGImap4Client *)imap4ClientForURL:(NSURL *)_url password:(NSString *)_pwd {
157 // TODO: move to some global IMAP4 connection pool manager
158 SOGoMailConnectionEntry *entry;
159 NGImap4Client *client;
160 NSDictionary *result;
165 /* check connection pool */
167 if ((entry = [self entryForURL:_url]) != nil) {
168 if ([entry isValidPassword:_pwd]) {
169 [self debugWithFormat:@"reused IMAP4 connection for URL: %@", _url];
170 return [entry client];
173 /* different password, password could have changed! */
177 /* setup connection and attempt login */
179 if ((client = [NGImap4Client clientWithURL:_url]) == nil)
182 result = [client login:[_url user] password:_pwd];
183 if (![[result valueForKey:@"result"] boolValue]) {
184 [self errorWithFormat:
185 @"IMAP4 login failed (host=%@,user=%@,pwd=%s,url=%@/%@/%@): %@",
186 [_url host], [_url user], [_pwd length] > 0 ? "yes" : "no",
187 [_url absoluteString], [_url baseURL],
188 NSStringFromClass([[_url baseURL] class]),
193 [self debugWithFormat:@"created new IMAP4 connection for URL: %@", _url];
195 /* cache connection in pool */
197 entry = [[SOGoMailConnectionEntry alloc] initWithClient:client
199 [self cacheEntry:entry forURL:_url];
200 [entry release]; entry = nil;
205 - (void)flushCachesForURL:(NSURL *)_url {
206 SOGoMailConnectionEntry *entry;
208 if ((entry = [self entryForURL:_url]) == nil) /* nothing cached */
211 [entry flushFolderHierarchyCache];
212 [entry flushMailCaches];
215 /* folder hierarchy */
217 - (NSArray *)_getDirectChildren:(NSArray *)_array folderName:(NSString *)_fn {
219 Scans string '_array' for strings which start with the string in '_fn'.
223 unsigned i, count, prefixlen;
225 if ((count = [_array count]) < 2)
226 /* one entry is the folder itself, so we need at least two */
227 return [NSArray array];
229 prefixlen = [_fn length] + 1;
230 ma = [NSMutableArray arrayWithCapacity:count];
231 for (i = 0; i < count; i++) {
234 p = [_array objectAtIndex:i];
235 if ([p length] <= prefixlen)
237 if (![p hasPrefix:_fn])
240 /* cut of common part */
241 p = [p substringFromIndex:prefixlen];
243 /* check whether the path is a sub-subfolder path */
244 if ([p rangeOfString:@"/"].length > 0)
250 [ma sortUsingSelector:@selector(compare:)];
254 - (NSString *)imap4Separator {
255 return imap4Separator;
258 - (NSString *)imap4FolderNameForURL:(NSURL *)_url removeFileName:(BOOL)_delfn {
259 /* a bit hackish, but should be OK */
260 NSString *folderName;
266 folderName = [_url path];
267 if ([folderName length] == 0)
269 if ([folderName characterAtIndex:0] == '/')
270 folderName = [folderName substringFromIndex:1];
272 if (_delfn) folderName = [folderName stringByDeletingLastPathComponent];
274 if ([[self imap4Separator] isEqualToString:@"/"])
277 names = [folderName pathComponents];
278 return [names componentsJoinedByString:[self imap4Separator]];
280 - (NSString *)imap4FolderNameForURL:(NSURL *)_url {
281 return [self imap4FolderNameForURL:_url removeFileName:NO];
284 - (NSArray *)extractSubfoldersForURL:(NSURL *)_url
285 fromResultSet:(NSDictionary *)_result
287 NSString *folderName;
288 NSDictionary *result;
292 /* Note: the result is normalized, that is, it contains / as the separator */
293 folderName = [_url path];
295 /* normalized results already have the / in front on libFoundation?! */
296 if ([folderName hasPrefix:@"/"])
297 folderName = [folderName substringFromIndex:1];
300 result = [_result valueForKey:@"list"];
302 /* Cyrus already tells us whether we need to check for children */
303 flags = [result objectForKey:folderName];
304 if ([flags containsObject:@"hasnochildren"]) {
306 [self logWithFormat:@"folder %@ has no children.", folderName];
311 [self logWithFormat:@"all keys %@: %@", folderName, [result allKeys]];
313 names = [self _getDirectChildren:[result allKeys] folderName:folderName];
315 [self debugWithFormat:@"subfolders of %@: %@", folderName,
316 [names componentsJoinedByString:@","]];
321 - (NSArray *)extractFoldersFromResultSet:(NSDictionary *)_result {
322 /* Note: the result is normalized, that is, it contains / as the separator */
323 return [[_result valueForKey:@"list"] allKeys];
326 - (NSArray *)subfoldersForURL:(NSURL *)_url password:(NSString *)_pwd {
327 SOGoMailConnectionEntry *entry;
328 NSDictionary *result;
331 [self debugWithFormat:@"subfolders for URL: %@ ...",[_url absoluteString]];
333 /* check connection cache */
335 if ((entry = [self entryForURL:_url password:_pwd]) == nil)
338 /* check hierarchy cache */
340 if ((result = [entry cachedHierarchyResults]) != nil)
341 return [self extractSubfoldersForURL:_url fromResultSet:result];
343 [self debugWithFormat:@" no folders cached yet .."];
345 /* fetch _all_ folders */
347 result = [[entry client] list:@"INBOX" pattern:@"*"];
348 if (![[result valueForKey:@"result"] boolValue]) {
349 [self errorWithFormat:@"listing of folder failed!"];
355 if ([result isNotNull]) {
356 if (entry == nil) /* required in case the entry was not setup */
357 entry = [self entryForURL:_url];
359 [entry cacheHierarchyResults:result];
361 [self logWithFormat:@"cached results in entry %@: 0x%08X(%d)",
362 entry, result, [result count]];
368 return [self extractSubfoldersForURL:_url fromResultSet:result];
371 - (NSArray *)allFoldersForURL:(NSURL *)_url password:(NSString *)_pwd {
372 SOGoMailConnectionEntry *entry;
373 NSDictionary *result;
376 [self debugWithFormat:@"folders for URL: %@ ...",[_url absoluteString]];
378 /* check connection cache */
380 if ((entry = [self entryForURL:_url password:_pwd]) == nil)
383 /* check hierarchy cache */
385 if ((result = [entry cachedHierarchyResults]) != nil)
386 return [self extractFoldersFromResultSet:result];
388 [self debugWithFormat:@" no folders cached yet .."];
390 /* fetch _all_ folders */
392 result = [[entry client] list:@"INBOX" pattern:@"*"];
393 if (![[result valueForKey:@"result"] boolValue]) {
394 [self logWithFormat:@"ERROR: listing of folder failed!"];
400 if ([result isNotNull]) {
401 if (entry == nil) /* required in case the entry was not setup */
402 entry = [self entryForURL:_url];
404 [entry cacheHierarchyResults:result];
406 [self logWithFormat:@"cached results in entry %@: 0x%08X(%d)",
407 entry, result, [result count]];
412 return [self extractFoldersFromResultSet:result];
417 - (NSArray *)fetchUIDsInURL:(NSURL *)_url qualifier:(id)_qualifier
418 sortOrdering:(id)_so password:(NSString *)_pwd
421 sortOrdering can be an NSString, an EOSortOrdering or an array of EOS.
423 SOGoMailConnectionEntry *entry;
424 NSDictionary *result;
427 /* check connection cache */
429 if ((entry = [self entryForURL:_url password:_pwd]) == nil)
434 uids = [entry cachedUIDsForURL:_url qualifier:_qualifier sortOrdering:_so];
436 if (debugCache) [self logWithFormat:@"reusing uid cache!"];
437 return [uids isNotNull] ? uids : nil;
440 /* select folder and fetch */
442 result = [[entry client] select:[self imap4FolderNameForURL:_url]];
443 if (![[result valueForKey:@"result"] boolValue]) {
444 [self errorWithFormat:@"could not select URL: %@: %@", _url, result];
448 result = [[entry client] sort:_so qualifier:_qualifier encoding:@"UTF-8"];
449 if (![[result valueForKey:@"result"] boolValue]) {
450 [self errorWithFormat:@"could not sort contents of URL: %@", _url];
454 uids = [result valueForKey:@"sort"];
455 if (![uids isNotNull]) {
456 [self errorWithFormat:@"got no UIDs for URL: %@: %@", _url, result];
462 [entry cacheUIDs:uids forURL:_url qualifier:_qualifier sortOrdering:_so];
466 - (NSArray *)fetchUIDs:(NSArray *)_uids inURL:(NSURL *)_url
467 parts:(NSArray *)_parts password:(NSString *)_pwd
469 // currently returns a dict?!
473 BODY.PEEK[<section>]<<partial>>
474 BODY [this is the bodystructure, supported]
475 BODYSTRUCTURE [not supported yet!]
476 ENVELOPE [this is a parsed header, but does not include type]
484 NGImap4Client *client;
485 NSDictionary *result;
489 if ([_uids count] == 0)
490 return nil; // TODO: might break empty folders?! return a dict!
492 if ((client = [self imap4ClientForURL:_url password:_pwd]) == nil)
497 result = [client select:[self imap4FolderNameForURL:_url]];
498 if (![[result valueForKey:@"result"] boolValue]) {
499 [self errorWithFormat:@"could not select URL: %@: %@", _url, result];
505 // TODO: split uids into batches, otherwise Cyrus will complain
506 // => not really important because we batch before (in the sort)
507 // if the list is too long, we get a:
508 // "* BYE Fatal error: word too long"
510 result = [client fetchUids:_uids parts:_parts];
511 if (![[result valueForKey:@"result"] boolValue]) {
512 [self errorWithFormat:@"could not fetch %d uids for url: %@",
517 //[self logWithFormat:@"RESULT: %@", result];
521 - (id)fetchURL:(NSURL *)_url parts:(NSArray *)_parts password:(NSString *)_pwd{
522 // currently returns a dict
523 NGImap4Client *client;
524 NSDictionary *result;
527 if (![_url isNotNull]) return nil;
529 if ((client = [self imap4ClientForURL:_url password:_pwd]) == nil)
534 result = [client select:[self imap4FolderNameForURL:_url
535 removeFileName:YES]];
536 if (![[result valueForKey:@"result"] boolValue]) {
537 [self errorWithFormat:@"could not select URL: %@: %@", _url, result];
543 uid = [[_url path] lastPathComponent];
545 result = [client fetchUids:[NSArray arrayWithObject:uid] parts:_parts];
546 if (![[result valueForKey:@"result"] boolValue]) {
547 [self errorWithFormat:@"could not fetch url: %@", _url];
550 //[self logWithFormat:@"RESULT: %@", result];
554 - (NSData *)fetchContentOfBodyPart:(NSString *)_partId
555 atURL:(NSURL *)_url password:(NSString *)_pwd
559 id result, fetch, body;
561 if (_partId == nil) return nil;
563 key = [@"body[" stringByAppendingString:_partId];
564 key = [key stringByAppendingString:@"]"];
565 parts = [NSArray arrayWithObjects:&key count:1];
569 result = [self fetchURL:_url parts:parts password:_pwd];
571 /* process results */
573 result = [result objectForKey:@"fetch"];
574 if ([result count] == 0) { /* did not find part */
575 [self errorWithFormat:@"did not find part: %@", _partId];
579 fetch = [result objectAtIndex:0];
580 if ((body = [fetch objectForKey:@"body"]) == nil) {
581 [self errorWithFormat:@"did not find body in response: %@", result];
585 if ((result = [body objectForKey:@"data"]) == nil) {
586 [self errorWithFormat:@"did not find data in body: %@", fetch];
592 - (NSException *)addOrRemove:(BOOL)_flag flags:(id)_f
593 toURL:(NSURL *)_url password:(NSString *)_p
595 NGImap4Client *client;
598 if (![_url isNotNull]) return nil;
599 if (![_f isNotNull]) return nil;
601 if ((client = [self imap4ClientForURL:_url password:_p]) == nil)
604 if (![_f isKindOfClass:[NSArray class]])
605 _f = [NSArray arrayWithObjects:&_f count:1];
607 result = [client storeUid:[[[_url path] lastPathComponent] intValue]
608 add:[NSNumber numberWithBool:_flag]
610 if (![[result valueForKey:@"result"] boolValue]) {
611 [self logWithFormat:@"DEBUG: fail result %@", result];
612 return [NSException exceptionWithHTTPStatus:500 /* server error */
613 reason:@"failed to add flag to IMAP4 message"];
615 /* result contains 'fetch' key with the current flags */
618 - (NSException *)addFlags:(id)_f toURL:(NSURL *)_u password:(NSString *)_p {
619 return [self addOrRemove:YES flags:_f toURL:_u password:_p];
621 - (NSException *)removeFlags:(id)_f toURL:(NSURL *)_u password:(NSString *)_p {
622 return [self addOrRemove:NO flags:_f toURL:_u password:_p];
625 - (NSException *)markURLDeleted:(NSURL *)_url password:(NSString *)_p {
626 return [self addOrRemove:YES flags:@"Deleted" toURL:_url password:_p];
629 - (NSException *)postData:(NSData *)_data flags:(id)_f
630 toFolderURL:(NSURL *)_url password:(NSString *)_p
632 NGImap4Client *client;
635 if (![_url isNotNull]) return nil;
636 if (![_f isNotNull]) _f = [NSArray array];
638 if ((client = [self imap4ClientForURL:_url password:_p]) == nil)
641 if (![_f isKindOfClass:[NSArray class]])
642 _f = [NSArray arrayWithObjects:&_f count:1];
644 result = [client append:_data
645 toFolder:[self imap4FolderNameForURL:_url]
647 if (![[result valueForKey:@"result"] boolValue]) {
648 [self logWithFormat:@"DEBUG: fail result %@", result];
649 return [NSException exceptionWithHTTPStatus:500 /* server error */
650 reason:@"failed to store message to IMAP4 message"];
652 /* result contains 'fetch' key with the current flags */
656 /* managing folders */
658 - (BOOL)isPermissionDeniedResult:(id)_result {
659 if ([[_result valueForKey:@"result"] intValue] != 0)
662 return [[_result valueForKey:@"reason"]
663 isEqualToString:@"Permission denied"];
666 - (NSException *)createMailbox:(NSString *)_mailbox atURL:(NSURL *)_url
667 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 newPath = [self imap4FolderNameForURL:_url];
683 newPath = [newPath stringByAppendingString:[self imap4Separator]];
684 newPath = [newPath stringByAppendingString:_mailbox];
688 result = [[entry client] create:newPath];
689 if ([self isPermissionDeniedResult:result]) {
690 return [NSException exceptionWithHTTPStatus:403 /* forbidden */
691 reason:@"creation of folders not allowed"];
693 else if ([[result valueForKey:@"result"] intValue] == 0) {
694 return [NSException exceptionWithHTTPStatus:500 /* server error */
695 reason:[result valueForKey:@"reason"]];
698 [entry flushFolderHierarchyCache];
699 // [self debugWithFormat:@"created mailbox: %@: %@", newPath, result];
703 - (NSException *)deleteMailboxAtURL:(NSURL *)_url password:(NSString *)_pwd {
704 SOGoMailConnectionEntry *entry;
708 /* check connection cache */
710 if ((entry = [self entryForURL:_url password:_pwd]) == nil) {
711 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
712 reason:@"did not find IMAP4 folder"];
717 path = [self imap4FolderNameForURL:_url];
718 result = [[entry client] delete:path];
720 if ([self isPermissionDeniedResult:result]) {
721 return [NSException exceptionWithHTTPStatus:403 /* forbidden */
722 reason:@"creation of folders not allowed"];
724 else if ([[result valueForKey:@"result"] intValue] == 0) {
725 return [NSException exceptionWithHTTPStatus:500 /* server error */
726 reason:[result valueForKey:@"reason"]];
729 [entry flushFolderHierarchyCache];
731 [self debugWithFormat:@"delete mailbox %@: %@", _url, result];
738 - (BOOL)isDebuggingEnabled {
742 @end /* SOGoMailManager */