2 Copyright (C) 2000-2005 SKYRIX Software AG
4 This file is part of SOPE.
6 SOPE 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 SOPE 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 SOPE; 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 {
350 // TODO: why is that an NSNummber?
352 Check whether stream is already open (could be closed because
355 return (self->socket == nil)
357 : ([(NGActiveSocket *)self->socket isAlive] ? YesNumber : NoNumber);
360 - (NSException *)_handleTextReleaseException:(NSException *)_ex {
361 [self logWithFormat:@"got exception during stream dealloc: %@", _ex];
364 - (NSException *)_handleSocketCloseException:(NSException *)_ex {
365 [self logWithFormat:@"got exception during socket close: %@", _ex];
368 - (NSException *)_handleSocketReleaseException:(NSException *)_ex {
369 [self logWithFormat:@"got exception during socket deallocation: %@", _ex];
372 - (void)closeConnection {
373 /* close a connection */
375 // TODO: this is a bit weird, probably because of the flush
376 // maybe just call -close on the text stream?
378 [self->text release];
380 [[self _handleTextReleaseException:localException] raise];
385 [self->socket close];
387 [[self _handleSocketCloseException:localException] raise];
391 [self->socket release];
393 [[self _handleSocketReleaseException:localException] raise];
397 [self->parser release]; self->parser = nil;
398 [self->delimiter release]; self->delimiter = nil;
399 [self removeFromConnectionRegister];
402 // ResponseNotifications
404 - (void)registerForResponseNotification:(id<NGImap4ResponseReceiver>)_obj {
405 [self->responseReceiver addObject:[NSValue valueWithNonretainedObject:_obj]];
408 - (void)removeFromResponseNotification:(id<NGImap4ResponseReceiver>)_obj {
409 [self->responseReceiver removeObject:
410 [NSValue valueWithNonretainedObject:_obj]];
413 - (void)sendResponseNotification:(NGHashMap *)_map {
415 id<NGImap4ResponseReceiver> obj;
416 NSEnumerator *enumerator;
419 resp = [self->normer normalizeResponse:_map];
420 enumerator = [self->responseReceiver objectEnumerator];
422 while ((val = [enumerator nextObject])) {
423 obj = [val nonretainedObjectValue];
424 [obj responseNotificationFrom:self response:resp];
430 - (NSDictionary *)login:(NSString *)_login password:(NSString *)_passwd {
431 /* login with plaintext password authenticating */
433 if ((_login == nil) || (_passwd == nil))
436 [self->login release]; self->login = nil;
437 [self->password release]; self->password = nil;
438 [self->serverGID release]; self->serverGID = nil;
440 self->login = [_login copy];
441 self->password = [[_passwd stringByEscapingImap4Password] copy];
447 if ([self->context lastException] != nil)
450 [self closeConnection];
452 [self openConnection];
454 if ([self->context lastException] != nil)
460 - (NSDictionary *)login {
462 On failure returns a dictionary with those keys:
463 'result' - a boolean => false
464 'reason' - reason why login failed
465 'RawResponse' - the raw IMAP4 response
467 'result' - a boolean => true
468 'expunge' - an array (containing what?)
469 'RawResponse' - the raw IMAP4 response
479 s = [NSString stringWithFormat:@"login \"%@\" \"%@\"",
480 self->login, self->password];
481 log = [NSString stringWithFormat:@"login %@ <%@>",
483 (self->password != nil) ? @"PASSWORD" : @"NO PASSWORD"];
484 map = [self processCommand:s logText:log];
486 if (self->selectedFolder != nil)
487 [self select:self->selectedFolder];
491 return [self->normer normalizeResponse:map];
494 - (NSDictionary *)logout {
495 /* logout from the connected host and close the connection */
498 map = [self processCommand:@"logout"];
499 [self closeConnection];
501 return [self->normer normalizeResponse:map];
504 /* Authenticated State */
506 - (NSDictionary *)list:(NSString *)_folder pattern:(NSString *)_pattern {
508 The method build statements like 'LIST "_folder" "_pattern"'.
509 The Cyrus IMAP4 v1.5.14 server ignores the given folder.
510 Instead of you should use the pattern to get the expected result.
511 If folder is NIL it would be set to empty string ''.
512 If pattern is NIL it would be set to ''.
514 The result dict contains the following keys:
516 'list' - a dictionary (key is folder name, value is flags)
517 'RawResponse' - the raw IMAP4 response
519 NSAutoreleasePool *pool;
521 NSDictionary *result;
524 pool = [[NSAutoreleasePool alloc] init];
526 if (_folder == nil) _folder = @"";
527 if (_pattern == nil) _pattern = @"";
529 if ([_folder length] > 0) {
530 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
535 if ([_pattern length] > 0)
536 if (!(_pattern = [self _folder2ImapFolder:_pattern]))
539 s = [NSString stringWithFormat:@"list \"%@\" \"%@\"", _folder, _pattern];
540 map = [self processCommand:s];
542 if (self->delimiter == nil) {
545 rdel = [[map objectEnumeratorForKey:@"list"] nextObject];
546 self->delimiter = [[rdel objectForKey:@"delimiter"] copy];
549 result = [[self->normer normalizeListResponse:map] copy];
551 return [result autorelease];
554 - (NSDictionary *)capability {
556 capres = [self processCommand:@"capability"];
557 return [self->normer normalizeCapabilityRespone:capres];
560 - (NSDictionary *)lsub:(NSString *)_folder pattern:(NSString *)_pattern {
562 The method build statements like 'LSUB "_folder" "_pattern"'.
563 The returnvalue is the same like the list:pattern: method
571 if ([_folder length] > 0) {
572 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
578 if ([_pattern length] > 0) {
579 if ((_pattern = [self _folder2ImapFolder:_pattern]) == nil)
583 s = [NSString stringWithFormat:@"lsub \"%@\" \"%@\"", _folder, _pattern];
584 map = [self processCommand:s];
586 if (self->delimiter == nil) {
589 rdel = [[map objectEnumeratorForKey:@"LIST"] nextObject];
590 self->delimiter = [[rdel objectForKey:@"delimiter"] copy];
592 return [self->normer normalizeListResponse:map];
595 - (NSDictionary *)select:(NSString *)_folder {
597 Select a folder (required for a lot of methods).
600 The result dict contains the following keys:
602 'access' - string (eg "READ-WRITE")
603 'exists' - number? (eg 1)
604 'recent' - number? (eg 0)
605 'expunge' - array (of what?)
606 'flags' - array of strings (eg (answered,flagged,draft,seen);
607 'RawResponse' - the raw IMAP4 response
612 tmp = self->selectedFolder; // remember ptr to old folder name
614 if ([_folder length] == 0)
616 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
619 self->selectedFolder = [_folder copy];
621 [tmp release]; tmp = nil; // release old folder name
623 s = [NSString stringWithFormat:@"select \"%@\"", self->selectedFolder];
624 return [self->normer normalizeSelectResponse:[self processCommand:s]];
627 - (NSDictionary *)status:(NSString *)_folder flags:(NSArray *)_flags {
632 if ((_flags == nil) || ([_flags count] == 0))
634 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
637 cmd = [NSString stringWithFormat:@"status \"%@\" (%@)",
638 _folder, [_flags componentsJoinedByString:@" "]];
639 return [self->normer normalizeStatusResponse:[self processCommand:cmd]];
642 - (NSDictionary *)noop {
644 return [self->normer normalizeResponse:[self processCommand:@"noop"]];
647 - (NSDictionary *)rename:(NSString *)_folder to:(NSString *)_newName {
650 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
652 if ((_newName = [self _folder2ImapFolder:_newName]) == nil)
655 cmd = [NSString stringWithFormat:@"rename \"%@\" \"%@\"", _folder, _newName];
657 return [self->normer normalizeResponse:[self processCommand:cmd]];
660 - (NSDictionary *)_performCommand:(NSString *)_op onFolder:(NSString *)_fname {
663 if ((_fname = [self _folder2ImapFolder:_fname]) == nil)
666 // eg: 'delete "blah"'
667 command = [NSString stringWithFormat:@"%@ \"%@\"", _op, _fname];
669 return [self->normer normalizeResponse:[self processCommand:command]];
672 - (NSDictionary *)delete:(NSString *)_name {
673 return [self _performCommand:@"delete" onFolder:_name];
675 - (NSDictionary *)create:(NSString *)_name {
676 return [self _performCommand:@"create" onFolder:_name];
678 - (NSDictionary *)subscribe:(NSString *)_name {
679 return [self _performCommand:@"subscribe" onFolder:_name];
681 - (NSDictionary *)unsubscribe:(NSString *)_name {
682 return [self _performCommand:@"unsubscribe" onFolder:_name];
685 - (NSDictionary *)expunge {
686 return [self->normer normalizeResponse:[self processCommand:@"expunge"]];
689 - (NSString *)_uidsJoinedForFetchCmd:(NSArray *)_uids {
690 return [_uids componentsJoinedByString:@","];
692 - (NSString *)_partsJoinedForFetchCmd:(NSArray *)_parts {
693 return [_parts componentsJoinedByString:@" "];
696 - (NSDictionary *)fetchUids:(NSArray *)_uids parts:(NSArray *)_parts {
698 eg: 'UID FETCH 1189,1325,1326 ([TODO])'
700 NSAutoreleasePool *pool;
702 NSDictionary *result;
703 NSString *uidsStr, *partsStr;
706 pool = [[NSAutoreleasePool alloc] init];
708 uidsStr = [self _uidsJoinedForFetchCmd:_uids];
709 partsStr = [self _partsJoinedForFetchCmd:_parts];
710 cmd = [NSString stringWithFormat:@"uid fetch %@ (%@)", uidsStr, partsStr];
712 fetchres = [self processCommand:cmd];
713 result = [[self->normer normalizeFetchResponse:fetchres] retain];
716 return [result autorelease];
719 - (NSDictionary *)fetchUid:(unsigned)_uid parts:(NSArray *)_parts {
720 // TODO: describe what exactly this can return!
721 NSAutoreleasePool *pool;
723 NSDictionary *result;
726 pool = [[NSAutoreleasePool alloc] init];
727 cmd = [NSString stringWithFormat:@"uid fetch %d (%@)", _uid,
728 [self _partsJoinedForFetchCmd:_parts]];
729 fetchres = [self processCommand:cmd];
730 result = [[self->normer normalizeFetchResponse:fetchres] retain];
733 return [result autorelease];
736 - (NSDictionary *)fetchFrom:(unsigned)_from to:(unsigned)_to
737 parts:(NSArray *)_parts
740 NSAutoreleasePool *pool;
741 NSMutableString *cmd;
742 NSDictionary *result;
743 NGHashMap *rawResult;
751 pool = [[NSAutoreleasePool alloc] init];
755 cmd = [NSMutableString stringWithCapacity:256];
756 [cmd appendString:@"fetch "];
757 [cmd appendFormat:@"%d:%d (", _from, _to];
758 for (i = 0, count = [_parts count]; i < count; i++) {
759 if (i != 0) [cmd appendString:@" "];
760 [cmd appendString:[_parts objectAtIndex:i]];
762 [cmd appendString:@")"];
764 if (fetchDebug) NSLog(@"%s: process: %@", __PRETTY_FUNCTION__, cmd);
765 rawResult = [self processCommand:cmd];
767 RawResult is a dict containing keys:
768 ResponseResult: dict eg: {descripted=Completed;result=ok;tagId=8;}
769 fetch: array of record dicts (eg "rfc822.header" key)
772 if (fetchDebug) NSLog(@"%s: normalize: %@", __PRETTY_FUNCTION__,rawResult);
773 result = [[self->normer normalizeFetchResponse:rawResult] retain];
774 if (fetchDebug) NSLog(@"%s: normalized: %@", __PRETTY_FUNCTION__, result);
777 if (fetchDebug) NSLog(@"%s: pool done.", __PRETTY_FUNCTION__);
778 return [result autorelease];
781 - (NSDictionary *)storeUid:(unsigned)_uid add:(NSNumber *)_add
782 flags:(NSArray *)_flags
784 NSString *icmd, *iflags;
786 iflags = [_flags2ImapFlags(self, _flags) componentsJoinedByString:@" "];
787 icmd = [NSString stringWithFormat:@"uid store %d %cFLAGS (%@)",
788 _uid, [_add boolValue] ? '+' : '-',
790 return [self->normer normalizeResponse:[self processCommand:icmd]];
793 - (NSDictionary *)storeFrom:(unsigned)_from to:(unsigned)_to
795 flags:(NSArray *)_flags
805 flagstr = [_flags2ImapFlags(self, _flags) componentsJoinedByString:@" "];
806 cmd = [NSString stringWithFormat:@"store %d:%d %cFLAGS (%@)",
807 _from, _to, [_add boolValue] ? '+' : '-', flagstr];
809 return [self->normer normalizeResponse:[self processCommand:cmd]];
812 - (NSDictionary *)copyFrom:(unsigned)_from to:(unsigned)_to
813 toFolder:(NSString *)_folder
821 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
824 cmd = [NSString stringWithFormat:@"copy %d:%d \"%@\"", _from, _to, _folder];
825 return [self->normer normalizeResponse:[self processCommand:cmd]];
828 - (NSDictionary *)copyUid:(unsigned)_uid toFolder:(NSString *)_folder {
831 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
834 cmd = [NSString stringWithFormat:@"uid copy %d \"%@\"", _uid, _folder];
836 return [self->normer normalizeResponse:[self processCommand:cmd]];
839 - (NSDictionary *)getQuotaRoot:(NSString *)_folder {
842 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
845 cmd = [NSString stringWithFormat:@"getquotaroot \"%@\"", _folder];
846 return [self->normer normalizeQuotaResponse:[self processCommand:cmd]];
849 - (NSDictionary *)append:(NSData *)_message toFolder:(NSString *)_folder
850 withFlags:(NSArray *)_flags
854 NSString *message, *icmd;
856 flags = _flags2ImapFlags(self, _flags);
857 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
861 /* Remove bare newlines */
869 old = [_message bytes];
870 len = [_message length];
872 new = calloc(len * 2 + 4, sizeof(char));
874 while (cntOld < (len - 1)) {
875 if (old[cntOld] == '\n') {
876 new[cntNew++] = '\r';
877 new[cntNew++] = '\n';
879 else if (old[cntOld] != '\r') {
880 new[cntNew++] = old[cntOld];
884 if (old[cntOld] == '\n') {
885 new[cntNew++] = '\r';
886 new[cntNew++] = '\n';
888 else if (old[cntOld] != '\r') {
889 new[cntNew++] = old[cntOld];
891 message = [(NSString *)[NSString alloc]
892 initWithCString:new length:cntNew];
893 if (new) free(new); new = NULL;
896 icmd = [NSString stringWithFormat:@"append \"%@\" (%@) {%d}",
898 [flags componentsJoinedByString:@" "],
899 [message cStringLength]];
900 result = [self processCommand:icmd
901 withTag:YES withNotification:NO];
903 if ([[result objectForKey:@"ContinuationResponse"] boolValue])
904 result = [self processCommand:message withTag:NO];
906 [message release]; message = nil;
908 return [self->normer normalizeResponse:result];
911 - (void)_handleSearchExprIssue:(NSString *)reason qualifier:(EOQualifier *)_q {
913 NSException *exception = nil;
916 if (PreventExceptions != 0)
919 if (_q == nil) _q = (id)[NSNull null];
921 descr = @"Could not process qualifier for imap search ";
922 descr = [descr stringByAppendingString:reason];
924 exception = [[NGImap4SearchException alloc] initWithFormat:@"%@", descr];
925 ui = [NSDictionary dictionaryWithObject:_q forKey:@"qualifier"];
926 [exception setUserInfo:ui];
927 [self->context setLastException:exception];
931 - (NSString *)_searchExprForQual:(EOQualifier *)_qualifier {
936 ' TEXT "why SOPE rocks"'
940 if (_qualifier == nil)
943 result = [_qualifier imap4SearchString];
944 if ([result isKindOfClass:[NSException class]]) {
945 [self _handleSearchExprIssue:[(NSException *)result reason]
946 qualifier:_qualifier];
952 - (NSDictionary *)threadBySubject:(BOOL)_bySubject
953 charset:(NSString *)_charSet
956 http://www.ietf.org/proceedings/03mar/I-D/draft-ietf-imapext-thread-12.txt
958 Returns an array of uids in sort order.
961 _bySubject - if yes, use "REFERENCES" else "ORDEREDSUBJECT" (TODO: ?!)
962 _charSet - default: "UTF-8"
965 UID THREAD REFERENCES|ORDEREDSUBJECT UTF-8 ALL
970 threadAlg = (_bySubject)
974 if ([_charSet length] == 0)
977 threadStr = [NSString stringWithFormat:@"UID THREAD %@ %@ ALL",
978 threadAlg, _charSet];
980 return [self->normer normalizeThreadResponse:
981 [self processCommand:threadStr]];
984 - (NSString *)_generateIMAP4SortOrdering:(EOSortOrdering *)_sortOrdering {
988 key = [_sortOrdering key];
989 if ([key length] == 0)
992 if (![AllowedSortKeys containsObject:[key uppercaseString]]) {
993 [self logWithFormat:@"ERROR[%s] key %@ is not allowed here!",
994 __PRETTY_FUNCTION__, key];
998 sel = [_sortOrdering selector];
999 if (sel_eq(sel, EOCompareDescending) ||
1000 sel_eq(sel, EOCompareCaseInsensitiveDescending)) {
1001 return [@"REVERSE " stringByAppendingString:key];
1003 // TODO: check other selectors whether they make sense instead of silent acc.
1008 - (NSString *)_generateIMAP4SortOrderings:(NSArray *)_sortOrderings {
1010 turn EOSortOrdering into an IMAP4 value for "SORT()"
1012 eg: "DATE REVERSE SUBJECT"
1014 It also checks a set of allowed sort-keys (don't know why)
1016 NSMutableString *sortStr;
1021 if ([_sortOrderings count] == 0)
1024 sortStr = [NSMutableString stringWithCapacity:128];
1025 soe = [_sortOrderings objectEnumerator];
1026 while ((so = [soe nextObject])) {
1029 s = [self _generateIMAP4SortOrdering:so];
1036 [sortStr appendString:@" "];
1038 [sortStr appendString:s];
1040 return isFirst ? nil : sortStr;
1043 - (NSDictionary *)primarySort:(NSString *)_sort
1044 qualifierString:(NSString *)_qualString
1045 encoding:(NSString *)_encoding
1048 http://www.ietf.org/internet-drafts/draft-ietf-imapext-sort-17.txt
1050 The result dict contains the following keys:
1051 'result' - a boolean
1052 'expunge' - array (of what?)
1053 'sort' - array of uids in sort order
1054 'RawResponse' - the raw IMAP4 response
1056 Eg: UID SORT ( DATE REVERSE SUBJECT ) UTF-8 TODO
1058 NSMutableString *sortStr;
1060 if (![_encoding isNotNull]) _encoding = @"UTF-8";
1061 if (![_qualString isNotNull]) _qualString = @" ALL";
1063 sortStr = [NSMutableString stringWithCapacity:128];
1065 [sortStr appendString:@"UID SORT ("];
1066 if (_sort != nil) [sortStr appendString:_sort];
1067 [sortStr appendString:@") "];
1069 [sortStr appendString:_encoding]; /* eg 'UTF-8' */
1071 /* Note: this is _space sensitive_! to many spaces lead to error! */
1072 [sortStr appendString:_qualString]; /* eg ' ALL' or ' TEXT "abc"' */
1074 return [self->normer normalizeSortResponse:[self processCommand:sortStr]];
1077 - (NSDictionary *)sort:(id)_sortSpec
1078 qualifier:(EOQualifier *)_qual
1079 encoding:(NSString *)_encoding
1082 http://www.ietf.org/internet-drafts/draft-ietf-imapext-sort-17.txt
1084 The _sortSpec can be:
1085 - a simple 'raw' IMAP4 sort string
1087 - an array of EOSortOrderings
1089 The result dict contains the following keys:
1090 'result' - a boolean
1091 'expunge' - array (of what?)
1092 'sort' - array of uids in sort order
1093 'RawResponse' - the raw IMAP4 response
1095 If no sortable key was found, the sort will run against 'DATE'.
1096 => TODO: this is inconsistent. If none are passed in, false will be
1099 Eg: UID SORT ( DATE REVERSE SUBJECT ) UTF-8 TODO
1103 if ([_sortSpec isKindOfClass:[NSArray class]])
1104 tmp = [self _generateIMAP4SortOrderings:_sortSpec];
1105 else if ([_sortSpec isKindOfClass:[EOSortOrdering class]])
1106 tmp = [self _generateIMAP4SortOrdering:_sortSpec];
1108 tmp = [_sortSpec stringValue];
1110 if ([tmp length] == 0) { /* found no valid key use date sorting */
1111 [self logWithFormat:@"Note: no key found for sorting, using 'DATE': %@",
1116 return [self primarySort:tmp
1117 qualifierString:[self _searchExprForQual:_qual]
1118 encoding:_encoding];
1120 - (NSDictionary *)sort:(NSArray *)_sortOrderings
1121 qualifier:(EOQualifier *)_qual
1123 // DEPRECATED, should not use context!
1124 return [self sort:_sortOrderings qualifier:_qual
1125 encoding:[[self context] sortEncoding]];
1128 - (NSDictionary *)searchWithQualifier:(EOQualifier *)_qualifier {
1131 s = [self _searchExprForQual:_qualifier];
1132 if ([s length] == 0) {
1133 // TODO: should set last-exception?
1134 [self logWithFormat:@"ERROR(%s): could not process search qualifier: %@",
1135 __PRETTY_FUNCTION__, _qualifier];
1139 s = [@"search" stringByAppendingString:s];
1140 return [self->normer normalizeSearchResponse:[self processCommand:s]];
1143 /* Private Methods */
1145 - (NSException *)_processCommandParserException:(NSException *)_exception {
1146 NSLog(@"ERROR(%s): catched IMAP4 parser exception %@: %@",
1147 __PRETTY_FUNCTION__, [_exception name], [_exception reason]);
1148 [self closeConnection];
1149 [self->context setLastException:_exception];
1152 - (NSException *)_processUnknownCommandParserException:(NSException *)_ex {
1153 NSLog(@"ERROR(%s): catched non-IMAP4 parsing exception %@: %@",
1154 __PRETTY_FUNCTION__, [_ex name], [_ex reason]);
1158 - (NSException *)_handleShutdownDuringCommandException:(NSException *)_ex {
1159 NSLog(@"ERROR(%s): IMAP4 socket was shut down by server %@: %@",
1160 __PRETTY_FUNCTION__, [_ex name], [_ex reason]);
1161 [self closeConnection];
1162 [self->context setLastException:_ex];
1166 - (BOOL)_isShutdownException:(NSException *)_ex {
1167 return [[_ex name] isEqualToString:@"NGSocketShutdownDuringReadException"];
1170 - (BOOL)_isLoginCommand:(NSString *)_command {
1171 return [_command hasPrefix:@"login"];
1174 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
1175 withNotification:(BOOL)_notification logText:(NSString *)_txt
1180 NSException *exception;
1185 if (ProfileImapEnabled == 1) {
1186 gettimeofday(&tv, NULL);
1187 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
1188 fprintf(stderr, "{");
1197 [self->context resetLastException];
1199 NSException *e = nil; // TODO: try to remove exception handler
1201 [self sendCommand:_command withTag:_tag logText:_txt];
1202 map = [self->parser parseResponseForTagId:self->tagId exception:&e];
1207 if ([localException isKindOfClass:[NGImap4ParserException class]]) {
1208 [[self _processCommandParserException:localException] raise];
1210 else if ([self _isShutdownException:localException]) {
1211 [[self _handleShutdownDuringCommandException:localException] raise];
1214 [[self _processUnknownCommandParserException:localException] raise];
1215 if (reconnectCnt == 0) {
1216 if (![self _isLoginCommand:_command]) {
1219 exception = localException;
1222 [self closeConnection];
1223 [self->context setLastException:localException];
1231 else if ([map objectForKey:@"bye"] && ![_command hasPrefix:@"logout"]) {
1232 if (reconnectCnt == 0) {
1238 } while (tryReconnect);
1240 if ([self->context lastException]) {
1242 [self->context setLastException:exception];
1246 if (_notification) [self sendResponseNotification:map];
1248 if (ProfileImapEnabled == 1) {
1249 gettimeofday(&tv, NULL);
1250 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
1251 fprintf(stderr, "}[%s] <Send Command [%s]> : time needed: %4.4fs\n",
1252 __PRETTY_FUNCTION__, [_command cString], ti < 0.0 ? -1.0 : ti);
1257 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
1258 withNotification:(BOOL)_notification
1260 return [self processCommand:_command withTag:_tag
1261 withNotification:_notification
1265 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag {
1266 return [self processCommand:_command withTag:_tag withNotification:YES
1270 - (NGHashMap *)processCommand:(NSString *)_command {
1271 return [self processCommand:_command withTag:YES withNotification:YES
1275 - (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt {
1276 return [self processCommand:_command withTag:YES withNotification:YES
1280 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag
1281 logText:(NSString *)_txt
1284 NGCTextStream *txtStream;
1286 txtStream = [self textStream];
1291 command = [NSString stringWithFormat:@"%d %@", self->tagId, _command];
1293 _txt = [NSString stringWithFormat:@"%d %@", self->tagId, _txt];
1300 if ([_txt length] > 5000) {
1301 fprintf(stderr, "C[%p]: %s...\n", self, [[_txt substringToIndex:5000]
1305 fprintf(stderr, "C[%p]: %s\n", self, [_txt cString]);
1309 if (![txtStream writeString:command])
1310 [self->context setLastException:[txtStream lastException]];
1311 else if (![txtStream writeString:@"\r\n"])
1312 [self->context setLastException:[txtStream lastException]];
1313 else if (![txtStream flush])
1314 [self->context setLastException:[txtStream lastException]];
1317 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag {
1318 [self sendCommand:_command withTag:_tag logText:_command];
1321 - (void)sendCommand:(NSString *)_command {
1322 [self sendCommand:_command withTag:YES logText:_command];
1325 - (NSArray *)_flags2ImapFlags:(NSArray *)_flags {
1326 /* adds backslashes in front of the flags */
1327 NSEnumerator *enumerator;
1333 objs = calloc([_flags count] + 2, sizeof(id));
1335 enumerator = [_flags objectEnumerator];
1336 while ((obj = [enumerator nextObject])) {
1337 objs[cnt] = [@"\\" stringByAppendingString:obj];
1340 result = [NSArray arrayWithObjects:objs count:cnt];
1341 if (objs != NULL) free(objs);
1344 static inline NSArray *_flags2ImapFlags(NGImap4Client *self, NSArray *_flags) {
1345 return [self _flags2ImapFlags:_flags];
1348 - (NSString *)_folder2ImapFolder:(NSString *)_folder {
1351 if (self->delimiter == nil) {
1354 res = [self list:@"" pattern:@""];
1356 if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
1360 array = [_folder pathComponents];
1362 if ([array count] > 0) {
1365 o = [array objectAtIndex:0];
1366 if (([o isEqualToString:@"/"]) || ([o length] == 0))
1367 array = [array subarrayWithRange:NSMakeRange(1, [array count] - 1)];
1369 o = [array lastObject];
1370 if (([o length] == 0) || ([o isEqualToString:@"/"]))
1371 array = [array subarrayWithRange:NSMakeRange(0, [array count] - 1)];
1373 return [[array componentsJoinedByString:self->delimiter]
1374 stringByEncodingImap4FolderName];
1377 - (NSString *)_imapFolder2Folder:(NSString *)_folder {
1380 array = [NSArray arrayWithObject:@""];
1382 if ([self delimiter] == nil) {
1385 res = [self list:@"" pattern:@""]; // fill the delimiter ivar?
1386 if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
1390 array = [array arrayByAddingObjectsFromArray:
1391 [_folder componentsSeparatedByString:[self delimiter]]];
1393 return [[NSString pathWithComponents:array] stringByDecodingImap4FolderName];
1396 - (void)setContext:(NGImap4Context *)_ctx {
1397 self->context = _ctx;
1399 - (NGImap4Context *)context {
1400 return self->context;
1403 /* ConnectionRegistration */
1405 - (void)removeFromConnectionRegister {
1408 for (cnt = 0; cnt < MaxImapClients; cnt++) {
1409 if (ImapClients[cnt] == self)
1410 ImapClients[cnt] = nil;
1414 - (void)registerConnection {
1417 cnt = CountClient % MaxImapClients;
1419 if (ImapClients[cnt]) {
1420 [(NGImap4Context *)ImapClients[cnt] closeConnection];
1422 ImapClients[cnt] = self;
1426 - (id<NGExtendedTextStream>)textStream {
1427 if (self->text == nil) {
1428 if ([self->context lastException] == nil)
1436 - (NSString *)description {
1437 NSMutableString *ms;
1440 ms = [NSMutableString stringWithCapacity:128];
1441 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
1443 if (self->login != nil)
1444 [ms appendFormat:@" login=%@%s", self->login, self->password?"(pwd)":""];
1446 if ((tmp = [self socket]) != nil)
1447 [ms appendFormat:@" socket=%@", tmp];
1448 else if (self->address)
1449 [ms appendFormat:@" address=%@", self->address];
1451 [ms appendString:@">"];
1455 @end /* NGImap4Client; */