2 Copyright (C) 2000-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
24 #include "NGImap4Client.h"
25 #include "NGImap4Context.h"
26 #include "NGImap4Support.h"
27 #include "NGImap4Functions.h"
28 #include "NGImap4ResponseParser.h"
29 #include "NGImap4ResponseNormalizer.h"
30 #include "NGImap4ServerGlobalID.h"
31 #include "NSString+Imap4.h"
34 #include "imTimeMacros.h"
36 @interface EOQualifier(IMAPAdditions)
37 - (NSString *)imap4SearchString;
40 @interface NGImap4Client(ConnectionRegistration)
42 - (void)removeFromConnectionRegister;
43 - (void)registerConnection;
44 - (NGCTextStream *)textStream;
46 @end /* NGImap4Client(ConnectionRegistration); */
48 #if GNUSTEP_BASE_LIBRARY
49 /* FIXME: TODO: move someplace better (hh: NGExtensions...) */
50 @implementation NSException(setUserInfo)
52 - (id)setUserInfo:(NSDictionary *)_userInfo {
53 ASSIGN(self->_e_info, _userInfo);
57 @end /* NSException(setUserInfo) */
60 @interface NGImap4Client(Private)
62 - (NSString *)_folder2ImapFolder:(NSString *)_folder;
64 - (NGHashMap *)processCommand:(NSString *)_command;
65 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag;
66 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
67 withNotification:(BOOL)_notification;
68 - (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt;
70 - (void)sendCommand:(NSString *)_command;
71 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag;
72 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag
73 logText:(NSString *)_txt;
75 - (void)sendResponseNotification:(NGHashMap *)map;
77 - (NSDictionary *)login;
82 An implementation of an Imap4 client
84 A folder name always looks like an absolute filename (/inbox/doof)
87 @implementation NGImap4Client
90 static inline NSArray *_flags2ImapFlags(NGImap4Client *, NSArray *);
92 static NSNumber *YesNumber = nil;
93 static NSNumber *NoNumber = nil;
95 static id *ImapClients = NULL;
96 static unsigned int CountClient = 0;
97 static unsigned int MaxImapClients = 0;
98 static int ProfileImapEnabled = -1;
99 static int LogImapEnabled = -1;
100 static int PreventExceptions = -1;
101 static NSArray *AllowedSortKeys = nil;
102 static BOOL fetchDebug = NO;
103 static BOOL ImapDebugEnabled = NO;
113 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
114 static BOOL didInit = NO;
118 PreventExceptions = [ud boolForKey:@"ImapPreventConnectionExceptions"]?1:0;
119 LogImapEnabled = [ud boolForKey:@"ImapLogEnabled"]?1:0;
120 ProfileImapEnabled = [ud boolForKey:@"ProfileImapEnabled"]?1:0;
121 ImapDebugEnabled = [ud boolForKey:@"ImapDebugEnabled"];
123 YesNumber = [[NSNumber numberWithBool:YES] retain];
124 NoNumber = [[NSNumber numberWithBool:NO] retain];
126 if (MaxImapClients < 1) {
127 MaxImapClients = [ud integerForKey:@"NGImapMaxConnectionCount"];
128 if (MaxImapClients < 1) MaxImapClients = 50;
130 if (ImapClients == NULL)
131 ImapClients = calloc(MaxImapClients + 2, sizeof(id));
133 AllowedSortKeys = [[NSArray alloc] initWithObjects:
134 @"ARRIVAL", @"CC", @"DATE", @"FROM",
135 @"SIZE", @"SUBJECT", @"TO", nil];
140 + (id)clientWithURL:(NSURL *)_url {
141 return [[(NGImap4Client *)[self alloc] initWithURL:_url] autorelease];
143 + (id)clientWithAddress:(id<NGSocketAddress>)_address {
145 [[(NGImap4Client *)[self alloc] initWithAddress:_address] autorelease];
148 + (id)clientWithHost:(id)_host {
149 return [[[self alloc] initWithHost:_host] autorelease];
152 - (id)initWithHost:(id)_host {
153 NGInternetSocketAddress *a;
155 a = [NGInternetSocketAddress addressWithPort:143 onHost:_host];
156 return [self initWithAddress:a];
158 - (id)initWithURL:(NSURL *)_url {
159 NGInternetSocketAddress *a;
163 if ((self->useSSL = [[_url scheme] isEqualToString:@"imaps"])) {
164 if (NSClassFromString(@"NGActiveSSLSocket") == nil) {
166 @"no SSL support available, cannot connect: %@", _url];
171 if ((tmp = [_url port])) {
172 port = [tmp intValue];
173 if (port <= 0) port = self->useSSL ? 993 : 143;
176 port = self->useSSL ? 993 : 143;
178 self->login = [[_url user] copy];
179 self->password = [[_url password] copy];
181 a = [NGInternetSocketAddress addressWithPort:port onHost:[_url host]];
182 return [self initWithAddress:a];
185 - (id)initWithAddress:(id<NGSocketAddress>)_address { /* designated init */
186 if ((self = [super init])) {
187 self->address = [_address retain];
188 self->debug = ImapDebugEnabled;
189 self->responseReceiver = [[NSMutableArray alloc] initWithCapacity:128];
190 self->normer = [[NGImap4ResponseNormalizer alloc] initWithClient:self];
196 [self removeFromConnectionRegister];
197 [self->normer release];
198 [self->text release];
199 [self->address release];
200 [self->socket release];
201 [self->parser release];
202 [self->responseReceiver release];
203 [self->login release];
204 [self->password release];
205 [self->selectedFolder release];
206 [self->delimiter release];
207 [self->serverGID release];
209 self->context = nil; /* not retained */
213 /* equality (required for adding clients to Foundation sets) */
215 - (BOOL)isEqual:(id)_obj {
219 if ([_obj isKindOfClass:[NGImap4Client class]])
220 return [self isEqualToClient:_obj];
225 - (BOOL)isEqualToClient:(NGImap4Client *)_obj {
226 if (_obj == self) return YES;
227 if (_obj == nil) return NO;
229 return [[_obj address] isEqual:self->address];
234 - (id<NGActiveSocket>)socket {
238 - (id<NGSocketAddress>)address {
239 return self->address;
242 - (NSString *)delimiter {
243 return self->delimiter;
246 - (EOGlobalID *)serverGlobalID {
247 NGInternetSocketAddress *is;
250 return self->serverGID;
252 is = (id)[self address];
254 self->serverGID = [[NGImap4ServerGlobalID alloc]
255 initWithHostname:[is hostName]
258 return self->serverGID;
264 Class socketClass = Nil;
267 socketClass = [self useSSL]
268 ? NSClassFromString(@"NGActiveSSLSocket")
269 : [NGActiveSocket class];
272 sock = [socketClass socketConnectedToAddress:self->address];
275 [self->context setLastException:localException];
283 - (NSDictionary *)_receiveServerGreetingWithoutTagId {
284 NSDictionary *res = nil;
290 hm = [self->parser parseResponseForTagId:-1 exception:&e];
292 res = [self->normer normalizeOpenConnectionResponse:hm];
295 [self->context setLastException:localException];
298 if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
304 - (NSDictionary *)_openConnection {
305 /* open connection as configured */
306 NGBufferedStream *buffer;
310 if (ProfileImapEnabled == 1) {
311 gettimeofday(&tv, NULL);
312 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
314 [self->socket release]; self->socket = nil;
315 [self->parser release]; self->parser = nil;
316 [self->text release]; self->text = nil;
318 [self->context resetLastException];
320 if ((self->socket = [[self _openSocket] retain]) == nil)
322 if ([self->context lastException])
326 [(NGBufferedStream *)[NGBufferedStream alloc] initWithSource:self->socket];
327 self->text = [(NGCTextStream *)[NGCTextStream alloc] initWithSource:buffer];
328 [buffer release]; buffer = nil;
330 self->parser = [[NGImap4ResponseParser alloc] initWithStream:self->socket];
333 if (ProfileImapEnabled == 1) {
334 gettimeofday(&tv, NULL);
335 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
336 fprintf(stderr, "[%s] <openConnection> : time needed: %4.4fs\n",
337 __PRETTY_FUNCTION__, ti < 0.0 ? -1.0 : ti);
339 [self registerConnection];
340 [self->context resetLastException];
342 return [self _receiveServerGreetingWithoutTagId];
345 - (NSDictionary *)openConnection {
346 return [self _openConnection];
349 - (NSNumber *)isConnected {
351 Check whether stream is already open (could be closed because
354 return (self->socket == nil)
356 : ([(NGActiveSocket *)self->socket isAlive] ? YesNumber : NoNumber);
359 - (NSException *)_handleTextReleaseException:(NSException *)_ex {
360 [self logWithFormat:@"got exception during stream dealloc: %@", _ex];
363 - (NSException *)_handleSocketCloseException:(NSException *)_ex {
364 [self logWithFormat:@"got exception during socket close: %@", _ex];
367 - (NSException *)_handleSocketReleaseException:(NSException *)_ex {
368 [self logWithFormat:@"got exception during socket deallocation: %@", _ex];
371 - (void)closeConnection {
372 /* close a connection */
374 // TODO: this is a bit weird, probably because of the flush
375 // maybe just call -close on the text stream?
377 [self->text release];
379 [[self _handleTextReleaseException:localException] raise];
384 [self->socket close];
386 [[self _handleSocketCloseException:localException] raise];
390 [self->socket release];
392 [[self _handleSocketReleaseException:localException] raise];
396 [self->parser release]; self->parser = nil;
397 [self->delimiter release]; self->delimiter = nil;
398 [self removeFromConnectionRegister];
401 // ResponseNotifications
403 - (void)registerForResponseNotification:(id<NGImap4ResponseReceiver>)_obj {
404 [self->responseReceiver addObject:[NSValue valueWithNonretainedObject:_obj]];
407 - (void)removeFromResponseNotification:(id<NGImap4ResponseReceiver>)_obj {
408 [self->responseReceiver removeObject:
409 [NSValue valueWithNonretainedObject:_obj]];
412 - (void)sendResponseNotification:(NGHashMap *)_map {
414 id<NGImap4ResponseReceiver> obj;
415 NSEnumerator *enumerator;
418 resp = [self->normer normalizeResponse:_map];
419 enumerator = [self->responseReceiver objectEnumerator];
421 while ((val = [enumerator nextObject])) {
422 obj = [val nonretainedObjectValue];
423 [obj responseNotificationFrom:self response:resp];
429 - (NSDictionary *)login:(NSString *)_login password:(NSString *)_passwd {
430 /* login with plaintext password authenticating */
432 if ((_login == nil) || (_passwd == nil))
435 [self->login release]; self->login = nil;
436 [self->password release]; self->password = nil;
437 [self->serverGID release]; self->serverGID = nil;
439 self->login = [_login copy];
440 self->password = [[_passwd stringByEscapingImap4Password] copy];
446 if ([self->context lastException] != nil)
449 [self closeConnection];
451 [self openConnection];
453 if ([self->context lastException] != nil)
459 - (NSDictionary *)login {
468 s = [NSString stringWithFormat:@"login \"%@\" \"%@\"",
469 self->login, self->password];
470 log = [NSString stringWithFormat:@"login %@ <%@>",
472 (self->password != nil) ? @"PASSWORD" : @"NO PASSWORD"];
473 map = [self processCommand:s logText:log];
475 if (self->selectedFolder)
476 [self select:self->selectedFolder];
480 return [self->normer normalizeResponse:map];
483 - (NSDictionary *)logout {
484 /* logout from the connected host and close the connection */
487 map = [self processCommand:@"logout"];
488 [self closeConnection];
490 return [self->normer normalizeResponse:map];
493 /* Authenticated State */
495 - (NSDictionary *)list:(NSString *)_folder pattern:(NSString *)_pattern {
497 The method build statements like 'LIST "_folder" "_pattern"'.
498 The Cyrus IMAP4 v1.5.14 server ignores the given folder.
499 Instead of you should use the pattern to get the expected result.
500 If folder is NIL it would be set to empty string ''.
501 If pattern is NIL it would be set to ''.
503 NSAutoreleasePool *pool;
505 NSDictionary *result;
508 pool = [[NSAutoreleasePool alloc] init];
510 if (_folder == nil) _folder = @"";
511 if (_pattern == nil) _pattern = @"";
513 if ([_folder length] > 0) {
514 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
519 if ([_pattern length] > 0)
520 if (!(_pattern = [self _folder2ImapFolder:_pattern]))
523 s = [NSString stringWithFormat:@"list \"%@\" \"%@\"", _folder, _pattern];
524 map = [self processCommand:s];
526 if (self->delimiter == nil) {
529 rdel = [[map objectEnumeratorForKey:@"list"] nextObject];
530 self->delimiter = [[rdel objectForKey:@"delimiter"] copy];
533 result = [[self->normer normalizeListResponse:map] copy];
535 return [result autorelease];
538 - (NSDictionary *)capability {
540 capres = [self processCommand:@"capability"];
541 return [self->normer normalizeCapabilityRespone:capres];
544 - (NSDictionary *)lsub:(NSString *)_folder pattern:(NSString *)_pattern {
546 The method build statements like 'LSUB "_folder" "_pattern"'.
547 The returnvalue is the same like the list:pattern: method
555 if ([_folder length] > 0) {
556 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
562 if ([_pattern length] > 0) {
563 if ((_pattern = [self _folder2ImapFolder:_pattern]) == nil)
567 s = [NSString stringWithFormat:@"lsub \"%@\" \"%@\"", _folder, _pattern];
568 map = [self processCommand:s];
570 if (self->delimiter == nil) {
573 rdel = [[map objectEnumeratorForKey:@"LIST"] nextObject];
574 self->delimiter = [[rdel objectForKey:@"delimiter"] copy];
576 return [self->normer normalizeListResponse:map];
579 - (NSDictionary *)select:(NSString *)_folder {
583 tmp = self->selectedFolder;
585 if ([_folder length] == 0)
587 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
590 self->selectedFolder = [_folder copy];
592 [tmp release]; tmp = nil;
594 s = [NSString stringWithFormat:@"select \"%@\"", self->selectedFolder];
595 return [self->normer normalizeSelectResponse:[self processCommand:s]];
598 - (NSDictionary *)status:(NSString *)_folder flags:(NSArray *)_flags {
603 if ((_flags == nil) || ([_flags count] == 0))
605 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
608 cmd = [NSString stringWithFormat:@"status \"%@\" (%@)",
609 _folder, [_flags componentsJoinedByString:@" "]];
610 return [self->normer normalizeStatusResponse:[self processCommand:cmd]];
613 - (NSDictionary *)noop {
615 return [self->normer normalizeResponse:[self processCommand:@"noop"]];
618 - (NSDictionary *)rename:(NSString *)_folder to:(NSString *)_newName {
621 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
623 if ((_newName = [self _folder2ImapFolder:_newName]) == nil)
626 cmd = [NSString stringWithFormat:@"rename \"%@\" \"%@\"", _folder, _newName];
628 return [self->normer normalizeResponse:[self processCommand:cmd]];
631 - (NSDictionary *)_performCommand:(NSString *)_op onFolder:(NSString *)_fname {
634 if ((_fname = [self _folder2ImapFolder:_fname]) == nil)
637 // eg: 'delete "blah"'
638 command = [NSString stringWithFormat:@"%@ \"%@\"", _op, _fname];
640 return [self->normer normalizeResponse:[self processCommand:command]];
643 - (NSDictionary *)delete:(NSString *)_name {
644 return [self _performCommand:@"delete" onFolder:_name];
646 - (NSDictionary *)create:(NSString *)_name {
647 return [self _performCommand:@"create" onFolder:_name];
649 - (NSDictionary *)subscribe:(NSString *)_name {
650 return [self _performCommand:@"subscribe" onFolder:_name];
652 - (NSDictionary *)unsubscribe:(NSString *)_name {
653 return [self _performCommand:@"unsubscribe" onFolder:_name];
656 - (NSDictionary *)expunge {
657 return [self->normer normalizeResponse:[self processCommand:@"expunge"]];
660 - (NSString *)_uidsJoinedForFetchCmd:(NSArray *)_uids {
661 return [_uids componentsJoinedByString:@","];
663 - (NSString *)_partsJoinedForFetchCmd:(NSArray *)_parts {
664 return [_parts componentsJoinedByString:@" "];
667 - (NSDictionary *)fetchUids:(NSArray *)_uids parts:(NSArray *)_parts {
668 NSAutoreleasePool *pool;
670 NSDictionary *result;
673 pool = [[NSAutoreleasePool alloc] init];
674 cmd = [NSString stringWithFormat:@"uid fetch %@ (%@)",
675 [self _uidsJoinedForFetchCmd:_uids],
676 [self _partsJoinedForFetchCmd:_parts]];
678 fetchres = [self processCommand:cmd];
679 result = [[self->normer normalizeFetchResponse:fetchres] retain];
682 return [result autorelease];
685 - (NSDictionary *)fetchUid:(unsigned)_uid parts:(NSArray *)_parts {
686 // TODO: describe what exactly this can return!
687 NSAutoreleasePool *pool;
689 NSDictionary *result;
692 pool = [[NSAutoreleasePool alloc] init];
693 cmd = [NSString stringWithFormat:@"uid fetch %d (%@)", _uid,
694 [self _partsJoinedForFetchCmd:_parts]];
695 fetchres = [self processCommand:cmd];
696 result = [[self->normer normalizeFetchResponse:fetchres] retain];
699 return [result autorelease];
702 - (NSDictionary *)fetchFrom:(unsigned)_from to:(unsigned)_to
703 parts:(NSArray *)_parts
706 NSAutoreleasePool *pool;
707 NSMutableString *cmd;
708 NSDictionary *result;
709 NGHashMap *rawResult;
717 pool = [[NSAutoreleasePool alloc] init];
721 cmd = [NSMutableString stringWithCapacity:256];
722 [cmd appendString:@"fetch "];
723 [cmd appendFormat:@"%d:%d (", _from, _to];
724 for (i = 0, count = [_parts count]; i < count; i++) {
725 if (i != 0) [cmd appendString:@" "];
726 [cmd appendString:[_parts objectAtIndex:i]];
728 [cmd appendString:@")"];
730 if (fetchDebug) NSLog(@"%s: process: %@", __PRETTY_FUNCTION__, cmd);
731 rawResult = [self processCommand:cmd];
733 RawResult is a dict containing keys:
734 ResponseResult: dict eg: {descripted=Completed;result=ok;tagId=8;}
735 fetch: array of record dicts (eg "rfc822.header" key)
738 if (fetchDebug) NSLog(@"%s: normalize: %@", __PRETTY_FUNCTION__,rawResult);
739 result = [[self->normer normalizeFetchResponse:rawResult] retain];
740 if (fetchDebug) NSLog(@"%s: normalized: %@", __PRETTY_FUNCTION__, result);
743 if (fetchDebug) NSLog(@"%s: pool done.", __PRETTY_FUNCTION__);
744 return [result autorelease];
747 - (NSDictionary *)storeUid:(unsigned)_uid add:(NSNumber *)_add
748 flags:(NSArray *)_flags
750 NSString *icmd, *iflags;
752 iflags = [_flags2ImapFlags(self, _flags) componentsJoinedByString:@" "];
753 icmd = [NSString stringWithFormat:@"uid store %d %cFLAGS (%@)",
754 _uid, [_add boolValue] ? '+' : '-',
756 return [self->normer normalizeResponse:[self processCommand:icmd]];
759 - (NSDictionary *)storeFrom:(unsigned)_from to:(unsigned)_to
761 flags:(NSArray *)_flags
771 flagstr = [_flags2ImapFlags(self, _flags) componentsJoinedByString:@" "];
772 cmd = [NSString stringWithFormat:@"store %d:%d %cFLAGS (%@)",
773 _from, _to, [_add boolValue] ? '+' : '-', flagstr];
775 return [self->normer normalizeResponse:[self processCommand:cmd]];
778 - (NSDictionary *)copyFrom:(unsigned)_from to:(unsigned)_to
779 toFolder:(NSString *)_folder
787 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
790 cmd = [NSString stringWithFormat:@"copy %d:%d \"%@\"", _from, _to, _folder];
791 return [self->normer normalizeResponse:[self processCommand:cmd]];
794 - (NSDictionary *)copyUid:(unsigned)_uid toFolder:(NSString *)_folder {
797 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
800 cmd = [NSString stringWithFormat:@"uid copy %d \"%@\"", _uid, _folder];
802 return [self->normer normalizeResponse:[self processCommand:cmd]];
805 - (NSDictionary *)getQuotaRoot:(NSString *)_folder {
808 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
811 cmd = [NSString stringWithFormat:@"getquotaroot \"%@\"", _folder];
812 return [self->normer normalizeQuotaResponse:[self processCommand:cmd]];
815 - (NSDictionary *)append:(NSData *)_message toFolder:(NSString *)_folder
816 withFlags:(NSArray *)_flags
820 NSString *message, *icmd;
822 flags = _flags2ImapFlags(self, _flags);
823 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
827 /* Remove bare newlines */
835 old = [_message bytes];
836 len = [_message length];
838 new = calloc(len * 2 + 4, sizeof(char));
840 while (cntOld < (len - 1)) {
841 if (old[cntOld] == '\n') {
842 new[cntNew++] = '\r';
843 new[cntNew++] = '\n';
845 else if (old[cntOld] != '\r') {
846 new[cntNew++] = old[cntOld];
850 if (old[cntOld] == '\n') {
851 new[cntNew++] = '\r';
852 new[cntNew++] = '\n';
854 else if (old[cntOld] != '\r') {
855 new[cntNew++] = old[cntOld];
857 message = [(NSString *)[NSString alloc]
858 initWithCString:new length:cntNew];
859 if (new) free(new); new = NULL;
862 icmd = [NSString stringWithFormat:@"append \"%@\" (%@) {%d}",
864 [flags componentsJoinedByString:@" "],
865 [message cStringLength]];
866 result = [self processCommand:icmd
867 withTag:YES withNotification:NO];
869 if ([[result objectForKey:@"ContinuationResponse"] boolValue])
870 result = [self processCommand:message withTag:NO];
872 [message release]; message = nil;
874 return [self->normer normalizeResponse:result];
877 - (void)_handleSearchExprIssue:(NSString *)reason qualifier:(EOQualifier *)_q {
879 NSException *exception = nil;
882 if (PreventExceptions != 0)
885 if (_q == nil) _q = (id)[NSNull null];
887 descr = @"Could not process qualifier for imap search ";
888 descr = [descr stringByAppendingString:reason];
890 exception = [[NGImap4SearchException alloc] initWithFormat:@"%@", descr];
891 ui = [NSDictionary dictionaryWithObject:_q forKey:@"qualifier"];
892 [exception setUserInfo:ui];
893 [self->context setLastException:exception];
897 - (NSString *)_searchExprForQual:(EOQualifier *)_qualifier {
900 if (_qualifier == nil)
903 result = [_qualifier imap4SearchString];
904 if ([result isKindOfClass:[NSException class]]) {
905 [self _handleSearchExprIssue:[(NSException *)result reason]
906 qualifier:_qualifier];
912 /* siehe draft-ietf-imapext-thread-12.txt
913 returns an array of uids in sort order */
915 - (NSDictionary *)threadBySubject:(BOOL)_bySubject
916 charset:(NSString *)_charSet
922 threadAlg = @"REFERENCES";
924 threadAlg = @"ORDEREDSUBJECT";
927 if (![_charSet length])
930 threadStr = [NSString stringWithFormat:@"UID THREAD %@ %@ ALL",
931 threadAlg, _charSet];
933 return [self->normer normalizeThreadResponse:
934 [self processCommand:threadStr]];
937 - (NSDictionary *)sort:(NSArray *)_sortOrderings
938 qualifier:(EOQualifier *)_qual
940 /* siehe draft-ietf-imapext-sort */
941 /* returns an array of uids in sort order */
942 NSMutableString *sortStr;
943 NSEnumerator *enumerator;
947 sortStr = [NSMutableString stringWithCapacity:128];
949 if ([_sortOrderings count] == 0)
950 return [NSDictionary dictionaryWithObjectsAndKeys:@"result", NoNumber,nil];
952 enumerator = [_sortOrderings objectEnumerator];
954 [sortStr appendString:@"UID SORT ("];
956 while ((so = [enumerator nextObject])) {
960 key = [[so key] uppercaseString];
962 if ([key length] == 0)
965 if (![AllowedSortKeys containsObject:key]) {
966 [self logWithFormat:@"ERROR[%s] key %@ is not allowed here!",
967 __PRETTY_FUNCTION__, key];
973 [sortStr appendString:@" "];
976 if (sel_eq(sel, EOCompareDescending) ||
977 sel_eq(sel, EOCompareCaseInsensitiveDescending)) {
978 [sortStr appendString:@"REVERSE "];
980 [sortStr appendString:[so key]];
982 if (isFirst) { /* found no valid key use date sorting */
983 [sortStr appendString:@"DATE"];
985 return [NSDictionary dictionaryWithObjectsAndKeys:
986 NoNumber, @"result", nil];
989 [sortStr appendString:@") "];
990 // TODO: make that the other way around! should be an ivar in the client
991 [sortStr appendString:[[self context] sortEncoding]];
992 [sortStr appendString:[self _searchExprForQual:_qual]];
994 return [self->normer normalizeSortResponse:[self processCommand:sortStr]];
997 - (NSDictionary *)searchWithQualifier:(EOQualifier *)_qualifier {
1000 s = [self _searchExprForQual:_qualifier];
1001 if ([s length] == 0) {
1002 // TODO: should set last-exception?
1003 [self logWithFormat:@"ERROR(%s): could not process search qualifier: %@",
1004 __PRETTY_FUNCTION__, _qualifier];
1008 s = [@"search" stringByAppendingString:s];
1009 return [self->normer normalizeSearchResponse:[self processCommand:s]];
1012 /* Private Methods */
1014 - (NSException *)_processCommandParserException:(NSException *)_exception {
1015 NSLog(@"ERROR(%s): catched IMAP4 parser exception %@: %@",
1016 __PRETTY_FUNCTION__, [_exception name], [_exception reason]);
1017 [self closeConnection];
1018 [self->context setLastException:_exception];
1021 - (NSException *)_processUnknownCommandParserException:(NSException *)_ex {
1022 NSLog(@"ERROR(%s): catched non-IMAP4 parsing exception %@: %@",
1023 __PRETTY_FUNCTION__, [_ex name], [_ex reason]);
1027 - (NSException *)_handleShutdownDuringCommandException:(NSException *)_ex {
1028 NSLog(@"ERROR(%s): IMAP4 socket was shut down by server %@: %@",
1029 __PRETTY_FUNCTION__, [_ex name], [_ex reason]);
1030 [self closeConnection];
1031 [self->context setLastException:_ex];
1035 - (BOOL)_isShutdownException:(NSException *)_ex {
1036 return [[_ex name] isEqualToString:@"NGSocketShutdownDuringReadException"];
1039 - (BOOL)_isLoginCommand:(NSString *)_command {
1040 return [_command hasPrefix:@"login"];
1043 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
1044 withNotification:(BOOL)_notification logText:(NSString *)_txt
1049 NSException *exception;
1054 if (ProfileImapEnabled == 1) {
1055 gettimeofday(&tv, NULL);
1056 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
1057 fprintf(stderr, "{");
1066 [self->context resetLastException];
1068 NSException *e = nil; // TODO: try to remove exception handler
1070 [self sendCommand:_command withTag:_tag logText:_txt];
1071 map = [self->parser parseResponseForTagId:self->tagId exception:&e];
1076 if ([localException isKindOfClass:[NGImap4ParserException class]]) {
1077 [[self _processCommandParserException:localException] raise];
1079 else if ([self _isShutdownException:localException]) {
1080 [[self _handleShutdownDuringCommandException:localException] raise];
1083 [[self _processUnknownCommandParserException:localException] raise];
1084 if (reconnectCnt == 0) {
1085 if (![self _isLoginCommand:_command]) {
1088 exception = localException;
1091 [self closeConnection];
1092 [self->context setLastException:localException];
1100 else if ([map objectForKey:@"bye"] && ![_command hasPrefix:@"logout"]) {
1101 if (reconnectCnt == 0) {
1107 } while (tryReconnect);
1109 if ([self->context lastException]) {
1111 [self->context setLastException:exception];
1115 if (_notification) [self sendResponseNotification:map];
1117 if (ProfileImapEnabled == 1) {
1118 gettimeofday(&tv, NULL);
1119 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
1120 fprintf(stderr, "}[%s] <Send Command [%s]> : time needed: %4.4fs\n",
1121 __PRETTY_FUNCTION__, [_command cString], ti < 0.0 ? -1.0 : ti);
1126 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
1127 withNotification:(BOOL)_notification
1129 return [self processCommand:_command withTag:_tag
1130 withNotification:_notification
1134 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag {
1135 return [self processCommand:_command withTag:_tag withNotification:YES
1139 - (NGHashMap *)processCommand:(NSString *)_command {
1140 return [self processCommand:_command withTag:YES withNotification:YES
1144 - (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt {
1145 return [self processCommand:_command withTag:YES withNotification:YES
1149 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag
1150 logText:(NSString *)_txt
1153 NGCTextStream *txtStream;
1155 txtStream = [self textStream];
1160 command = [NSString stringWithFormat:@"%d %@", self->tagId, _command];
1162 _txt = [NSString stringWithFormat:@"%d %@", self->tagId, _txt];
1169 if ([_txt length] > 5000) {
1170 fprintf(stderr, "C[%p]: %s...\n", self, [[_txt substringToIndex:5000]
1174 fprintf(stderr, "C[%p]: %s\n", self, [_txt cString]);
1178 if (![txtStream writeString:command])
1179 [self->context setLastException:[txtStream lastException]];
1180 else if (![txtStream writeString:@"\r\n"])
1181 [self->context setLastException:[txtStream lastException]];
1182 else if (![txtStream flush])
1183 [self->context setLastException:[txtStream lastException]];
1186 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag {
1187 [self sendCommand:_command withTag:_tag logText:_command];
1190 - (void)sendCommand:(NSString *)_command {
1191 [self sendCommand:_command withTag:YES logText:_command];
1194 static inline NSArray *_flags2ImapFlags(NGImap4Client *self, NSArray *_flags) {
1195 NSEnumerator *enumerator;
1201 objs = calloc([_flags count] + 2, sizeof(id));
1203 enumerator = [_flags objectEnumerator];
1204 while ((obj = [enumerator nextObject])) {
1205 objs[cnt] = [@"\\" stringByAppendingString:obj];
1208 result = [NSArray arrayWithObjects:objs count:cnt];
1209 if (objs) free(objs);
1213 - (NSString *)_folder2ImapFolder:(NSString *)_folder {
1216 if (self->delimiter == nil) {
1219 res = [self list:@"" pattern:@""];
1221 if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
1225 array = [_folder pathComponents];
1227 if ([array count] > 0) {
1230 o = [array objectAtIndex:0];
1231 if (([o isEqualToString:@"/"]) || ([o length] == 0))
1232 array = [array subarrayWithRange:NSMakeRange(1, [array count] - 1)];
1234 o = [array lastObject];
1235 if (([o length] == 0) || ([o isEqualToString:@"/"]))
1236 array = [array subarrayWithRange:NSMakeRange(0, [array count] - 1)];
1238 return [[array componentsJoinedByString:self->delimiter]
1239 stringByEncodingImap4FolderName];
1242 - (NSString *)_imapFolder2Folder:(NSString *)_folder {
1245 array = [NSArray arrayWithObject:@""];
1247 if ([self delimiter] == nil) {
1250 res = [self list:@"" pattern:@""]; // fill the delimiter ivar?
1251 if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
1255 array = [array arrayByAddingObjectsFromArray:
1256 [_folder componentsSeparatedByString:[self delimiter]]];
1258 return [[NSString pathWithComponents:array] stringByDecodingImap4FolderName];
1261 - (void)setContext:(NGImap4Context *)_ctx {
1262 self->context = _ctx;
1264 - (NGImap4Context *)context {
1265 return self->context;
1268 /* ConnectionRegistration */
1270 - (void)removeFromConnectionRegister {
1273 for (cnt = 0; cnt < MaxImapClients; cnt++) {
1274 if (ImapClients[cnt] == self)
1275 ImapClients[cnt] = nil;
1279 - (void)registerConnection {
1282 cnt = CountClient % MaxImapClients;
1284 if (ImapClients[cnt]) {
1285 [(NGImap4Context *)ImapClients[cnt] closeConnection];
1287 ImapClients[cnt] = self;
1291 - (id<NGExtendedTextStream>)textStream {
1292 if (self->text == nil) {
1293 if ([self->context lastException] == nil)
1301 - (NSString *)description {
1302 NSMutableString *ms;
1304 ms = [NSMutableString stringWithCapacity:128];
1305 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
1306 [ms appendFormat:@" socket=%@", [self socket]];
1307 [ms appendString:@">"];
1311 @end /* NGImap4Client; */