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 "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 length] == 0)
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 {
163 ? [@"Could not select IMAP4 folder: " stringByAppendingString:
164 [_url absoluteString]]
165 : @"Could not select IMAP4 folder!";
167 ui = [NSDictionary dictionaryWithObjectsAndKeys:
168 [NSNumber numberWithInt:404], @"http-status",
172 return [NSException exceptionWithName:@"NGImap4Exception"
173 reason:r userInfo:ui];
176 - (NSException *)errorForResult:(NSDictionary *)_result text:(NSString *)_txt {
181 if ([[_result valueForKey:@"result"] boolValue])
182 return nil; /* everything went fine! */
184 if ((r = [_result valueForKey:@"reason"]) != nil)
185 r = [[_txt stringByAppendingString:@": "] stringByAppendingString:r];
189 if ([r isEqualToString:@"Permission denied"]) {
190 /* different for each server?, no error codes in IMAP4 ... */
191 status = 403 /* Forbidden */;
194 status = 500 /* internal server error */;
196 ui = [NSDictionary dictionaryWithObjectsAndKeys:
197 [NSNumber numberWithInt:status], @"http-status",
198 _result, @"rawResult",
201 return [NSException exceptionWithName:@"NGImap4Exception"
202 reason:r userInfo:ui];
205 /* IMAP4 path/url processing methods */
207 NSArray *SOGoMailGetDirectChildren(NSArray *_array, NSString *_fn) {
209 Scans string '_array' for strings which start with the string in '_fn'.
213 unsigned i, count, prefixlen;
215 if ((count = [_array count]) < 2)
216 /* one entry is the folder itself, so we need at least two */
217 return [NSArray array];
220 // TODO: somehow results are different on OSX
221 prefixlen = [_fn isEqualToString:@""] ? 0 : [_fn length] + 1;
223 prefixlen = [_fn isEqualToString:@"/"] ? 1 : [_fn length] + 1;
225 ma = [NSMutableArray arrayWithCapacity:count];
226 for (i = 0; i < count; i++) {
229 p = [_array objectAtIndex:i];
230 if ([p length] <= prefixlen)
232 if (prefixlen != 0 && ![p hasPrefix:_fn])
235 /* cut of common part */
236 p = [p substringFromIndex:prefixlen];
238 /* check whether the path is a sub-subfolder path */
239 if ([p rangeOfString:@"/"].length > 0)
245 [ma sortUsingSelector:@selector(compare:)];
249 - (NSArray *)extractSubfoldersForURL:(NSURL *)_url
250 fromResultSet:(NSDictionary *)_result
252 NSString *folderName;
253 NSDictionary *result;
257 /* Note: the result is normalized, that is, it contains / as the separator */
258 folderName = [_url path];
260 /* normalized results already have the / in front on libFoundation?! */
261 if ([folderName hasPrefix:@"/"])
262 folderName = [folderName substringFromIndex:1];
265 result = [_result valueForKey:@"list"];
267 /* Cyrus already tells us whether we need to check for children */
268 flags = [result objectForKey:folderName];
269 if ([flags containsObject:@"hasnochildren"]) {
271 NSLog(@"%s: folder %@ has no children.", __PRETTY_FUNCTION__,folderName);
276 NSLog(@"%s: all keys %@: %@", __PRETTY_FUNCTION__, folderName,
277 [[result allKeys] componentsJoinedByString:@", "]);
280 names = SOGoMailGetDirectChildren([result allKeys], folderName);
282 NSLog(@"%s: subfolders of '%@': %@", __PRETTY_FUNCTION__, folderName,
283 [names componentsJoinedByString:@","]);
288 - (NSString *)imap4Separator {
289 return self->separator;
292 - (NSString *)imap4FolderNameForURL:(NSURL *)_url removeFileName:(BOOL)_delfn {
293 /* a bit hackish, but should be OK */
294 NSString *folderName;
300 folderName = [_url path];
301 if ([folderName length] == 0)
303 if ([folderName characterAtIndex:0] == '/')
304 folderName = [folderName substringFromIndex:1];
306 if (_delfn) folderName = [folderName stringByDeletingLastPathComponent];
308 if ([[self imap4Separator] isEqualToString:@"/"])
311 names = [folderName pathComponents];
312 return [names componentsJoinedByString:[self imap4Separator]];
314 - (NSString *)imap4FolderNameForURL:(NSURL *)_url {
315 return [self imap4FolderNameForURL:_url removeFileName:NO];
318 - (NSArray *)extractFoldersFromResultSet:(NSDictionary *)_result {
319 /* Note: the result is normalized, that is, it contains / as the separator */
320 return [[_result valueForKey:@"list"] allKeys];
323 /* folder selections */
325 - (BOOL)selectFolder:(id)_url {
326 NSDictionary *result;
329 newFolder = [_url isKindOfClass:[NSURL class]]
330 ? [self imap4FolderNameForURL:_url]
334 if ([[[self client] selectedFolderName] isEqualToString:newFolder])
338 result = [[self client] select:newFolder];
339 if (![[result valueForKey:@"result"] boolValue]) {
340 [self errorWithFormat:@"could not select URL: %@: %@", _url, result];
347 - (BOOL)isPermissionDeniedResult:(id)_result {
348 if ([[_result valueForKey:@"result"] intValue] != 0)
351 return [[_result valueForKey:@"reason"]
352 isEqualToString:@"Permission denied"];
355 /* folder operations */
357 - (NSDictionary *)primaryFetchMailboxHierarchyForURL:(NSURL *)_url {
358 NSDictionary *result;
360 if ((result = [self cachedHierarchyResults]) != nil)
361 return [result isNotNull] ? result : nil;
363 if (debugCache) [self logWithFormat:@" no folders cached yet .."];
365 result = [[self client] list:(onlyFetchInbox ? @"INBOX" : @"*")
367 if (![[result valueForKey:@"result"] boolValue]) {
368 [self errorWithFormat:@"Could not list mailbox hierarchy!"];
374 if ([result isNotNull]) {
375 [self cacheHierarchyResults:result];
377 [self logWithFormat:@"cached results: 0x%08X(%d)",
378 result, [result count]];
384 - (NSArray *)subfoldersForURL:(NSURL *)_url {
385 NSDictionary *result;
387 if ((result = [self primaryFetchMailboxHierarchyForURL:_url]) == nil)
389 if ([result isKindOfClass:[NSException class]]) {
390 [self errorWithFormat:@"failed to retrieve hierarchy: %@", result];
394 return [self extractSubfoldersForURL:_url fromResultSet:result];
397 - (NSArray *)allFoldersForURL:(NSURL *)_url {
398 NSDictionary *result;
400 if ((result = [self primaryFetchMailboxHierarchyForURL:_url]) == nil)
402 if ([result isKindOfClass:[NSException class]]) {
403 [self errorWithFormat:@"failed to retrieve hierarchy: %@", result];
407 return [self extractFoldersFromResultSet:result];
410 /* message operations */
412 - (NSArray *)fetchUIDsInURL:(NSURL *)_url qualifier:(id)_qualifier
416 sortOrdering can be an NSString, an EOSortOrdering or an array of EOS.
418 NSDictionary *result;
423 uids = [self cachedUIDsForURL:_url qualifier:_qualifier sortOrdering:_so];
425 if (debugCache) [self logWithFormat:@"reusing uid cache!"];
426 return [uids isNotNull] ? uids : nil;
429 /* select folder and fetch */
431 if (![self selectFolder:_url])
434 result = [[self client] sort:_so qualifier:_qualifier encoding:@"UTF-8"];
435 if (![[result valueForKey:@"result"] boolValue]) {
436 [self errorWithFormat:@"could not sort contents of URL: %@", _url];
440 uids = [result valueForKey:@"sort"];
441 if (![uids isNotNull]) {
442 [self errorWithFormat:@"got no UIDs for URL: %@: %@", _url, result];
448 [self cacheUIDs:uids forURL:_url qualifier:_qualifier sortOrdering:_so];
452 - (NSArray *)fetchUIDs:(NSArray *)_uids inURL:(NSURL *)_url
453 parts:(NSArray *)_parts
455 // currently returns a dict?!
459 BODY.PEEK[<section>]<<partial>>
460 BODY [this is the bodystructure, supported]
461 BODYSTRUCTURE [not supported yet!]
462 ENVELOPE [this is a parsed header, but does not include type]
470 NSDictionary *result;
474 if ([_uids count] == 0)
475 return nil; // TODO: might break empty folders?! return a dict!
479 if (![self selectFolder:_url])
484 // TODO: split uids into batches, otherwise Cyrus will complain
485 // => not really important because we batch before (in the sort)
486 // if the list is too long, we get a:
487 // "* BYE Fatal error: word too long"
489 result = [[self client] fetchUids:_uids parts:_parts];
490 if (![[result valueForKey:@"result"] boolValue]) {
491 [self errorWithFormat:@"could not fetch %d uids for url: %@",
496 //[self logWithFormat:@"RESULT: %@", result];
500 - (id)fetchURL:(NSURL *)_url parts:(NSArray *)_parts {
501 // currently returns a dict
502 NSDictionary *result;
505 if (![_url isNotNull]) return nil;
509 uid = [self imap4FolderNameForURL:_url removeFileName:YES];
510 if (![self selectFolder:uid])
515 uid = [[_url path] lastPathComponent];
517 result = [client fetchUids:[NSArray arrayWithObject:uid] parts:_parts];
518 if (![[result valueForKey:@"result"] boolValue]) {
519 [self errorWithFormat:@"could not fetch url: %@", _url];
522 //[self logWithFormat:@"RESULT: %@", result];
526 - (NSData *)fetchContentOfBodyPart:(NSString *)_partId atURL:(NSURL *)_url {
529 id result, fetch, body;
531 if (_partId == nil) return nil;
533 key = [@"body[" stringByAppendingString:_partId];
534 key = [key stringByAppendingString:@"]"];
535 parts = [NSArray arrayWithObjects:&key count:1];
539 result = [self fetchURL:_url parts:parts];
541 /* process results */
543 result = [(NSDictionary *)result objectForKey:@"fetch"];
544 if ([result count] == 0) { /* did not find part */
545 [self errorWithFormat:@"did not find part: %@", _partId];
549 fetch = [result objectAtIndex:0];
550 if ((body = [(NSDictionary *)fetch objectForKey:@"body"]) == nil) {
551 [self errorWithFormat:@"did not find body in response: %@", result];
555 if ((result = [(NSDictionary *)body objectForKey:@"data"]) == nil) {
556 [self errorWithFormat:@"did not find data in body: %@", fetch];
564 - (NSException *)addOrRemove:(BOOL)_flag flags:(id)_f toURL:(NSURL *)_url {
567 if (![_url isNotNull]) return nil;
568 if (![_f isNotNull]) return nil;
570 if (![_f isKindOfClass:[NSArray class]])
571 _f = [NSArray arrayWithObjects:&_f count:1];
575 result = [self imap4FolderNameForURL:_url removeFileName:YES];
576 if (![self selectFolder:result])
577 return [self errorCouldNotSelectURL:_url];
581 result = [[self client] storeUid:[[[_url path] lastPathComponent] intValue]
582 add:[NSNumber numberWithBool:_flag]
584 if (![[result valueForKey:@"result"] boolValue]) {
585 return [self errorForResult:result
586 text:@"Failed to change flags of IMAP4 message"];
588 /* result contains 'fetch' key with the current flags */
591 - (NSException *)addFlags:(id)_f toURL:(NSURL *)_u {
592 return [self addOrRemove:YES flags:_f toURL:_u];
594 - (NSException *)removeFlags:(id)_f toURL:(NSURL *)_u {
595 return [self addOrRemove:NO flags:_f toURL:_u];
598 - (NSException *)markURLDeleted:(NSURL *)_url {
599 return [self addOrRemove:YES flags:@"Deleted" toURL:_url];
602 - (NSException *)addFlags:(id)_f toAllMessagesInURL:(NSURL *)_url {
605 if (![_url isNotNull]) return nil;
606 if (![_f isNotNull]) return nil;
608 if (![_f isKindOfClass:[NSArray class]])
609 _f = [NSArray arrayWithObjects:&_f count:1];
613 if (![self selectFolder:[self imap4FolderNameForURL:_url]])
614 return [self errorCouldNotSelectURL:_url];
616 /* fetch all sequence numbers */
618 result = [client searchWithQualifier:nil /* means: ALL */];
619 if (![[result valueForKey:@"result"] boolValue]) {
620 return [self errorForResult:result
621 text:@"Could not search in IMAP4 folder"];
624 result = [result valueForKey:@"search"];
625 if ([result count] == 0) /* no messages in there, nothin' to be done */
630 result = [[self client] storeFlags:_f forMSNs:result addOrRemove:YES];
631 if (![[result valueForKey:@"result"] boolValue]) {
632 return [self errorForResult:result
633 text:@"Failed to change flags of IMAP4 message"];
639 /* posting new data */
641 - (NSException *)postData:(NSData *)_data flags:(id)_f
642 toFolderURL:(NSURL *)_url
646 if (![_url isNotNull]) return nil;
647 if (![_f isNotNull]) _f = [NSArray array];
649 if (![_f isKindOfClass:[NSArray class]])
650 _f = [NSArray arrayWithObjects:&_f count:1];
652 result = [[self client] append:_data
653 toFolder:[self imap4FolderNameForURL:_url]
655 if (![[result valueForKey:@"result"] boolValue])
656 return [self errorForResult:result text:@"Failed to store message"];
658 /* result contains 'fetch' key with the current flags */
660 // TODO: need to flush any caches?
666 - (NSException *)expungeAtURL:(NSURL *)_url {
672 p = [self imap4FolderNameForURL:_url removeFileName:NO];
673 if (![self selectFolder:p])
674 return [self errorCouldNotSelectURL:_url];
678 result = [[self client] expunge];
680 if (![[result valueForKey:@"result"] boolValue]) {
681 [self errorWithFormat:@"could not expunge url: %@", _url];
684 //[self logWithFormat:@"RESULT: %@", result];
688 /* copying and moving */
690 - (NSException *)copyMailURL:(NSURL *)_srcurl toFolderURL:(NSURL *)_desturl {
691 NSString *srcname, *destname;
697 srcname = [self imap4FolderNameForURL:_srcurl removeFileName:YES];
698 uid = [[[_srcurl path] lastPathComponent] unsignedIntValue];
699 destname = [self imap4FolderNameForURL:_desturl];
701 /* select source folder */
703 if (![self selectFolder:srcname])
704 return [self errorCouldNotSelectURL:_srcurl];
708 result = [[self client] copyUid:uid toFolder:destname];
709 if (![[result valueForKey:@"result"] boolValue])
710 return [self errorForResult:result text:@"Copy operation failed"];
712 // TODO: need to flush some caches?
717 /* managing folders */
719 - (BOOL)doesMailboxExistAtURL:(NSURL *)_url {
720 NSString *folderName;
723 /* check in hierarchy cache */
725 if ((result = [self cachedHierarchyResults]) != nil) {
728 result = [(NSDictionary *)result objectForKey:@"list"];
731 /* normalized results already have the / in front on libFoundation?! */
732 if ([p hasPrefix:@"/"])
733 p = [p substringFromIndex:1];
735 return ([(NSDictionary *)result objectForKey:p] != nil) ? YES : NO;
738 /* check using IMAP4 select */
739 // TODO: we should probably just fetch the whole hierarchy?
741 folderName = [self imap4FolderNameForURL:_url];
742 result = [[self client] select:folderName];
743 if (![[result valueForKey:@"result"] boolValue])
749 - (id)infoForMailboxAtURL:(NSURL *)_url {
750 NGImap4MailboxInfo *info;
751 NSString *folderName;
754 folderName = [self imap4FolderNameForURL:_url];
755 result = [[self client] select:folderName];
756 if (![[result valueForKey:@"result"] boolValue])
757 return [self errorCouldNotSelectURL:_url];
759 info = [[NGImap4MailboxInfo alloc] initWithURL:_url folderName:folderName
760 selectDictionary:result];
761 return [info autorelease];
764 - (NSException *)createMailbox:(NSString *)_mailbox atURL:(NSURL *)_url {
770 newPath = [self imap4FolderNameForURL:_url];
771 newPath = [newPath stringByAppendingString:[self imap4Separator]];
772 newPath = [newPath stringByAppendingString:_mailbox];
776 result = [[self client] create:newPath];
777 if (![[result valueForKey:@"result"] boolValue])
778 return [self errorForResult:result text:@"Failed to create folder"];
780 [self flushFolderHierarchyCache];
781 // [self debugWithFormat:@"created mailbox: %@: %@", newPath, result];
785 - (NSException *)deleteMailboxAtURL:(NSURL *)_url {
791 path = [self imap4FolderNameForURL:_url];
792 result = [[self client] delete:path];
793 if (![[result valueForKey:@"result"] boolValue])
794 return [self errorForResult:result text:@"Failed to delete folder"];
796 [self flushFolderHierarchyCache];
798 [self debugWithFormat:@"delete mailbox %@: %@", _url, result];
803 - (NSException *)moveMailboxAtURL:(NSURL *)_srcurl toURL:(NSURL *)_desturl {
804 NSString *srcname, *destname;
809 srcname = [self imap4FolderNameForURL:_srcurl];
810 destname = [self imap4FolderNameForURL:_desturl];
812 result = [[self client] rename:srcname to:destname];
813 if (![[result valueForKey:@"result"] boolValue])
814 return [self errorForResult:result text:@"Failed to move folder"];
816 [self flushFolderHierarchyCache];
818 [self debugWithFormat:@"renamed mailbox %@: %@", _srcurl, result];
825 - (NSDictionary *)aclForMailboxAtURL:(NSURL *)_url {
827 Returns a mapping of uid => permission strings, eg:
831 NSString *folderName;
834 folderName = [self imap4FolderNameForURL:_url];
835 result = [[self client] getACL:folderName];
836 if (![[result valueForKey:@"result"] boolValue]) {
837 return (id)[self errorForResult:result
838 text:@"Failed to get ACL of folder"];
841 return [result valueForKey:@"acl"];
844 - (NSString *)myRightsForMailboxAtURL:(NSURL *)_url {
845 NSString *folderName;
850 if ((result = [self cachedMyRightsForURL:_url]) != nil)
855 folderName = [self imap4FolderNameForURL:_url];
856 result = [[self client] myRights:folderName];
857 if (![[result valueForKey:@"result"] boolValue]) {
858 return (id)[self errorForResult:result
859 text:@"Failed to get myrights on folder"];
864 if ((result = [result valueForKey:@"myrights"]) != nil)
865 [self cacheMyRights:result forURL:_url];
871 - (NSString *)description {
874 ms = [NSMutableString stringWithCapacity:128];
875 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
877 [ms appendFormat:@" client=0x%08X", self->client];
878 if ([self->password length] > 0)
879 [ms appendString:@" pwd"];
881 [ms appendFormat:@" created=%@", self->creationTime];
883 if (self->subfolders != nil)
884 [ms appendFormat:@" #cached-folders=%d", [self->subfolders count]];
886 if (self->cachedUIDs != nil)
887 [ms appendFormat:@" #cached-uids=%d", [self->cachedUIDs count]];
889 [ms appendString:@">"];
893 @end /* NGImap4Connection */