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 EOSortOrdering(IMAPAdditions)
41 - (NSString *)imap4SortString;
44 @interface NSArray(IMAPAdditions)
45 - (NSString *)imap4SortStringForSortOrderings;
48 @interface NGImap4Client(ConnectionRegistration)
50 - (void)removeFromConnectionRegister;
51 - (void)registerConnection;
52 - (NGCTextStream *)textStream;
54 @end /* NGImap4Client(ConnectionRegistration); */
56 #if GNUSTEP_BASE_LIBRARY
57 /* FIXME: TODO: move someplace better (hh: NGExtensions...) */
58 @implementation NSException(setUserInfo)
60 - (id)setUserInfo:(NSDictionary *)_userInfo {
61 ASSIGN(self->_e_info, _userInfo);
65 @end /* NSException(setUserInfo) */
68 @interface NGImap4Client(Private)
70 - (NSString *)_folder2ImapFolder:(NSString *)_folder;
72 - (NGHashMap *)processCommand:(NSString *)_command;
73 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag;
74 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
75 withNotification:(BOOL)_notification;
76 - (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt;
78 - (void)sendCommand:(NSString *)_command;
79 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag;
80 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag
81 logText:(NSString *)_txt;
83 - (void)sendResponseNotification:(NGHashMap *)map;
85 - (NSDictionary *)login;
90 An implementation of an Imap4 client
92 A folder name always looks like an absolute filename (/inbox/doof)
95 @implementation NGImap4Client
98 static inline NSArray *_flags2ImapFlags(NGImap4Client *, NSArray *);
100 static NSNumber *YesNumber = nil;
101 static NSNumber *NoNumber = nil;
103 static id *ImapClients = NULL;
104 static unsigned int CountClient = 0;
105 static unsigned int MaxImapClients = 0;
106 static int ProfileImapEnabled = -1;
107 static int LogImapEnabled = -1;
108 static int PreventExceptions = -1;
109 static BOOL fetchDebug = NO;
110 static BOOL ImapDebugEnabled = NO;
120 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
121 static BOOL didInit = NO;
125 PreventExceptions = [ud boolForKey:@"ImapPreventConnectionExceptions"]?1:0;
126 LogImapEnabled = [ud boolForKey:@"ImapLogEnabled"]?1:0;
127 ProfileImapEnabled = [ud boolForKey:@"ProfileImapEnabled"]?1:0;
128 ImapDebugEnabled = [ud boolForKey:@"ImapDebugEnabled"];
130 YesNumber = [[NSNumber numberWithBool:YES] retain];
131 NoNumber = [[NSNumber numberWithBool:NO] retain];
133 if (MaxImapClients < 1) {
134 MaxImapClients = [ud integerForKey:@"NGImapMaxConnectionCount"];
135 if (MaxImapClients < 1) MaxImapClients = 50;
137 if (ImapClients == NULL)
138 ImapClients = calloc(MaxImapClients + 2, sizeof(id));
143 + (id)clientWithURL:(NSURL *)_url {
144 return [[(NGImap4Client *)[self alloc] initWithURL:_url] autorelease];
146 + (id)clientWithAddress:(id<NGSocketAddress>)_address {
148 [[(NGImap4Client *)[self alloc] initWithAddress:_address] autorelease];
151 + (id)clientWithHost:(id)_host {
152 return [[[self alloc] initWithHost:_host] autorelease];
155 - (id)initWithHost:(id)_host {
156 NGInternetSocketAddress *a;
158 a = [NGInternetSocketAddress addressWithPort:143 onHost:_host];
159 return [self initWithAddress:a];
161 - (id)initWithURL:(NSURL *)_url {
162 NGInternetSocketAddress *a;
166 if ((self->useSSL = [[_url scheme] isEqualToString:@"imaps"])) {
167 if (NSClassFromString(@"NGActiveSSLSocket") == nil) {
169 @"no SSL support available, cannot connect: %@", _url];
174 if ((tmp = [_url port])) {
175 port = [tmp intValue];
176 if (port <= 0) port = self->useSSL ? 993 : 143;
179 port = self->useSSL ? 993 : 143;
181 self->login = [[_url user] copy];
182 self->password = [[_url password] copy];
184 a = [NGInternetSocketAddress addressWithPort:port onHost:[_url host]];
185 return [self initWithAddress:a];
188 - (id)initWithAddress:(id<NGSocketAddress>)_address { /* designated init */
189 if ((self = [super init])) {
190 self->address = [_address retain];
191 self->debug = ImapDebugEnabled;
192 self->responseReceiver = [[NSMutableArray alloc] initWithCapacity:128];
193 self->normer = [[NGImap4ResponseNormalizer alloc] initWithClient:self];
199 [self removeFromConnectionRegister];
200 [self->normer release];
201 [self->text release];
202 [self->address release];
203 [self->socket release];
204 [self->parser release];
205 [self->responseReceiver release];
206 [self->login release];
207 [self->password release];
208 [self->selectedFolder release];
209 [self->delimiter release];
210 [self->serverGID release];
212 self->context = nil; /* not retained */
216 /* equality (required for adding clients to Foundation sets) */
218 - (BOOL)isEqual:(id)_obj {
222 if ([_obj isKindOfClass:[NGImap4Client class]])
223 return [self isEqualToClient:_obj];
228 - (BOOL)isEqualToClient:(NGImap4Client *)_obj {
229 if (_obj == self) return YES;
230 if (_obj == nil) return NO;
232 return [[_obj address] isEqual:self->address];
237 - (id<NGActiveSocket>)socket {
241 - (id<NGSocketAddress>)address {
242 return self->address;
245 - (NSString *)delimiter {
246 return self->delimiter;
249 - (EOGlobalID *)serverGlobalID {
250 NGInternetSocketAddress *is;
253 return self->serverGID;
255 is = (id)[self address];
257 self->serverGID = [[NGImap4ServerGlobalID alloc]
258 initWithHostname:[is hostName]
261 return self->serverGID;
264 - (NSString *)selectedFolderName {
265 return self->selectedFolder;
271 Class socketClass = Nil;
274 socketClass = [self useSSL]
275 ? NSClassFromString(@"NGActiveSSLSocket")
276 : [NGActiveSocket class];
279 sock = [socketClass socketConnectedToAddress:self->address];
282 [self->context setLastException:localException];
290 - (NSDictionary *)_receiveServerGreetingWithoutTagId {
291 NSDictionary *res = nil;
297 hm = [self->parser parseResponseForTagId:-1 exception:&e];
299 res = [self->normer normalizeOpenConnectionResponse:hm];
302 [self->context setLastException:localException];
305 if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
311 - (NSDictionary *)_openConnection {
312 /* open connection as configured */
313 NGBufferedStream *buffer;
317 if (ProfileImapEnabled == 1) {
318 gettimeofday(&tv, NULL);
319 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
321 [self->socket release]; self->socket = nil;
322 [self->parser release]; self->parser = nil;
323 [self->text release]; self->text = nil;
325 [self->context resetLastException];
327 if ((self->socket = [[self _openSocket] retain]) == nil)
329 if ([self->context lastException])
333 [(NGBufferedStream *)[NGBufferedStream alloc] initWithSource:self->socket];
334 self->text = [(NGCTextStream *)[NGCTextStream alloc] initWithSource:buffer];
335 [buffer release]; buffer = nil;
337 self->parser = [[NGImap4ResponseParser alloc] initWithStream:self->socket];
340 if (ProfileImapEnabled == 1) {
341 gettimeofday(&tv, NULL);
342 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
343 fprintf(stderr, "[%s] <openConnection> : time needed: %4.4fs\n",
344 __PRETTY_FUNCTION__, ti < 0.0 ? -1.0 : ti);
346 [self registerConnection];
347 [self->context resetLastException];
349 return [self _receiveServerGreetingWithoutTagId];
352 - (NSDictionary *)openConnection {
353 return [self _openConnection];
356 - (NSNumber *)isConnected {
357 // TODO: why is that an NSNummber?
359 Check whether stream is already open (could be closed because
362 return (self->socket == nil)
364 : ([(NGActiveSocket *)self->socket isAlive] ? YesNumber : NoNumber);
367 - (NSException *)_handleTextReleaseException:(NSException *)_ex {
368 [self logWithFormat:@"got exception during stream dealloc: %@", _ex];
371 - (NSException *)_handleSocketCloseException:(NSException *)_ex {
372 [self logWithFormat:@"got exception during socket close: %@", _ex];
375 - (NSException *)_handleSocketReleaseException:(NSException *)_ex {
376 [self logWithFormat:@"got exception during socket deallocation: %@", _ex];
379 - (void)closeConnection {
380 /* close a connection */
382 // TODO: this is a bit weird, probably because of the flush
383 // maybe just call -close on the text stream?
385 [self->text release];
387 [[self _handleTextReleaseException:localException] raise];
392 [self->socket close];
394 [[self _handleSocketCloseException:localException] raise];
398 [self->socket release];
400 [[self _handleSocketReleaseException:localException] raise];
404 [self->parser release]; self->parser = nil;
405 [self->delimiter release]; self->delimiter = nil;
406 [self removeFromConnectionRegister];
409 // ResponseNotifications
411 - (void)registerForResponseNotification:(id<NGImap4ResponseReceiver>)_obj {
412 [self->responseReceiver addObject:[NSValue valueWithNonretainedObject:_obj]];
415 - (void)removeFromResponseNotification:(id<NGImap4ResponseReceiver>)_obj {
416 [self->responseReceiver removeObject:
417 [NSValue valueWithNonretainedObject:_obj]];
420 - (void)sendResponseNotification:(NGHashMap *)_map {
422 id<NGImap4ResponseReceiver> obj;
423 NSEnumerator *enumerator;
426 resp = [self->normer normalizeResponse:_map];
427 enumerator = [self->responseReceiver objectEnumerator];
429 while ((val = [enumerator nextObject])) {
430 obj = [val nonretainedObjectValue];
431 [obj responseNotificationFrom:self response:resp];
437 - (NSDictionary *)login:(NSString *)_login password:(NSString *)_passwd {
438 /* login with plaintext password authenticating */
440 if ((_login == nil) || (_passwd == nil))
443 [self->login release]; self->login = nil;
444 [self->password release]; self->password = nil;
445 [self->serverGID release]; self->serverGID = nil;
447 self->login = [_login copy];
448 self->password = [[_passwd stringByEscapingImap4Password] copy];
454 if ([self->context lastException] != nil)
457 [self closeConnection];
459 [self openConnection];
461 if ([self->context lastException] != nil)
467 - (NSDictionary *)login {
469 On failure returns a dictionary with those keys:
470 'result' - a boolean => false
471 'reason' - reason why login failed
472 'RawResponse' - the raw IMAP4 response
474 'result' - a boolean => true
475 'expunge' - an array (containing what?)
476 'RawResponse' - the raw IMAP4 response
486 s = [NSString stringWithFormat:@"login \"%@\" \"%@\"",
487 self->login, self->password];
488 log = [NSString stringWithFormat:@"login %@ <%@>",
490 (self->password != nil) ? @"PASSWORD" : @"NO PASSWORD"];
491 map = [self processCommand:s logText:log];
493 if (self->selectedFolder != nil)
494 [self select:self->selectedFolder];
498 return [self->normer normalizeResponse:map];
501 - (NSDictionary *)logout {
502 /* logout from the connected host and close the connection */
505 map = [self processCommand:@"logout"];
506 [self closeConnection];
508 return [self->normer normalizeResponse:map];
511 /* Authenticated State */
513 - (NSDictionary *)list:(NSString *)_folder pattern:(NSString *)_pattern {
515 The method build statements like 'LIST "_folder" "_pattern"'.
516 The Cyrus IMAP4 v1.5.14 server ignores the given folder.
517 Instead of you should use the pattern to get the expected result.
518 If folder is NIL it would be set to empty string ''.
519 If pattern is NIL it would be set to ''.
521 The result dict contains the following keys:
523 'list' - a dictionary (key is folder name, value is flags)
524 'RawResponse' - the raw IMAP4 response
526 NSAutoreleasePool *pool;
528 NSDictionary *result;
531 pool = [[NSAutoreleasePool alloc] init];
533 if (_folder == nil) _folder = @"";
534 if (_pattern == nil) _pattern = @"";
536 if ([_folder length] > 0) {
537 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
542 if ([_pattern length] > 0)
543 if (!(_pattern = [self _folder2ImapFolder:_pattern]))
546 s = [NSString stringWithFormat:@"list \"%@\" \"%@\"", _folder, _pattern];
547 map = [self processCommand:s];
549 if (self->delimiter == nil) {
552 rdel = [[map objectEnumeratorForKey:@"list"] nextObject];
553 self->delimiter = [[rdel objectForKey:@"delimiter"] copy];
556 result = [[self->normer normalizeListResponse:map] copy];
558 return [result autorelease];
561 - (NSDictionary *)capability {
563 capres = [self processCommand:@"capability"];
564 return [self->normer normalizeCapabilityRespone:capres];
567 - (NSDictionary *)lsub:(NSString *)_folder pattern:(NSString *)_pattern {
569 The method build statements like 'LSUB "_folder" "_pattern"'.
570 The returnvalue is the same like the list:pattern: method
578 if ([_folder length] > 0) {
579 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
585 if ([_pattern length] > 0) {
586 if ((_pattern = [self _folder2ImapFolder:_pattern]) == nil)
590 s = [NSString stringWithFormat:@"lsub \"%@\" \"%@\"", _folder, _pattern];
591 map = [self processCommand:s];
593 if (self->delimiter == nil) {
596 rdel = [[map objectEnumeratorForKey:@"LIST"] nextObject];
597 self->delimiter = [[rdel objectForKey:@"delimiter"] copy];
599 return [self->normer normalizeListResponse:map];
602 - (NSDictionary *)select:(NSString *)_folder {
604 Select a folder (required for a lot of methods).
607 The result dict contains the following keys:
609 'access' - string (eg "READ-WRITE")
610 'exists' - number? (eg 1)
611 'recent' - number? (eg 0)
612 'expunge' - array (of what?)
613 'flags' - array of strings (eg (answered,flagged,draft,seen);
614 'RawResponse' - the raw IMAP4 response
619 tmp = self->selectedFolder; // remember ptr to old folder name
621 if ([_folder length] == 0)
623 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
626 self->selectedFolder = [_folder copy];
628 [tmp release]; tmp = nil; // release old folder name
630 s = [NSString stringWithFormat:@"select \"%@\"", self->selectedFolder];
631 return [self->normer normalizeSelectResponse:[self processCommand:s]];
634 - (NSDictionary *)status:(NSString *)_folder flags:(NSArray *)_flags {
639 if ((_flags == nil) || ([_flags count] == 0))
641 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
644 cmd = [NSString stringWithFormat:@"status \"%@\" (%@)",
645 _folder, [_flags componentsJoinedByString:@" "]];
646 return [self->normer normalizeStatusResponse:[self processCommand:cmd]];
649 - (NSDictionary *)noop {
651 return [self->normer normalizeResponse:[self processCommand:@"noop"]];
654 - (NSDictionary *)rename:(NSString *)_folder to:(NSString *)_newName {
657 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
659 if ((_newName = [self _folder2ImapFolder:_newName]) == nil)
662 cmd = [NSString stringWithFormat:@"rename \"%@\" \"%@\"", _folder, _newName];
664 return [self->normer normalizeResponse:[self processCommand:cmd]];
667 - (NSDictionary *)_performCommand:(NSString *)_op onFolder:(NSString *)_fname {
670 if ((_fname = [self _folder2ImapFolder:_fname]) == nil)
673 // eg: 'delete "blah"'
674 command = [NSString stringWithFormat:@"%@ \"%@\"", _op, _fname];
676 return [self->normer normalizeResponse:[self processCommand:command]];
679 - (NSDictionary *)delete:(NSString *)_name {
680 return [self _performCommand:@"delete" onFolder:_name];
682 - (NSDictionary *)create:(NSString *)_name {
683 return [self _performCommand:@"create" onFolder:_name];
685 - (NSDictionary *)subscribe:(NSString *)_name {
686 return [self _performCommand:@"subscribe" onFolder:_name];
688 - (NSDictionary *)unsubscribe:(NSString *)_name {
689 return [self _performCommand:@"unsubscribe" onFolder:_name];
692 - (NSDictionary *)expunge {
693 return [self->normer normalizeResponse:[self processCommand:@"expunge"]];
696 - (NSString *)_uidsJoinedForFetchCmd:(NSArray *)_uids {
697 return [_uids componentsJoinedByString:@","];
699 - (NSString *)_partsJoinedForFetchCmd:(NSArray *)_parts {
700 return [_parts componentsJoinedByString:@" "];
703 - (NSDictionary *)fetchUids:(NSArray *)_uids parts:(NSArray *)_parts {
705 eg: 'UID FETCH 1189,1325,1326 ([TODO])'
707 NSAutoreleasePool *pool;
709 NSDictionary *result;
710 NSString *uidsStr, *partsStr;
713 pool = [[NSAutoreleasePool alloc] init];
715 uidsStr = [self _uidsJoinedForFetchCmd:_uids];
716 partsStr = [self _partsJoinedForFetchCmd:_parts];
717 cmd = [NSString stringWithFormat:@"uid fetch %@ (%@)", uidsStr, partsStr];
719 fetchres = [self processCommand:cmd];
720 result = [[self->normer normalizeFetchResponse:fetchres] retain];
723 return [result autorelease];
726 - (NSDictionary *)fetchUid:(unsigned)_uid parts:(NSArray *)_parts {
727 // TODO: describe what exactly this can return!
728 NSAutoreleasePool *pool;
730 NSDictionary *result;
733 pool = [[NSAutoreleasePool alloc] init];
734 cmd = [NSString stringWithFormat:@"uid fetch %d (%@)", _uid,
735 [self _partsJoinedForFetchCmd:_parts]];
736 fetchres = [self processCommand:cmd];
737 result = [[self->normer normalizeFetchResponse:fetchres] retain];
740 return [result autorelease];
743 - (NSDictionary *)fetchFrom:(unsigned)_from to:(unsigned)_to
744 parts:(NSArray *)_parts
747 NSAutoreleasePool *pool;
748 NSMutableString *cmd;
749 NSDictionary *result;
750 NGHashMap *rawResult;
758 pool = [[NSAutoreleasePool alloc] init];
762 cmd = [NSMutableString stringWithCapacity:256];
763 [cmd appendString:@"fetch "];
764 [cmd appendFormat:@"%d:%d (", _from, _to];
765 for (i = 0, count = [_parts count]; i < count; i++) {
766 if (i != 0) [cmd appendString:@" "];
767 [cmd appendString:[_parts objectAtIndex:i]];
769 [cmd appendString:@")"];
771 if (fetchDebug) NSLog(@"%s: process: %@", __PRETTY_FUNCTION__, cmd);
772 rawResult = [self processCommand:cmd];
774 RawResult is a dict containing keys:
775 ResponseResult: dict eg: {descripted=Completed;result=ok;tagId=8;}
776 fetch: array of record dicts (eg "rfc822.header" key)
779 if (fetchDebug) NSLog(@"%s: normalize: %@", __PRETTY_FUNCTION__,rawResult);
780 result = [[self->normer normalizeFetchResponse:rawResult] retain];
781 if (fetchDebug) NSLog(@"%s: normalized: %@", __PRETTY_FUNCTION__, result);
784 if (fetchDebug) NSLog(@"%s: pool done.", __PRETTY_FUNCTION__);
785 return [result autorelease];
788 - (NSDictionary *)storeUid:(unsigned)_uid add:(NSNumber *)_add
789 flags:(NSArray *)_flags
791 NSString *icmd, *iflags;
793 iflags = [_flags2ImapFlags(self, _flags) componentsJoinedByString:@" "];
794 icmd = [NSString stringWithFormat:@"uid store %d %cFLAGS (%@)",
795 _uid, [_add boolValue] ? '+' : '-',
797 return [self->normer normalizeResponse:[self processCommand:icmd]];
800 - (NSDictionary *)storeFrom:(unsigned)_from to:(unsigned)_to
802 flags:(NSArray *)_flags
812 flagstr = [_flags2ImapFlags(self, _flags) componentsJoinedByString:@" "];
813 cmd = [NSString stringWithFormat:@"store %d:%d %cFLAGS (%@)",
814 _from, _to, [_add boolValue] ? '+' : '-', flagstr];
816 return [self->normer normalizeResponse:[self processCommand:cmd]];
819 - (NSDictionary *)copyFrom:(unsigned)_from to:(unsigned)_to
820 toFolder:(NSString *)_folder
828 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
831 cmd = [NSString stringWithFormat:@"copy %d:%d \"%@\"", _from, _to, _folder];
832 return [self->normer normalizeResponse:[self processCommand:cmd]];
835 - (NSDictionary *)copyUid:(unsigned)_uid toFolder:(NSString *)_folder {
838 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
841 cmd = [NSString stringWithFormat:@"uid copy %d \"%@\"", _uid, _folder];
843 return [self->normer normalizeResponse:[self processCommand:cmd]];
845 - (NSDictionary *)copyUids:(NSArray *)_uids toFolder:(NSString *)_folder {
848 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
851 cmd = [NSString stringWithFormat:@"uid copy %@ \"%@\"",
852 [_uids componentsJoinedByString:@","], _folder];
854 return [self->normer normalizeResponse:[self processCommand:cmd]];
857 - (NSDictionary *)getQuotaRoot:(NSString *)_folder {
860 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
863 cmd = [NSString stringWithFormat:@"getquotaroot \"%@\"", _folder];
864 return [self->normer normalizeQuotaResponse:[self processCommand:cmd]];
867 - (NSDictionary *)append:(NSData *)_message toFolder:(NSString *)_folder
868 withFlags:(NSArray *)_flags
872 NSString *message, *icmd;
874 flags = _flags2ImapFlags(self, _flags);
875 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
879 /* Remove bare newlines */
887 old = [_message bytes];
888 len = [_message length];
890 new = calloc(len * 2 + 4, sizeof(char));
892 while (cntOld < (len - 1)) {
893 if (old[cntOld] == '\n') {
894 new[cntNew] = '\r'; cntNew++;
895 new[cntNew] = '\n'; cntNew++;
897 else if (old[cntOld] != '\r') {
898 new[cntNew] = old[cntOld]; cntNew++;
902 if (old[cntOld] == '\n') {
903 new[cntNew] = '\r'; cntNew++;
904 new[cntNew] = '\n'; cntNew++;
906 else if (old[cntOld] != '\r') {
907 new[cntNew] = old[cntOld]; cntNew++;
910 // TODO: fix this junk, do not treat the message as a string, its NSData
911 message = [(NSString *)[NSString alloc] initWithCString:new length:cntNew];
912 if (new != NULL) free(new); new = NULL;
915 icmd = [NSString stringWithFormat:@"append \"%@\" (%@) {%d}",
917 [flags componentsJoinedByString:@" "],
918 [message cStringLength]];
919 result = [self processCommand:icmd
920 withTag:YES withNotification:NO];
922 // TODO: explain that
923 if ([[result objectForKey:@"ContinuationResponse"] boolValue])
924 result = [self processCommand:message withTag:NO];
926 [message release]; message = nil;
928 return [self->normer normalizeResponse:result];
931 - (void)_handleSearchExprIssue:(NSString *)reason qualifier:(EOQualifier *)_q {
933 NSException *exception = nil;
936 if (PreventExceptions != 0)
939 if (_q == nil) _q = (id)[NSNull null];
941 descr = @"Could not process qualifier for imap search ";
942 descr = [descr stringByAppendingString:reason];
944 exception = [[NGImap4SearchException alloc] initWithFormat:@"%@", descr];
945 ui = [NSDictionary dictionaryWithObject:_q forKey:@"qualifier"];
946 [exception setUserInfo:ui];
947 [self->context setLastException:exception];
951 - (NSString *)_searchExprForQual:(EOQualifier *)_qualifier {
956 ' TEXT "why SOPE rocks"'
960 if (_qualifier == nil)
963 result = [_qualifier imap4SearchString];
964 if ([result isKindOfClass:[NSException class]]) {
965 [self _handleSearchExprIssue:[(NSException *)result reason]
966 qualifier:_qualifier];
972 - (NSDictionary *)threadBySubject:(BOOL)_bySubject
973 charset:(NSString *)_charSet
976 http://www.ietf.org/proceedings/03mar/I-D/draft-ietf-imapext-thread-12.txt
978 Returns an array of uids in sort order.
981 _bySubject - if yes, use "REFERENCES" else "ORDEREDSUBJECT" (TODO: ?!)
982 _charSet - default: "UTF-8"
985 UID THREAD REFERENCES|ORDEREDSUBJECT UTF-8 ALL
990 threadAlg = (_bySubject)
994 if ([_charSet length] == 0)
997 threadStr = [NSString stringWithFormat:@"UID THREAD %@ %@ ALL",
998 threadAlg, _charSet];
1000 return [self->normer normalizeThreadResponse:
1001 [self processCommand:threadStr]];
1004 - (NSString *)_generateIMAP4SortOrdering:(EOSortOrdering *)_sortOrdering {
1005 // TODO: still called by anything?
1006 return [_sortOrdering imap4SortString];
1008 - (NSString *)_generateIMAP4SortOrderings:(NSArray *)_sortOrderings {
1009 return [_sortOrderings imap4SortStringForSortOrderings];
1012 - (NSDictionary *)primarySort:(NSString *)_sort
1013 qualifierString:(NSString *)_qualString
1014 encoding:(NSString *)_encoding
1017 http://www.ietf.org/internet-drafts/draft-ietf-imapext-sort-17.txt
1019 The result dict contains the following keys:
1020 'result' - a boolean
1021 'expunge' - array (of what?)
1022 'sort' - array of uids in sort order
1023 'RawResponse' - the raw IMAP4 response
1025 Eg: UID SORT ( DATE REVERSE SUBJECT ) UTF-8 TODO
1027 NSMutableString *sortStr;
1029 if (![_encoding isNotNull]) _encoding = @"UTF-8";
1030 if (![_qualString isNotNull]) _qualString = @" ALL";
1032 sortStr = [NSMutableString stringWithCapacity:128];
1034 [sortStr appendString:@"UID SORT ("];
1035 if (_sort != nil) [sortStr appendString:_sort];
1036 [sortStr appendString:@") "];
1038 [sortStr appendString:_encoding]; /* eg 'UTF-8' */
1040 /* Note: this is _space sensitive_! to many spaces lead to error! */
1041 [sortStr appendString:_qualString]; /* eg ' ALL' or ' TEXT "abc"' */
1043 return [self->normer normalizeSortResponse:[self processCommand:sortStr]];
1046 - (NSDictionary *)sort:(id)_sortSpec
1047 qualifier:(EOQualifier *)_qual
1048 encoding:(NSString *)_encoding
1051 http://www.ietf.org/internet-drafts/draft-ietf-imapext-sort-17.txt
1053 The _sortSpec can be:
1054 - a simple 'raw' IMAP4 sort string
1056 - an array of EOSortOrderings
1058 The result dict contains the following keys:
1059 'result' - a boolean
1060 'expunge' - array (of what?)
1061 'sort' - array of uids in sort order
1062 'RawResponse' - the raw IMAP4 response
1064 If no sortable key was found, the sort will run against 'DATE'.
1065 => TODO: this is inconsistent. If none are passed in, false will be
1068 Eg: UID SORT ( DATE REVERSE SUBJECT ) UTF-8 TODO
1072 if ([_sortSpec isKindOfClass:[NSArray class]])
1073 tmp = [self _generateIMAP4SortOrderings:_sortSpec];
1074 else if ([_sortSpec isKindOfClass:[EOSortOrdering class]])
1075 tmp = [self _generateIMAP4SortOrdering:_sortSpec];
1077 tmp = [_sortSpec stringValue];
1079 if ([tmp length] == 0) { /* found no valid key use date sorting */
1080 [self logWithFormat:@"Note: no key found for sorting, using 'DATE': %@",
1085 return [self primarySort:tmp
1086 qualifierString:[self _searchExprForQual:_qual]
1087 encoding:_encoding];
1089 - (NSDictionary *)sort:(NSArray *)_sortOrderings
1090 qualifier:(EOQualifier *)_qual
1092 // DEPRECATED, should not use context!
1093 return [self sort:_sortOrderings qualifier:_qual
1094 encoding:[[self context] sortEncoding]];
1097 - (NSDictionary *)searchWithQualifier:(EOQualifier *)_qualifier {
1100 s = [self _searchExprForQual:_qualifier];
1101 if ([s length] == 0) {
1102 // TODO: should set last-exception?
1103 [self logWithFormat:@"ERROR(%s): could not process search qualifier: %@",
1104 __PRETTY_FUNCTION__, _qualifier];
1108 s = [@"search" stringByAppendingString:s];
1109 return [self->normer normalizeSearchResponse:[self processCommand:s]];
1112 /* Private Methods */
1114 - (NSException *)_processCommandParserException:(NSException *)_exception {
1115 NSLog(@"ERROR(%s): catched IMAP4 parser exception %@: %@",
1116 __PRETTY_FUNCTION__, [_exception name], [_exception reason]);
1117 [self closeConnection];
1118 [self->context setLastException:_exception];
1121 - (NSException *)_processUnknownCommandParserException:(NSException *)_ex {
1122 NSLog(@"ERROR(%s): catched non-IMAP4 parsing exception %@: %@",
1123 __PRETTY_FUNCTION__, [_ex name], [_ex reason]);
1127 - (NSException *)_handleShutdownDuringCommandException:(NSException *)_ex {
1128 NSLog(@"ERROR(%s): IMAP4 socket was shut down by server %@: %@",
1129 __PRETTY_FUNCTION__, [_ex name], [_ex reason]);
1130 [self closeConnection];
1131 [self->context setLastException:_ex];
1135 - (BOOL)_isShutdownException:(NSException *)_ex {
1136 return [[_ex name] isEqualToString:@"NGSocketShutdownDuringReadException"];
1139 - (BOOL)_isLoginCommand:(NSString *)_command {
1140 return [_command hasPrefix:@"login"];
1143 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
1144 withNotification:(BOOL)_notification logText:(NSString *)_txt
1149 NSException *exception;
1154 if (ProfileImapEnabled == 1) {
1155 gettimeofday(&tv, NULL);
1156 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
1157 fprintf(stderr, "{");
1166 [self->context resetLastException];
1168 NSException *e = nil; // TODO: try to remove exception handler
1170 [self sendCommand:_command withTag:_tag logText:_txt];
1171 map = [self->parser parseResponseForTagId:self->tagId exception:&e];
1176 if ([localException isKindOfClass:[NGImap4ParserException class]]) {
1177 [[self _processCommandParserException:localException] raise];
1179 else if ([self _isShutdownException:localException]) {
1180 [[self _handleShutdownDuringCommandException:localException] raise];
1183 [[self _processUnknownCommandParserException:localException] raise];
1184 if (reconnectCnt == 0) {
1185 if (![self _isLoginCommand:_command]) {
1188 exception = localException;
1191 [self closeConnection];
1192 [self->context setLastException:localException];
1200 else if ([map objectForKey:@"bye"] && ![_command hasPrefix:@"logout"]) {
1201 if (reconnectCnt == 0) {
1207 } while (tryReconnect);
1209 if ([self->context lastException]) {
1211 [self->context setLastException:exception];
1215 if (_notification) [self sendResponseNotification:map];
1217 if (ProfileImapEnabled == 1) {
1218 gettimeofday(&tv, NULL);
1219 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
1220 fprintf(stderr, "}[%s] <Send Command [%s]> : time needed: %4.4fs\n",
1221 __PRETTY_FUNCTION__, [_command cString], ti < 0.0 ? -1.0 : ti);
1226 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
1227 withNotification:(BOOL)_notification
1229 return [self processCommand:_command withTag:_tag
1230 withNotification:_notification
1234 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag {
1235 return [self processCommand:_command withTag:_tag withNotification:YES
1239 - (NGHashMap *)processCommand:(NSString *)_command {
1240 return [self processCommand:_command withTag:YES withNotification:YES
1244 - (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt {
1245 return [self processCommand:_command withTag:YES withNotification:YES
1249 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag
1250 logText:(NSString *)_txt
1253 NGCTextStream *txtStream;
1255 txtStream = [self textStream];
1260 command = [NSString stringWithFormat:@"%d %@", self->tagId, _command];
1262 _txt = [NSString stringWithFormat:@"%d %@", self->tagId, _txt];
1269 if ([_txt length] > 5000) {
1270 fprintf(stderr, "C[%p]: %s...\n", self, [[_txt substringToIndex:5000]
1274 fprintf(stderr, "C[%p]: %s\n", self, [_txt cString]);
1278 if (![txtStream writeString:command])
1279 [self->context setLastException:[txtStream lastException]];
1280 else if (![txtStream writeString:@"\r\n"])
1281 [self->context setLastException:[txtStream lastException]];
1282 else if (![txtStream flush])
1283 [self->context setLastException:[txtStream lastException]];
1286 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag {
1287 [self sendCommand:_command withTag:_tag logText:_command];
1290 - (void)sendCommand:(NSString *)_command {
1291 [self sendCommand:_command withTag:YES logText:_command];
1294 - (NSArray *)_flags2ImapFlags:(NSArray *)_flags {
1295 /* adds backslashes in front of the flags */
1296 NSEnumerator *enumerator;
1302 objs = calloc([_flags count] + 2, sizeof(id));
1304 enumerator = [_flags objectEnumerator];
1305 while ((obj = [enumerator nextObject])) {
1306 objs[cnt] = [@"\\" stringByAppendingString:obj];
1309 result = [NSArray arrayWithObjects:objs count:cnt];
1310 if (objs != NULL) free(objs);
1313 static inline NSArray *_flags2ImapFlags(NGImap4Client *self, NSArray *_flags) {
1314 return [self _flags2ImapFlags:_flags];
1317 - (NSString *)_folder2ImapFolder:(NSString *)_folder {
1320 if (self->delimiter == nil) {
1323 res = [self list:@"" pattern:@""];
1325 if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
1329 array = [_folder pathComponents];
1331 if ([array count] > 0) {
1334 o = [array objectAtIndex:0];
1335 if (([o isEqualToString:@"/"]) || ([o length] == 0))
1336 array = [array subarrayWithRange:NSMakeRange(1, [array count] - 1)];
1338 o = [array lastObject];
1339 if (([o length] == 0) || ([o isEqualToString:@"/"]))
1340 array = [array subarrayWithRange:NSMakeRange(0, [array count] - 1)];
1342 return [[array componentsJoinedByString:self->delimiter]
1343 stringByEncodingImap4FolderName];
1346 - (NSString *)_imapFolder2Folder:(NSString *)_folder {
1349 array = [NSArray arrayWithObject:@""];
1351 if ([self delimiter] == nil) {
1354 res = [self list:@"" pattern:@""]; // fill the delimiter ivar?
1355 if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
1359 array = [array arrayByAddingObjectsFromArray:
1360 [_folder componentsSeparatedByString:[self delimiter]]];
1362 return [[NSString pathWithComponents:array] stringByDecodingImap4FolderName];
1365 - (void)setContext:(NGImap4Context *)_ctx {
1366 self->context = _ctx;
1368 - (NGImap4Context *)context {
1369 return self->context;
1372 /* ConnectionRegistration */
1374 - (void)removeFromConnectionRegister {
1377 for (cnt = 0; cnt < MaxImapClients; cnt++) {
1378 if (ImapClients[cnt] == self)
1379 ImapClients[cnt] = nil;
1383 - (void)registerConnection {
1386 cnt = CountClient % MaxImapClients;
1388 if (ImapClients[cnt]) {
1389 [(NGImap4Context *)ImapClients[cnt] closeConnection];
1391 ImapClients[cnt] = self;
1395 - (id<NGExtendedTextStream>)textStream {
1396 if (self->text == nil) {
1397 if ([self->context lastException] == nil)
1405 - (NSString *)description {
1406 NSMutableString *ms;
1409 ms = [NSMutableString stringWithCapacity:128];
1410 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
1412 if (self->login != nil)
1413 [ms appendFormat:@" login=%@%s", self->login, self->password?"(pwd)":""];
1415 if ((tmp = [self socket]) != nil)
1416 [ms appendFormat:@" socket=%@", tmp];
1417 else if (self->address)
1418 [ms appendFormat:@" address=%@", self->address];
1420 [ms appendString:@">"];
1424 @end /* NGImap4Client; */