2 Copyright (C) 2004-2007 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 "NGImap4Connection.h"
23 #include "NGImap4MailboxInfo.h"
24 #include "NGImap4Client.h"
27 @implementation NGImap4Connection
29 static BOOL debugOn = NO;
30 static BOOL debugCache = NO;
31 static BOOL debugKeys = NO;
32 static BOOL alwaysSelect = NO;
33 static BOOL onlyFetchInbox = NO;
34 static NSString *imap4Separator = nil;
37 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
39 debugOn = [ud boolForKey:@"NGImap4ConnectionDebugEnabled"];
40 debugCache = [ud boolForKey:@"NGImap4ConnectionCacheDebugEnabled"];
41 debugKeys = [ud boolForKey:@"NGImap4ConnectionFolderDebugEnabled"];
42 alwaysSelect = [ud boolForKey:@"NGImap4ConnectionAlwaysSelect"];
44 if (debugOn) NSLog(@"Note: NGImap4ConnectionDebugEnabled is enabled!");
46 NSLog(@"WARNING: 'NGImap4ConnectionAlwaysSelect' enabled (slow down)");
49 [[ud stringForKey:@"NGImap4ConnectionStringSeparator"] copy];
50 if (![imap4Separator isNotEmpty])
51 imap4Separator = @"/";
52 NSLog(@"Note(NGImap4Connection): using '%@' as the IMAP4 folder separator.",
56 - (id)initWithClient:(NGImap4Client *)_client password:(NSString *)_pwd {
57 if (_client == nil || _pwd == nil) {
62 if ((self = [super init])) {
63 self->client = [_client retain];
64 self->password = [_pwd copy];
66 self->creationTime = [[NSDate alloc] init];
68 // TODO: retrieve from IMAP4 instead of using a default
69 self->separator = imap4Separator;
74 return [self initWithClient:nil password:nil];
78 [self->separator release];
79 [self->urlToRights release];
80 [self->cachedUIDs release];
81 [self->uidFolderURL release];
82 [self->uidSortOrdering release];
83 [self->creationTime release];
84 [self->subfolders release];
85 [self->password release];
86 [self->client release];
92 - (NGImap4Client *)client {
95 - (BOOL)isValidPassword:(NSString *)_pwd {
96 return [self->password isEqualToString:_pwd];
99 - (NSDate *)creationTime {
100 return self->creationTime;
103 - (void)cacheHierarchyResults:(NSDictionary *)_hierarchy {
104 ASSIGNCOPY(self->subfolders, _hierarchy);
106 - (NSDictionary *)cachedHierarchyResults {
107 return self->subfolders;
109 - (void)flushFolderHierarchyCache {
110 [self->subfolders release]; self->subfolders = nil;
111 [self->urlToRights release]; self->urlToRights = nil;
116 - (NSString *)cachedMyRightsForURL:(NSURL *)_url {
117 return (_url != nil) ? [self->urlToRights objectForKey:_url] : nil;
119 - (void)cacheMyRights:(NSString *)_rights forURL:(NSURL *)_url {
120 if (self->urlToRights == nil)
121 self->urlToRights = [[NSMutableDictionary alloc] initWithCapacity:8];
122 [self->urlToRights setObject:_rights forKey:_url];
127 - (id)cachedUIDsForURL:(NSURL *)_url qualifier:(id)_q sortOrdering:(id)_so {
130 if (![_so isEqual:self->uidSortOrdering])
132 if (![self->uidFolderURL isEqual:_url])
135 return self->cachedUIDs;
138 - (void)cacheUIDs:(NSArray *)_uids forURL:(NSURL *)_url
139 qualifier:(id)_q sortOrdering:(id)_so
144 ASSIGNCOPY(self->uidSortOrdering, _so);
145 ASSIGNCOPY(self->uidFolderURL, _url);
146 ASSIGNCOPY(self->cachedUIDs, _uids);
149 - (void)flushMailCaches {
150 ASSIGN(self->uidSortOrdering, nil);
151 ASSIGN(self->uidFolderURL, nil);
152 ASSIGN(self->cachedUIDs, nil);
158 - (NSException *)errorCouldNotSelectURL:(NSURL *)_url {
164 ? [@"Could not select IMAP4 folder: " stringByAppendingString:
165 [_url absoluteString]]
166 : (NSString *)@"Could not select IMAP4 folder!";
168 ui = [[NSDictionary alloc] initWithObjectsAndKeys:
169 [NSNumber numberWithInt:404], @"http-status",
173 e = [NSException exceptionWithName:@"NGImap4Exception"
174 reason:r userInfo:ui];
175 [ui release]; ui = nil;
179 - (NSException *)errorForResult:(NSDictionary *)_result text:(NSString *)_txt {
184 if ([[_result valueForKey:@"result"] boolValue])
185 return nil; /* everything went fine! */
187 if ((r = [_result valueForKey:@"reason"]) != nil)
188 r = [[_txt stringByAppendingString:@": "] stringByAppendingString:r];
192 if ([r isEqualToString:@"Permission denied"]) {
193 /* different for each server?, no error codes in IMAP4 ... */
194 status = 403 /* Forbidden */;
197 status = 500 /* internal server error */;
199 ui = [NSDictionary dictionaryWithObjectsAndKeys:
200 [NSNumber numberWithInt:status], @"http-status",
201 _result, @"rawResult",
204 return [NSException exceptionWithName:@"NGImap4Exception"
205 reason:r userInfo:ui];
208 /* IMAP4 path/url processing methods */
210 NSArray *SOGoMailGetDirectChildren(NSArray *_array, NSString *_fn) {
212 Scans string '_array' for strings which start with the string in '_fn'.
216 unsigned i, count, prefixlen;
218 if ((count = [_array count]) < 2) {
219 /* one entry is the folder itself, so we need at least two */
220 return [NSArray array];
223 // TODO: somehow results are different on OSX
224 // we should investigate and test all Foundation libraries and document the
227 prefixlen = [_fn isEqualToString:@""] ? 0 : [_fn length] + 1;
228 #elif GNUSTEP_BASE_LIBRARY
229 prefixlen = [_fn isEqualToString:@"/"] ? 1 : [_fn length];
231 prefixlen = [_fn isEqualToString:@"/"] ? 1 : [_fn length] + 1;
233 ma = [NSMutableArray arrayWithCapacity:count];
234 for (i = 0; i < count; i++) {
237 p = [_array objectAtIndex:i];
238 if ([p length] <= prefixlen)
240 if (prefixlen != 0 && ![p hasPrefix:_fn])
243 /* cut of common part */
244 p = [p substringFromIndex:prefixlen];
246 /* check whether the path is a sub-subfolder path */
247 if ([p rangeOfString:@"/"].length > 0)
253 [ma sortUsingSelector:@selector(compare:)];
257 - (NSArray *)extractSubfoldersForURL:(NSURL *)_url
258 fromResultSet:(NSDictionary *)_result
260 NSString *folderName;
261 NSDictionary *result;
265 /* Note: the result is normalized, that is, it contains / as the separator */
266 folderName = [_url path];
268 /* normalized results already have the / in front on libFoundation?! */
269 if ([folderName hasPrefix:@"/"])
270 folderName = [folderName substringFromIndex:1];
273 result = [_result valueForKey:@"list"];
275 /* Cyrus already tells us whether we need to check for children */
276 flags = [result objectForKey:folderName];
277 if ([flags containsObject:@"hasnochildren"]) {
279 [self logWithFormat:@"%s: folder %@ has no children.",
280 __PRETTY_FUNCTION__,folderName];
284 if ([flags containsObject:@"noinferiors"]) {
286 [self logWithFormat:@"%s: folder %@ cannot contain children.",
287 __PRETTY_FUNCTION__,folderName];
293 [self logWithFormat:@"%s: all keys %@: %@",
294 __PRETTY_FUNCTION__, folderName,
295 [[result allKeys] componentsJoinedByString:@", "]];
298 names = SOGoMailGetDirectChildren([result allKeys], folderName);
301 @"%s: subfolders of '%@': %@", __PRETTY_FUNCTION__, folderName,
302 [names componentsJoinedByString:@","]];
307 - (NSString *)imap4Separator {
308 return self->separator;
311 - (NSString *)imap4FolderNameForURL:(NSURL *)_url removeFileName:(BOOL)_delfn {
312 /* a bit hackish, but should be OK */
313 NSString *folderName;
319 folderName = [_url path];
320 if (![folderName isNotEmpty])
322 if ([folderName characterAtIndex:0] == '/')
323 folderName = [folderName substringFromIndex:1];
325 if (_delfn) folderName = [folderName stringByDeletingLastPathComponent];
327 if ([[self imap4Separator] isEqualToString:@"/"])
330 names = [folderName pathComponents];
331 return [names componentsJoinedByString:[self imap4Separator]];
333 - (NSString *)imap4FolderNameForURL:(NSURL *)_url {
334 return [self imap4FolderNameForURL:_url removeFileName:NO];
337 - (NSArray *)extractFoldersFromResultSet:(NSDictionary *)_result {
338 /* Note: the result is normalized, that is, it contains / as the separator */
339 return [[_result valueForKey:@"list"] allKeys];
342 /* folder selections */
344 - (BOOL)selectFolder:(id)_url {
345 NSDictionary *result;
348 newFolder = [_url isKindOfClass:[NSURL class]]
349 ? [self imap4FolderNameForURL:_url]
353 if ([[[self client] selectedFolderName] isEqualToString:newFolder])
357 result = [[self client] select:newFolder];
358 if (![[result valueForKey:@"result"] boolValue]) {
359 [self errorWithFormat:@"could not select URL: %@: %@", _url, result];
366 - (BOOL)isPermissionDeniedResult:(id)_result {
367 if ([[_result valueForKey:@"result"] intValue] != 0)
370 return [[_result valueForKey:@"reason"]
371 isEqualToString:@"Permission denied"];
374 /* folder operations */
376 - (NSDictionary *)primaryFetchMailboxHierarchyForURL:(NSURL *)_url {
377 NSDictionary *result;
379 if ((result = [self cachedHierarchyResults]) != nil)
380 return [result isNotNull] ? result : (NSDictionary *)nil;
382 if (debugCache) [self logWithFormat:@" no folders cached yet .."];
384 result = [[self client] list:(onlyFetchInbox ? @"INBOX" : @"*")
386 if (![[result valueForKey:@"result"] boolValue]) {
387 [self errorWithFormat:@"Could not list mailbox hierarchy!"];
393 if ([result isNotNull]) {
394 [self cacheHierarchyResults:result];
396 [self logWithFormat:@"cached results: 0x%p(%d)",
397 result, [result count]];
403 - (NSArray *)subfoldersForURL:(NSURL *)_url {
404 NSDictionary *result;
406 if ((result = [self primaryFetchMailboxHierarchyForURL:_url]) == nil)
408 if ([result isKindOfClass:[NSException class]]) {
409 [self errorWithFormat:@"failed to retrieve hierarchy: %@", result];
413 return [self extractSubfoldersForURL:_url fromResultSet:result];
416 - (NSArray *)allFoldersForURL:(NSURL *)_url {
417 NSDictionary *result;
419 if ((result = [self primaryFetchMailboxHierarchyForURL:_url]) == nil)
421 if ([result isKindOfClass:[NSException class]]) {
422 [self errorWithFormat:@"failed to retrieve hierarchy: %@", result];
426 return [self extractFoldersFromResultSet:result];
429 /* message operations */
431 - (NSArray *)fetchUIDsInURL:(NSURL *)_url qualifier:(id)_qualifier
435 sortOrdering can be an NSString, an EOSortOrdering or an array of EOS.
437 NSDictionary *result;
442 uids = [self cachedUIDsForURL:_url qualifier:_qualifier sortOrdering:_so];
444 if (debugCache) [self logWithFormat:@"reusing uid cache!"];
445 return [uids isNotNull] ? uids : (NSArray *)nil;
448 /* select folder and fetch */
450 if (![self selectFolder:_url])
453 result = [[self client] sort:_so qualifier:_qualifier encoding:@"UTF-8"];
454 if (![[result valueForKey:@"result"] boolValue]) {
455 [self errorWithFormat:@"could not sort contents of URL: %@", _url];
459 uids = [result valueForKey:@"sort"];
460 if (![uids isNotNull]) {
461 [self errorWithFormat:@"got no UIDs for URL: %@: %@", _url, result];
467 [self cacheUIDs:uids forURL:_url qualifier:_qualifier sortOrdering:_so];
471 - (NSArray *)fetchUIDs:(NSArray *)_uids inURL:(NSURL *)_url
472 parts:(NSArray *)_parts
474 // currently returns a dict?!
478 BODY.PEEK[<section>]<<partial>>
479 BODY [this is the bodystructure, supported]
480 BODYSTRUCTURE [not supported yet!]
481 ENVELOPE [this is a parsed header, but does not include type]
489 NSDictionary *result;
493 if (![_uids isNotEmpty])
494 return nil; // TODO: might break empty folders?! return a dict!
498 if (![self selectFolder:_url])
503 // TODO: split uids into batches, otherwise Cyrus will complain
504 // => not really important because we batch before (in the sort)
505 // if the list is too long, we get a:
506 // "* BYE Fatal error: word too long"
508 result = [[self client] fetchUids:_uids parts:_parts];
509 if (![[result valueForKey:@"result"] boolValue]) {
510 [self errorWithFormat:@"could not fetch %d uids for url: %@",
515 //[self logWithFormat:@"RESULT: %@", result];
519 - (id)fetchURL:(NSURL *)_url parts:(NSArray *)_parts {
520 // currently returns a dict
521 NSDictionary *result;
524 if (![_url isNotNull]) return nil;
528 uid = [self imap4FolderNameForURL:_url removeFileName:YES];
529 if (![self selectFolder:uid])
534 uid = [[_url path] lastPathComponent];
536 result = [client fetchUids:[NSArray arrayWithObject:uid] parts:_parts];
537 if (![[result valueForKey:@"result"] boolValue]) {
538 [self errorWithFormat:@"could not fetch url: %@", _url];
541 //[self logWithFormat:@"RESULT: %@", result];
545 - (NSData *)fetchContentOfBodyPart:(NSString *)_partId atURL:(NSURL *)_url {
548 id result, fetch, body;
550 if (_partId == nil) return nil;
552 key = [@"body[" stringByAppendingString:_partId];
553 key = [key stringByAppendingString:@"]"];
554 parts = [NSArray arrayWithObjects:&key count:1];
558 result = [self fetchURL:_url parts:parts];
560 /* process results */
562 result = [(NSDictionary *)result objectForKey:@"fetch"];
563 if (![result isNotEmpty]) { /* did not find part */
564 [self errorWithFormat:@"did not find part: %@", _partId];
568 fetch = [result objectAtIndex:0];
569 if ((body = [(NSDictionary *)fetch objectForKey:@"body"]) == nil) {
570 [self errorWithFormat:@"did not find body in response: %@", result];
574 if ((result = [(NSDictionary *)body objectForKey:@"data"]) == nil) {
575 [self errorWithFormat:@"did not find data in body: %@", fetch];
583 - (NSException *)addOrRemove:(BOOL)_flag flags:(id)_f toURL:(NSURL *)_url {
586 if (![_url isNotNull]) return nil;
587 if (![_f isNotNull]) return nil;
589 if (![_f isKindOfClass:[NSArray class]])
590 _f = [NSArray arrayWithObjects:&_f count:1];
594 result = [self imap4FolderNameForURL:_url removeFileName:YES];
595 if (![self selectFolder:result])
596 return [self errorCouldNotSelectURL:_url];
600 result = [[self client] storeUid:[[[_url path] lastPathComponent] intValue]
601 add:[NSNumber numberWithBool:_flag]
603 if (![[result valueForKey:@"result"] boolValue]) {
604 return [self errorForResult:result
605 text:@"Failed to change flags of IMAP4 message"];
607 /* result contains 'fetch' key with the current flags */
610 - (NSException *)addFlags:(id)_f toURL:(NSURL *)_u {
611 return [self addOrRemove:YES flags:_f toURL:_u];
613 - (NSException *)removeFlags:(id)_f toURL:(NSURL *)_u {
614 return [self addOrRemove:NO flags:_f toURL:_u];
617 - (NSException *)markURLDeleted:(NSURL *)_url {
618 return [self addOrRemove:YES flags:@"Deleted" toURL:_url];
621 - (NSException *)addFlags:(id)_f toAllMessagesInURL:(NSURL *)_url {
624 if (![_url isNotNull]) return nil;
625 if (![_f isNotNull]) return nil;
627 if (![_f isKindOfClass:[NSArray class]])
628 _f = [NSArray arrayWithObjects:&_f count:1];
632 if (![self selectFolder:[self imap4FolderNameForURL:_url]])
633 return [self errorCouldNotSelectURL:_url];
635 /* fetch all sequence numbers */
637 result = [client searchWithQualifier:nil /* means: ALL */];
638 if (![[result valueForKey:@"result"] boolValue]) {
639 return [self errorForResult:result
640 text:@"Could not search in IMAP4 folder"];
643 result = [result valueForKey:@"search"];
644 if (![result isNotEmpty]) /* no messages in there, nothin' to be done */
649 result = [[self client] storeFlags:_f forMSNs:result addOrRemove:YES];
650 if (![[result valueForKey:@"result"] boolValue]) {
651 return [self errorForResult:result
652 text:@"Failed to change flags of IMAP4 message"];
658 /* posting new data */
660 - (NSException *)postData:(NSData *)_data flags:(id)_f
661 toFolderURL:(NSURL *)_url
665 if (![_url isNotNull]) return nil;
666 if (![_f isNotNull]) _f = [NSArray array];
668 if (![_f isKindOfClass:[NSArray class]])
669 _f = [NSArray arrayWithObjects:&_f count:1];
671 result = [[self client] append:_data
672 toFolder:[self imap4FolderNameForURL:_url]
674 if (![[result valueForKey:@"result"] boolValue])
675 return [self errorForResult:result text:@"Failed to store message"];
677 /* result contains 'fetch' key with the current flags */
679 // TODO: need to flush any caches?
685 - (NSException *)expungeAtURL:(NSURL *)_url {
691 p = [self imap4FolderNameForURL:_url removeFileName:NO];
692 if (![self selectFolder:p])
693 return [self errorCouldNotSelectURL:_url];
697 result = [[self client] expunge];
699 if (![[result valueForKey:@"result"] boolValue]) {
700 [self errorWithFormat:@"could not expunge url: %@", _url];
703 //[self logWithFormat:@"RESULT: %@", result];
707 /* copying and moving */
709 - (NSException *)copyMailURL:(NSURL *)_srcurl toFolderURL:(NSURL *)_desturl {
710 NSString *srcname, *destname;
716 srcname = [self imap4FolderNameForURL:_srcurl removeFileName:YES];
717 uid = [[[_srcurl path] lastPathComponent] unsignedIntValue];
718 destname = [self imap4FolderNameForURL:_desturl];
720 /* select source folder */
722 if (![self selectFolder:srcname])
723 return [self errorCouldNotSelectURL:_srcurl];
727 result = [[self client] copyUid:uid toFolder:destname];
728 if (![[result valueForKey:@"result"] boolValue])
729 return [self errorForResult:result text:@"Copy operation failed"];
731 // TODO: need to flush some caches?
736 /* managing folders */
738 - (BOOL)doesMailboxExistAtURL:(NSURL *)_url {
739 NSString *folderName;
742 /* check in hierarchy cache */
744 if ((result = [self cachedHierarchyResults]) != nil) {
747 result = [(NSDictionary *)result objectForKey:@"list"];
750 /* normalized results already have the / in front on libFoundation?! */
751 if ([p hasPrefix:@"/"])
752 p = [p substringFromIndex:1];
754 return ([(NSDictionary *)result objectForKey:p] != nil) ? YES : NO;
757 /* check using IMAP4 select */
758 // TODO: we should probably just fetch the whole hierarchy?
760 folderName = [self imap4FolderNameForURL:_url];
761 result = [[self client] select:folderName];
762 if (![[result valueForKey:@"result"] boolValue])
768 - (id)infoForMailboxAtURL:(NSURL *)_url {
769 NGImap4MailboxInfo *info;
770 NSString *folderName;
773 folderName = [self imap4FolderNameForURL:_url];
774 result = [[self client] select:folderName];
775 if (![[result valueForKey:@"result"] boolValue])
776 return [self errorCouldNotSelectURL:_url];
778 info = [[NGImap4MailboxInfo alloc] initWithURL:_url folderName:folderName
779 selectDictionary:result];
780 return [info autorelease];
783 - (NSException *)createMailbox:(NSString *)_mailbox atURL:(NSURL *)_url {
789 newPath = [self imap4FolderNameForURL:_url];
790 newPath = [newPath stringByAppendingString:[self imap4Separator]];
791 newPath = [newPath stringByAppendingString:_mailbox];
795 result = [[self client] create:newPath];
796 if (![[result valueForKey:@"result"] boolValue])
797 return [self errorForResult:result text:@"Failed to create folder"];
799 [self flushFolderHierarchyCache];
800 // [self debugWithFormat:@"created mailbox: %@: %@", newPath, result];
804 - (NSException *)deleteMailboxAtURL:(NSURL *)_url {
810 path = [self imap4FolderNameForURL:_url];
811 result = [[self client] delete:path];
812 if (![[result valueForKey:@"result"] boolValue])
813 return [self errorForResult:result text:@"Failed to delete folder"];
815 [self flushFolderHierarchyCache];
817 [self debugWithFormat:@"delete mailbox %@: %@", _url, result];
822 - (NSException *)moveMailboxAtURL:(NSURL *)_srcurl toURL:(NSURL *)_desturl {
823 NSString *srcname, *destname;
828 srcname = [self imap4FolderNameForURL:_srcurl];
829 destname = [self imap4FolderNameForURL:_desturl];
831 result = [[self client] rename:srcname to:destname];
832 if (![[result valueForKey:@"result"] boolValue])
833 return [self errorForResult:result text:@"Failed to move folder"];
835 [self flushFolderHierarchyCache];
837 [self debugWithFormat:@"renamed mailbox %@: %@", _srcurl, result];
844 - (NSDictionary *)aclForMailboxAtURL:(NSURL *)_url {
846 Returns a mapping of uid => permission strings, eg:
850 NSString *folderName;
853 folderName = [self imap4FolderNameForURL:_url];
854 result = [[self client] getACL:folderName];
855 if (![[result valueForKey:@"result"] boolValue]) {
856 return (id)[self errorForResult:result
857 text:@"Failed to get ACL of folder"];
860 return [result valueForKey:@"acl"];
863 - (NSString *)myRightsForMailboxAtURL:(NSURL *)_url {
864 NSString *folderName;
869 if ((result = [self cachedMyRightsForURL:_url]) != nil)
874 folderName = [self imap4FolderNameForURL:_url];
875 result = [[self client] myRights:folderName];
876 if (![[result valueForKey:@"result"] boolValue]) {
877 return (id)[self errorForResult:result
878 text:@"Failed to get myrights on folder"];
883 if ((result = [result valueForKey:@"myrights"]) != nil)
884 [self cacheMyRights:result forURL:_url];
890 - (NSString *)description {
893 ms = [NSMutableString stringWithCapacity:128];
894 [ms appendFormat:@"<0x%p[%@]:", self, NSStringFromClass([self class])];
896 [ms appendFormat:@" client=0x%p", self->client];
897 if ([self->password isNotEmpty])
898 [ms appendString:@" pwd"];
900 [ms appendFormat:@" created=%@", self->creationTime];
902 if (self->subfolders != nil)
903 [ms appendFormat:@" #cached-folders=%d", [self->subfolders count]];
905 if (self->cachedUIDs != nil)
906 [ms appendFormat:@" #cached-uids=%d", [self->cachedUIDs count]];
908 [ms appendString:@">"];
912 @end /* NGImap4Connection */