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 {
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;
614 if ([_folder length] == 0)
616 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
619 self->selectedFolder = [_folder copy];
621 [tmp release]; tmp = nil;
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 {
697 NSAutoreleasePool *pool;
699 NSDictionary *result;
702 pool = [[NSAutoreleasePool alloc] init];
703 cmd = [NSString stringWithFormat:@"uid fetch %@ (%@)",
704 [self _uidsJoinedForFetchCmd:_uids],
705 [self _partsJoinedForFetchCmd:_parts]];
707 fetchres = [self processCommand:cmd];
708 result = [[self->normer normalizeFetchResponse:fetchres] retain];
711 return [result autorelease];
714 - (NSDictionary *)fetchUid:(unsigned)_uid parts:(NSArray *)_parts {
715 // TODO: describe what exactly this can return!
716 NSAutoreleasePool *pool;
718 NSDictionary *result;
721 pool = [[NSAutoreleasePool alloc] init];
722 cmd = [NSString stringWithFormat:@"uid fetch %d (%@)", _uid,
723 [self _partsJoinedForFetchCmd:_parts]];
724 fetchres = [self processCommand:cmd];
725 result = [[self->normer normalizeFetchResponse:fetchres] retain];
728 return [result autorelease];
731 - (NSDictionary *)fetchFrom:(unsigned)_from to:(unsigned)_to
732 parts:(NSArray *)_parts
735 NSAutoreleasePool *pool;
736 NSMutableString *cmd;
737 NSDictionary *result;
738 NGHashMap *rawResult;
746 pool = [[NSAutoreleasePool alloc] init];
750 cmd = [NSMutableString stringWithCapacity:256];
751 [cmd appendString:@"fetch "];
752 [cmd appendFormat:@"%d:%d (", _from, _to];
753 for (i = 0, count = [_parts count]; i < count; i++) {
754 if (i != 0) [cmd appendString:@" "];
755 [cmd appendString:[_parts objectAtIndex:i]];
757 [cmd appendString:@")"];
759 if (fetchDebug) NSLog(@"%s: process: %@", __PRETTY_FUNCTION__, cmd);
760 rawResult = [self processCommand:cmd];
762 RawResult is a dict containing keys:
763 ResponseResult: dict eg: {descripted=Completed;result=ok;tagId=8;}
764 fetch: array of record dicts (eg "rfc822.header" key)
767 if (fetchDebug) NSLog(@"%s: normalize: %@", __PRETTY_FUNCTION__,rawResult);
768 result = [[self->normer normalizeFetchResponse:rawResult] retain];
769 if (fetchDebug) NSLog(@"%s: normalized: %@", __PRETTY_FUNCTION__, result);
772 if (fetchDebug) NSLog(@"%s: pool done.", __PRETTY_FUNCTION__);
773 return [result autorelease];
776 - (NSDictionary *)storeUid:(unsigned)_uid add:(NSNumber *)_add
777 flags:(NSArray *)_flags
779 NSString *icmd, *iflags;
781 iflags = [_flags2ImapFlags(self, _flags) componentsJoinedByString:@" "];
782 icmd = [NSString stringWithFormat:@"uid store %d %cFLAGS (%@)",
783 _uid, [_add boolValue] ? '+' : '-',
785 return [self->normer normalizeResponse:[self processCommand:icmd]];
788 - (NSDictionary *)storeFrom:(unsigned)_from to:(unsigned)_to
790 flags:(NSArray *)_flags
800 flagstr = [_flags2ImapFlags(self, _flags) componentsJoinedByString:@" "];
801 cmd = [NSString stringWithFormat:@"store %d:%d %cFLAGS (%@)",
802 _from, _to, [_add boolValue] ? '+' : '-', flagstr];
804 return [self->normer normalizeResponse:[self processCommand:cmd]];
807 - (NSDictionary *)copyFrom:(unsigned)_from to:(unsigned)_to
808 toFolder:(NSString *)_folder
816 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
819 cmd = [NSString stringWithFormat:@"copy %d:%d \"%@\"", _from, _to, _folder];
820 return [self->normer normalizeResponse:[self processCommand:cmd]];
823 - (NSDictionary *)copyUid:(unsigned)_uid toFolder:(NSString *)_folder {
826 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
829 cmd = [NSString stringWithFormat:@"uid copy %d \"%@\"", _uid, _folder];
831 return [self->normer normalizeResponse:[self processCommand:cmd]];
834 - (NSDictionary *)getQuotaRoot:(NSString *)_folder {
837 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
840 cmd = [NSString stringWithFormat:@"getquotaroot \"%@\"", _folder];
841 return [self->normer normalizeQuotaResponse:[self processCommand:cmd]];
844 - (NSDictionary *)append:(NSData *)_message toFolder:(NSString *)_folder
845 withFlags:(NSArray *)_flags
849 NSString *message, *icmd;
851 flags = _flags2ImapFlags(self, _flags);
852 if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
856 /* Remove bare newlines */
864 old = [_message bytes];
865 len = [_message length];
867 new = calloc(len * 2 + 4, sizeof(char));
869 while (cntOld < (len - 1)) {
870 if (old[cntOld] == '\n') {
871 new[cntNew++] = '\r';
872 new[cntNew++] = '\n';
874 else if (old[cntOld] != '\r') {
875 new[cntNew++] = old[cntOld];
879 if (old[cntOld] == '\n') {
880 new[cntNew++] = '\r';
881 new[cntNew++] = '\n';
883 else if (old[cntOld] != '\r') {
884 new[cntNew++] = old[cntOld];
886 message = [(NSString *)[NSString alloc]
887 initWithCString:new length:cntNew];
888 if (new) free(new); new = NULL;
891 icmd = [NSString stringWithFormat:@"append \"%@\" (%@) {%d}",
893 [flags componentsJoinedByString:@" "],
894 [message cStringLength]];
895 result = [self processCommand:icmd
896 withTag:YES withNotification:NO];
898 if ([[result objectForKey:@"ContinuationResponse"] boolValue])
899 result = [self processCommand:message withTag:NO];
901 [message release]; message = nil;
903 return [self->normer normalizeResponse:result];
906 - (void)_handleSearchExprIssue:(NSString *)reason qualifier:(EOQualifier *)_q {
908 NSException *exception = nil;
911 if (PreventExceptions != 0)
914 if (_q == nil) _q = (id)[NSNull null];
916 descr = @"Could not process qualifier for imap search ";
917 descr = [descr stringByAppendingString:reason];
919 exception = [[NGImap4SearchException alloc] initWithFormat:@"%@", descr];
920 ui = [NSDictionary dictionaryWithObject:_q forKey:@"qualifier"];
921 [exception setUserInfo:ui];
922 [self->context setLastException:exception];
926 - (NSString *)_searchExprForQual:(EOQualifier *)_qualifier {
931 ' TEXT "why SOPE rocks"'
935 if (_qualifier == nil)
938 result = [_qualifier imap4SearchString];
939 if ([result isKindOfClass:[NSException class]]) {
940 [self _handleSearchExprIssue:[(NSException *)result reason]
941 qualifier:_qualifier];
947 - (NSDictionary *)threadBySubject:(BOOL)_bySubject
948 charset:(NSString *)_charSet
951 http://www.ietf.org/proceedings/03mar/I-D/draft-ietf-imapext-thread-12.txt
953 Returns an array of uids in sort order.
956 _bySubject - if yes, use "REFERENCES" else "ORDEREDSUBJECT" (TODO: ?!)
957 _charSet - default: "UTF-8"
960 UID THREAD REFERENCES|ORDEREDSUBJECT UTF-8 ALL
965 threadAlg = (_bySubject)
969 if ([_charSet length] == 0)
972 threadStr = [NSString stringWithFormat:@"UID THREAD %@ %@ ALL",
973 threadAlg, _charSet];
975 return [self->normer normalizeThreadResponse:
976 [self processCommand:threadStr]];
979 - (NSString *)_generateIMAP4SortOrdering:(EOSortOrdering *)_sortOrdering {
983 key = [_sortOrdering key];
984 if ([key length] == 0)
987 if (![AllowedSortKeys containsObject:[key uppercaseString]]) {
988 [self logWithFormat:@"ERROR[%s] key %@ is not allowed here!",
989 __PRETTY_FUNCTION__, key];
993 sel = [_sortOrdering selector];
994 if (sel_eq(sel, EOCompareDescending) ||
995 sel_eq(sel, EOCompareCaseInsensitiveDescending)) {
996 return [@"REVERSE " stringByAppendingString:key];
998 // TODO: check other selectors whether they make sense instead of silent acc.
1003 - (NSString *)_generateIMAP4SortOrderings:(NSArray *)_sortOrderings {
1005 turn EOSortOrdering into an IMAP4 value for "SORT()"
1007 eg: "DATE REVERSE SUBJECT"
1009 It also checks a set of allowed sort-keys (don't know why)
1011 NSMutableString *sortStr;
1016 if ([_sortOrderings count] == 0)
1019 sortStr = [NSMutableString stringWithCapacity:128];
1020 soe = [_sortOrderings objectEnumerator];
1021 while ((so = [soe nextObject])) {
1024 s = [self _generateIMAP4SortOrdering:so];
1031 [sortStr appendString:@" "];
1033 [sortStr appendString:s];
1035 return isFirst ? nil : sortStr;
1038 - (NSDictionary *)primarySort:(NSString *)_sort
1039 qualifierString:(NSString *)_qualString
1040 encoding:(NSString *)_encoding
1043 http://www.ietf.org/internet-drafts/draft-ietf-imapext-sort-17.txt
1045 The result dict contains the following keys:
1046 'result' - a boolean
1047 'expunge' - array (of what?)
1048 'sort' - array of uids in sort order
1049 'RawResponse' - the raw IMAP4 response
1051 Eg: UID SORT ( DATE REVERSE SUBJECT ) UTF-8 TODO
1053 NSMutableString *sortStr;
1055 if (![_encoding isNotNull]) _encoding = @"UTF-8";
1056 if (![_qualString isNotNull]) _qualString = @" ALL";
1058 sortStr = [NSMutableString stringWithCapacity:128];
1060 [sortStr appendString:@"UID SORT ("];
1061 if (_sort != nil) [sortStr appendString:_sort];
1062 [sortStr appendString:@") "];
1064 [sortStr appendString:_encoding]; /* eg 'UTF-8' */
1066 /* Note: this is _space sensitive_! to many spaces lead to error! */
1067 [sortStr appendString:_qualString]; /* eg ' ALL' or ' TEXT "abc"' */
1069 return [self->normer normalizeSortResponse:[self processCommand:sortStr]];
1072 - (NSDictionary *)sort:(id)_sortSpec
1073 qualifier:(EOQualifier *)_qual
1074 encoding:(NSString *)_encoding
1077 http://www.ietf.org/internet-drafts/draft-ietf-imapext-sort-17.txt
1079 The _sortSpec can be:
1080 - a simple 'raw' IMAP4 sort string
1082 - an array of EOSortOrderings
1084 The result dict contains the following keys:
1085 'result' - a boolean
1086 'expunge' - array (of what?)
1087 'sort' - array of uids in sort order
1088 'RawResponse' - the raw IMAP4 response
1090 If no sortable key was found, the sort will run against 'DATE'.
1091 => TODO: this is inconsistent. If none are passed in, false will be
1094 Eg: UID SORT ( DATE REVERSE SUBJECT ) UTF-8 TODO
1098 if ([_sortSpec isKindOfClass:[NSArray class]])
1099 tmp = [self _generateIMAP4SortOrderings:_sortSpec];
1100 else if ([_sortSpec isKindOfClass:[EOSortOrdering class]])
1101 tmp = [self _generateIMAP4SortOrdering:_sortSpec];
1103 tmp = [_sortSpec stringValue];
1105 if ([tmp length] == 0) { /* found no valid key use date sorting */
1106 [self logWithFormat:@"Note: no key found for sorting, using 'DATE': %@",
1111 return [self primarySort:tmp
1112 qualifierString:[self _searchExprForQual:_qual]
1113 encoding:_encoding];
1115 - (NSDictionary *)sort:(NSArray *)_sortOrderings
1116 qualifier:(EOQualifier *)_qual
1118 // DEPRECATED, should not use context!
1119 return [self sort:_sortOrderings qualifier:_qual
1120 encoding:[[self context] sortEncoding]];
1123 - (NSDictionary *)searchWithQualifier:(EOQualifier *)_qualifier {
1126 s = [self _searchExprForQual:_qualifier];
1127 if ([s length] == 0) {
1128 // TODO: should set last-exception?
1129 [self logWithFormat:@"ERROR(%s): could not process search qualifier: %@",
1130 __PRETTY_FUNCTION__, _qualifier];
1134 s = [@"search" stringByAppendingString:s];
1135 return [self->normer normalizeSearchResponse:[self processCommand:s]];
1138 /* Private Methods */
1140 - (NSException *)_processCommandParserException:(NSException *)_exception {
1141 NSLog(@"ERROR(%s): catched IMAP4 parser exception %@: %@",
1142 __PRETTY_FUNCTION__, [_exception name], [_exception reason]);
1143 [self closeConnection];
1144 [self->context setLastException:_exception];
1147 - (NSException *)_processUnknownCommandParserException:(NSException *)_ex {
1148 NSLog(@"ERROR(%s): catched non-IMAP4 parsing exception %@: %@",
1149 __PRETTY_FUNCTION__, [_ex name], [_ex reason]);
1153 - (NSException *)_handleShutdownDuringCommandException:(NSException *)_ex {
1154 NSLog(@"ERROR(%s): IMAP4 socket was shut down by server %@: %@",
1155 __PRETTY_FUNCTION__, [_ex name], [_ex reason]);
1156 [self closeConnection];
1157 [self->context setLastException:_ex];
1161 - (BOOL)_isShutdownException:(NSException *)_ex {
1162 return [[_ex name] isEqualToString:@"NGSocketShutdownDuringReadException"];
1165 - (BOOL)_isLoginCommand:(NSString *)_command {
1166 return [_command hasPrefix:@"login"];
1169 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
1170 withNotification:(BOOL)_notification logText:(NSString *)_txt
1175 NSException *exception;
1180 if (ProfileImapEnabled == 1) {
1181 gettimeofday(&tv, NULL);
1182 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
1183 fprintf(stderr, "{");
1192 [self->context resetLastException];
1194 NSException *e = nil; // TODO: try to remove exception handler
1196 [self sendCommand:_command withTag:_tag logText:_txt];
1197 map = [self->parser parseResponseForTagId:self->tagId exception:&e];
1202 if ([localException isKindOfClass:[NGImap4ParserException class]]) {
1203 [[self _processCommandParserException:localException] raise];
1205 else if ([self _isShutdownException:localException]) {
1206 [[self _handleShutdownDuringCommandException:localException] raise];
1209 [[self _processUnknownCommandParserException:localException] raise];
1210 if (reconnectCnt == 0) {
1211 if (![self _isLoginCommand:_command]) {
1214 exception = localException;
1217 [self closeConnection];
1218 [self->context setLastException:localException];
1226 else if ([map objectForKey:@"bye"] && ![_command hasPrefix:@"logout"]) {
1227 if (reconnectCnt == 0) {
1233 } while (tryReconnect);
1235 if ([self->context lastException]) {
1237 [self->context setLastException:exception];
1241 if (_notification) [self sendResponseNotification:map];
1243 if (ProfileImapEnabled == 1) {
1244 gettimeofday(&tv, NULL);
1245 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
1246 fprintf(stderr, "}[%s] <Send Command [%s]> : time needed: %4.4fs\n",
1247 __PRETTY_FUNCTION__, [_command cString], ti < 0.0 ? -1.0 : ti);
1252 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
1253 withNotification:(BOOL)_notification
1255 return [self processCommand:_command withTag:_tag
1256 withNotification:_notification
1260 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag {
1261 return [self processCommand:_command withTag:_tag withNotification:YES
1265 - (NGHashMap *)processCommand:(NSString *)_command {
1266 return [self processCommand:_command withTag:YES withNotification:YES
1270 - (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt {
1271 return [self processCommand:_command withTag:YES withNotification:YES
1275 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag
1276 logText:(NSString *)_txt
1279 NGCTextStream *txtStream;
1281 txtStream = [self textStream];
1286 command = [NSString stringWithFormat:@"%d %@", self->tagId, _command];
1288 _txt = [NSString stringWithFormat:@"%d %@", self->tagId, _txt];
1295 if ([_txt length] > 5000) {
1296 fprintf(stderr, "C[%p]: %s...\n", self, [[_txt substringToIndex:5000]
1300 fprintf(stderr, "C[%p]: %s\n", self, [_txt cString]);
1304 if (![txtStream writeString:command])
1305 [self->context setLastException:[txtStream lastException]];
1306 else if (![txtStream writeString:@"\r\n"])
1307 [self->context setLastException:[txtStream lastException]];
1308 else if (![txtStream flush])
1309 [self->context setLastException:[txtStream lastException]];
1312 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag {
1313 [self sendCommand:_command withTag:_tag logText:_command];
1316 - (void)sendCommand:(NSString *)_command {
1317 [self sendCommand:_command withTag:YES logText:_command];
1320 static inline NSArray *_flags2ImapFlags(NGImap4Client *self, NSArray *_flags) {
1321 NSEnumerator *enumerator;
1327 objs = calloc([_flags count] + 2, sizeof(id));
1329 enumerator = [_flags objectEnumerator];
1330 while ((obj = [enumerator nextObject])) {
1331 objs[cnt] = [@"\\" stringByAppendingString:obj];
1334 result = [NSArray arrayWithObjects:objs count:cnt];
1335 if (objs) free(objs);
1339 - (NSString *)_folder2ImapFolder:(NSString *)_folder {
1342 if (self->delimiter == nil) {
1345 res = [self list:@"" pattern:@""];
1347 if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
1351 array = [_folder pathComponents];
1353 if ([array count] > 0) {
1356 o = [array objectAtIndex:0];
1357 if (([o isEqualToString:@"/"]) || ([o length] == 0))
1358 array = [array subarrayWithRange:NSMakeRange(1, [array count] - 1)];
1360 o = [array lastObject];
1361 if (([o length] == 0) || ([o isEqualToString:@"/"]))
1362 array = [array subarrayWithRange:NSMakeRange(0, [array count] - 1)];
1364 return [[array componentsJoinedByString:self->delimiter]
1365 stringByEncodingImap4FolderName];
1368 - (NSString *)_imapFolder2Folder:(NSString *)_folder {
1371 array = [NSArray arrayWithObject:@""];
1373 if ([self delimiter] == nil) {
1376 res = [self list:@"" pattern:@""]; // fill the delimiter ivar?
1377 if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
1381 array = [array arrayByAddingObjectsFromArray:
1382 [_folder componentsSeparatedByString:[self delimiter]]];
1384 return [[NSString pathWithComponents:array] stringByDecodingImap4FolderName];
1387 - (void)setContext:(NGImap4Context *)_ctx {
1388 self->context = _ctx;
1390 - (NGImap4Context *)context {
1391 return self->context;
1394 /* ConnectionRegistration */
1396 - (void)removeFromConnectionRegister {
1399 for (cnt = 0; cnt < MaxImapClients; cnt++) {
1400 if (ImapClients[cnt] == self)
1401 ImapClients[cnt] = nil;
1405 - (void)registerConnection {
1408 cnt = CountClient % MaxImapClients;
1410 if (ImapClients[cnt]) {
1411 [(NGImap4Context *)ImapClients[cnt] closeConnection];
1413 ImapClients[cnt] = self;
1417 - (id<NGExtendedTextStream>)textStream {
1418 if (self->text == nil) {
1419 if ([self->context lastException] == nil)
1427 - (NSString *)description {
1428 NSMutableString *ms;
1431 ms = [NSMutableString stringWithCapacity:128];
1432 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
1434 if (self->login != nil)
1435 [ms appendFormat:@" login=%@%s", self->login, self->password?"(pwd)":""];
1437 if ((tmp = [self socket]) != nil)
1438 [ms appendFormat:@" socket=%@", tmp];
1439 else if (self->address)
1440 [ms appendFormat:@" address=%@", self->address];
1442 [ms appendString:@">"];
1446 @end /* NGImap4Client; */