]> err.no Git - sope/blob - sope-mime/NGImap4/NGImap4Client.m
new Xcode projects
[sope] / sope-mime / NGImap4 / NGImap4Client.m
1 /*
2   Copyright (C) 2000-2004 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
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
9   later version.
10
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.
15
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
19   02111-1307, USA.
20 */
21
22 #include <unistd.h>
23
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"
32 #include "imCommon.h"
33 #include <sys/time.h>
34 #include "imTimeMacros.h"
35
36 @interface EOQualifier(IMAPAdditions)
37 - (NSString *)imap4SearchString;
38 @end
39
40 @interface NGImap4Client(ConnectionRegistration)
41
42 - (void)removeFromConnectionRegister;
43 - (void)registerConnection;
44 - (NGCTextStream *)textStream;
45
46 @end /* NGImap4Client(ConnectionRegistration); */
47
48 #if GNUSTEP_BASE_LIBRARY
49 /* FIXME: TODO: move someplace better (hh: NGExtensions...) */
50 @implementation NSException(setUserInfo)
51
52 - (id)setUserInfo:(NSDictionary *)_userInfo {
53   ASSIGN(self->_e_info, _userInfo);
54   return self;
55 }
56
57 @end /* NSException(setUserInfo) */
58 #endif
59
60 @interface NGImap4Client(Private)
61
62 - (NSString *)_folder2ImapFolder:(NSString *)_folder;
63
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;
69
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;
74
75 - (void)sendResponseNotification:(NGHashMap *)map;
76
77 - (NSDictionary *)login;
78
79 @end
80
81 /*
82   An implementation of an Imap4 client
83   
84   A folder name always looks like an absolute filename (/inbox/doof) 
85 */
86
87 @implementation NGImap4Client
88
89 // TODO: replace?
90 static inline NSArray *_flags2ImapFlags(NGImap4Client *, NSArray *);
91
92 static NSNumber *YesNumber     = nil;
93 static NSNumber *NoNumber      = nil;
94
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;
104
105 - (BOOL)useSSL {
106   return self->useSSL;
107 }
108
109 + (int)version {
110   return 2;
111 }
112 + (void)initialize {
113   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
114   static BOOL didInit = NO;
115   if (didInit) return;
116   didInit = YES;
117   
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"];
122   
123   YesNumber = [[NSNumber numberWithBool:YES] retain];
124   NoNumber  = [[NSNumber numberWithBool:NO]  retain];
125   
126   if (MaxImapClients < 1) {
127     MaxImapClients = [ud integerForKey:@"NGImapMaxConnectionCount"];
128     if (MaxImapClients < 1) MaxImapClients = 50;
129   }
130   if (ImapClients == NULL)
131     ImapClients = calloc(MaxImapClients + 2, sizeof(id));
132
133   AllowedSortKeys = [[NSArray alloc] initWithObjects:
134                                          @"ARRIVAL", @"CC", @"DATE", @"FROM",
135                                          @"SIZE", @"SUBJECT", @"TO", nil];
136 }
137
138 /* constructors */
139
140 + (id)clientWithURL:(NSURL *)_url {
141   return [[(NGImap4Client *)[self alloc] initWithURL:_url] autorelease];
142 }
143 + (id)clientWithAddress:(id<NGSocketAddress>)_address {
144   return
145     [[(NGImap4Client *)[self alloc] initWithAddress:_address] autorelease];
146 }
147
148 + (id)clientWithHost:(id)_host {
149   return [[[self alloc] initWithHost:_host] autorelease];
150 }
151
152 - (id)initWithHost:(id)_host {
153   NGInternetSocketAddress *a;
154   
155   a = [NGInternetSocketAddress addressWithPort:143 onHost:_host];
156   return [self initWithAddress:a];
157 }
158 - (id)initWithURL:(NSURL *)_url {
159   NGInternetSocketAddress *a;
160   int port;
161   id  tmp;
162   
163   if ((self->useSSL = [[_url scheme] isEqualToString:@"imaps"])) {
164     if (NSClassFromString(@"NGActiveSSLSocket") == nil) {
165       [self logWithFormat:
166             @"no SSL support available, cannot connect: %@", _url];
167       [self release];
168       return nil;
169     }
170   }
171   if ((tmp = [_url port])) {
172     port = [tmp intValue];
173     if (port <= 0) port = self->useSSL ? 993 : 143;
174   }
175   else
176     port = self->useSSL ? 993 : 143;
177   
178   self->login    = [[_url user]     copy];
179   self->password = [[_url password] copy];
180   
181   a = [NGInternetSocketAddress addressWithPort:port onHost:[_url host]];
182   return [self initWithAddress:a];
183 }
184
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];
191   }
192   return self;
193 }
194
195 - (void)dealloc {
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];
208   
209   self->context = nil; /* not retained */
210   [super dealloc];
211 }
212
213 /* equality (required for adding clients to Foundation sets) */
214
215 - (BOOL)isEqual:(id)_obj {
216   if (_obj == self)
217     return YES;
218   
219   if ([_obj isKindOfClass:[NGImap4Client class]])
220     return [self isEqualToClient:_obj];
221   
222   return NO;
223 }
224
225 - (BOOL)isEqualToClient:(NGImap4Client *)_obj {
226   if (_obj == self) return YES;
227   if (_obj == nil)  return NO;
228   
229   return [[_obj address] isEqual:self->address];
230 }
231
232 /* accessors */
233
234 - (id<NGActiveSocket>)socket {
235   return self->socket;
236 }
237
238 - (id<NGSocketAddress>)address {
239   return self->address;
240 }
241
242 - (NSString *)delimiter {
243   return self->delimiter;
244 }
245
246 - (EOGlobalID *)serverGlobalID {
247   NGInternetSocketAddress *is;
248   
249   if (self->serverGID)
250     return self->serverGID;
251   
252   is = (id)[self address];
253   
254   self->serverGID = [[NGImap4ServerGlobalID alloc]
255                       initWithHostname:[is hostName]
256                       port:[is port]
257                       login:self->login];
258   return self->serverGID;
259 }
260
261 /* connection */
262
263 - (id)_openSocket {
264   Class socketClass = Nil;
265   id sock;
266
267   socketClass = [self useSSL] 
268     ? NSClassFromString(@"NGActiveSSLSocket")
269     : [NGActiveSocket class];
270   
271   NS_DURING {
272     sock = [socketClass socketConnectedToAddress:self->address];
273   }
274   NS_HANDLER {
275     [self->context setLastException:localException];
276     sock = nil;
277   }
278   NS_ENDHANDLER;
279   
280   return sock;
281 }
282
283 - (NSDictionary *)_receiveServerGreetingWithoutTagId {
284   NSDictionary *res = nil;
285   
286   NS_DURING {
287     NSException *e;
288     NGHashMap *hm;
289     
290     hm = [self->parser parseResponseForTagId:-1 exception:&e];
291     [e raise];
292     res = [self->normer normalizeOpenConnectionResponse:hm];
293   }
294   NS_HANDLER
295     [self->context setLastException:localException];
296   NS_ENDHANDLER;
297   
298   if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
299     return nil;
300   
301   return res;
302 }
303
304 - (NSDictionary *)_openConnection {
305   /* open connection as configured */
306   NGBufferedStream *buffer;
307   struct timeval tv;
308   double         ti = 0.0;
309   
310   if (ProfileImapEnabled == 1) {
311     gettimeofday(&tv, NULL);
312     ti =  (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
313   }
314   [self->socket release]; self->socket = nil;
315   [self->parser release]; self->parser = nil;
316   [self->text   release]; self->text   = nil;
317   
318   [self->context resetLastException];
319
320   if ((self->socket = [[self _openSocket] retain]) == nil)
321     return nil;
322   if ([self->context lastException])
323     return nil;
324   
325   buffer     = 
326     [(NGBufferedStream *)[NGBufferedStream alloc] initWithSource:self->socket];
327   self->text = [(NGCTextStream *)[NGCTextStream alloc] initWithSource:buffer];
328   [buffer release]; buffer = nil;
329   
330   self->parser = [[NGImap4ResponseParser alloc] initWithStream:self->socket];
331   self->tagId  = 0;
332   
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);    
338   }
339   [self registerConnection];
340   [self->context resetLastException];
341   
342   return [self _receiveServerGreetingWithoutTagId];
343 }
344
345 - (NSDictionary *)openConnection {
346   return [self _openConnection];
347 }
348
349 - (NSNumber *)isConnected {
350   /* 
351      Check whether stream is already open (could be closed because 
352      server-timeout) 
353   */
354   return (self->socket == nil)
355     ? NoNumber 
356     : ([(NGActiveSocket *)self->socket isAlive] ? YesNumber : NoNumber);
357 }
358
359 - (NSException *)_handleTextReleaseException:(NSException *)_ex {
360   [self logWithFormat:@"got exception during stream dealloc: %@", _ex];
361   return nil;
362 }
363 - (NSException *)_handleSocketCloseException:(NSException *)_ex {
364   [self logWithFormat:@"got exception during socket close: %@", _ex];
365   return nil;
366 }
367 - (NSException *)_handleSocketReleaseException:(NSException *)_ex {
368   [self logWithFormat:@"got exception during socket deallocation: %@", _ex];
369   return nil;
370 }
371 - (void)closeConnection {
372   /* close a connection */
373   
374   // TODO: this is a bit weird, probably because of the flush
375   //       maybe just call -close on the text stream?
376   NS_DURING
377     [self->text release]; 
378   NS_HANDLER
379     [[self _handleTextReleaseException:localException] raise];
380   NS_ENDHANDLER;
381   self->text = nil;
382   
383   NS_DURING
384     [self->socket close];
385   NS_HANDLER
386     [[self _handleSocketCloseException:localException] raise];
387   NS_ENDHANDLER;
388   
389   NS_DURING
390     [self->socket release];
391   NS_HANDLER
392     [[self _handleSocketReleaseException:localException] raise];
393   NS_ENDHANDLER;
394   self->socket = nil;
395   
396   [self->parser    release]; self->parser    = nil;
397   [self->delimiter release]; self->delimiter = nil;
398   [self removeFromConnectionRegister];
399 }
400
401 // ResponseNotifications
402
403 - (void)registerForResponseNotification:(id<NGImap4ResponseReceiver>)_obj {
404   [self->responseReceiver addObject:[NSValue valueWithNonretainedObject:_obj]];
405 }
406
407 - (void)removeFromResponseNotification:(id<NGImap4ResponseReceiver>)_obj {
408   [self->responseReceiver removeObject:
409        [NSValue valueWithNonretainedObject:_obj]];
410 }
411
412 - (void)sendResponseNotification:(NGHashMap *)_map {
413   NSValue                     *val;
414   id<NGImap4ResponseReceiver> obj;
415   NSEnumerator                *enumerator;
416   NSDictionary                *resp;
417
418   resp       = [self->normer normalizeResponse:_map];
419   enumerator = [self->responseReceiver objectEnumerator];
420
421   while ((val = [enumerator nextObject])) {
422     obj = [val nonretainedObjectValue];
423     [obj responseNotificationFrom:self response:resp];
424   }
425 }
426
427 /* commands */
428
429 - (NSDictionary *)login:(NSString *)_login password:(NSString *)_passwd {
430   /* login with plaintext password authenticating */
431
432   if ((_login == nil) || (_passwd == nil))
433     return nil;
434
435   [self->login     release]; self->login    = nil;
436   [self->password  release]; self->password = nil;
437   [self->serverGID release]; self->serverGID = nil;
438   
439   self->login    = [_login copy];
440   self->password = [[_passwd stringByEscapingImap4Password] copy];
441   
442   return [self login];
443 }
444
445 - (void)reconnect {
446   if ([self->context lastException] != nil)
447     return;
448     
449   [self closeConnection];  
450   self->tagId = 0;
451   [self openConnection];
452
453   if ([self->context lastException] != nil)
454     return;
455   
456   [self login];
457 }
458
459 - (NSDictionary *)login {
460   NGHashMap *map;
461   NSString  *s, *log;
462
463   if (self->isLogin )
464     return nil;
465   
466   self->isLogin = YES;
467
468   s = [NSString stringWithFormat:@"login \"%@\" \"%@\"",
469                   self->login, self->password];
470   log = [NSString stringWithFormat:@"login %@ <%@>",
471                     self->login,
472                     (self->password != nil) ? @"PASSWORD" : @"NO PASSWORD"];
473   map = [self processCommand:s logText:log];
474   
475   if (self->selectedFolder)
476     [self select:self->selectedFolder];
477   
478   self->isLogin = NO;
479   
480   return [self->normer normalizeResponse:map];
481 }
482
483 - (NSDictionary *)logout {
484   /* logout from the connected host and close the connection */
485   NGHashMap *map;
486
487   map = [self processCommand:@"logout"];
488   [self closeConnection];
489   
490   return [self->normer normalizeResponse:map];
491 }
492
493 /* Authenticated State */
494
495 - (NSDictionary *)list:(NSString *)_folder pattern:(NSString *)_pattern {
496   /*
497     The method build statements like 'LIST "_folder" "_pattern"'.
498     The Cyrus IMAP4 v1.5.14 server ignores the given folder.
499     Instead of you should use the pattern to get the expected result.
500     If folder is NIL it would be set to empty string ''.
501     If pattern is NIL it would be set to ''.
502   */
503   NSAutoreleasePool *pool;
504   NGHashMap         *map;
505   NSDictionary      *result;
506   NSString *s;
507   
508   pool = [[NSAutoreleasePool alloc] init];
509   
510   if (_folder  == nil) _folder  = @"";
511   if (_pattern == nil) _pattern = @"";
512   
513   if ([_folder length] > 0) {
514     if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
515       return nil;
516   }
517   
518
519   if ([_pattern length] > 0)
520     if (!(_pattern = [self _folder2ImapFolder:_pattern]))
521       return nil;
522   
523   s = [NSString stringWithFormat:@"list \"%@\" \"%@\"", _folder, _pattern];
524   map = [self processCommand:s];
525   
526   if (self->delimiter == nil) {
527     NSDictionary *rdel;
528     
529     rdel = [[map objectEnumeratorForKey:@"list"] nextObject];
530     self->delimiter = [[rdel objectForKey:@"delimiter"] copy];
531   }
532   
533   result = [[self->normer normalizeListResponse:map] copy];
534   [pool release];
535   return [result autorelease];
536 }
537
538 - (NSDictionary *)capability {
539   id capres;
540   capres = [self processCommand:@"capability"];
541   return [self->normer normalizeCapabilityRespone:capres];
542 }
543
544 - (NSDictionary *)lsub:(NSString *)_folder pattern:(NSString *)_pattern {
545   /*
546     The method build statements like 'LSUB "_folder" "_pattern"'.
547     The returnvalue is the same like the list:pattern: method
548   */
549   NGHashMap *map;
550   NSString  *s;
551
552   if (_folder == nil)
553     _folder = @"";
554
555   if ([_folder length] > 0) {
556     if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
557       return nil;
558   }
559   if (_pattern == nil)
560     _pattern = @"";
561
562   if ([_pattern length] > 0) {
563     if ((_pattern = [self _folder2ImapFolder:_pattern]) == nil)
564       return nil;
565   }
566   
567   s = [NSString stringWithFormat:@"lsub \"%@\" \"%@\"", _folder, _pattern];
568   map = [self processCommand:s];
569
570   if (self->delimiter == nil) {
571     NSDictionary *rdel;
572     
573     rdel = [[map objectEnumeratorForKey:@"LIST"] nextObject];
574     self->delimiter = [[rdel objectForKey:@"delimiter"] copy];
575   }
576   return [self->normer normalizeListResponse:map];
577 }
578
579 - (NSDictionary *)select:(NSString *)_folder {
580   NSString *s;
581   id tmp;
582
583   tmp = self->selectedFolder;
584
585   if ([_folder length] == 0)
586     return nil;
587   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
588     return nil;
589
590   self->selectedFolder = [_folder copy];
591   
592   [tmp release]; tmp = nil;
593
594   s = [NSString stringWithFormat:@"select \"%@\"", self->selectedFolder];
595   return [self->normer normalizeSelectResponse:[self processCommand:s]];
596 }
597
598 - (NSDictionary *)status:(NSString *)_folder flags:(NSArray *)_flags {
599   NSString *cmd;
600   
601   if (_folder == nil)
602     return nil;
603   if ((_flags == nil) || ([_flags count] == 0))
604     return nil;
605   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
606     return nil;
607   
608   cmd     = [NSString stringWithFormat:@"status \"%@\" (%@)",
609                       _folder, [_flags componentsJoinedByString:@" "]];
610   return [self->normer normalizeStatusResponse:[self processCommand:cmd]];
611 }
612
613 - (NSDictionary *)noop {
614   // at any state
615   return [self->normer normalizeResponse:[self processCommand:@"noop"]];
616 }
617
618 - (NSDictionary *)rename:(NSString *)_folder to:(NSString *)_newName {
619   NSString *cmd;
620   
621   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
622     return nil;
623   if ((_newName = [self _folder2ImapFolder:_newName]) == nil)
624     return nil;
625   
626   cmd = [NSString stringWithFormat:@"rename \"%@\" \"%@\"", _folder, _newName];
627   
628   return [self->normer normalizeResponse:[self processCommand:cmd]];
629 }
630
631 - (NSDictionary *)_performCommand:(NSString *)_op onFolder:(NSString *)_fname {
632   NSString *command;
633   
634   if ((_fname = [self _folder2ImapFolder:_fname]) == nil)
635     return nil;
636   
637   // eg: 'delete "blah"'
638   command = [NSString stringWithFormat:@"%@ \"%@\"", _op, _fname];
639   
640   return [self->normer normalizeResponse:[self processCommand:command]];
641 }
642
643 - (NSDictionary *)delete:(NSString *)_name {
644   return [self _performCommand:@"delete" onFolder:_name];
645 }
646 - (NSDictionary *)create:(NSString *)_name {
647   return [self _performCommand:@"create" onFolder:_name];
648 }
649 - (NSDictionary *)subscribe:(NSString *)_name {
650   return [self _performCommand:@"subscribe" onFolder:_name];
651 }
652 - (NSDictionary *)unsubscribe:(NSString *)_name {
653   return [self _performCommand:@"unsubscribe" onFolder:_name];
654 }
655
656 - (NSDictionary *)expunge {
657   return [self->normer normalizeResponse:[self processCommand:@"expunge"]];
658 }
659
660 - (NSString *)_uidsJoinedForFetchCmd:(NSArray *)_uids {
661   return [_uids componentsJoinedByString:@","];
662 }
663 - (NSString *)_partsJoinedForFetchCmd:(NSArray *)_parts {
664   return [_parts componentsJoinedByString:@" "];
665 }
666
667 - (NSDictionary *)fetchUids:(NSArray *)_uids parts:(NSArray *)_parts {
668   NSAutoreleasePool *pool;
669   NSString          *cmd;
670   NSDictionary      *result;
671   id fetchres;
672   
673   pool = [[NSAutoreleasePool alloc] init];
674   cmd  = [NSString stringWithFormat:@"uid fetch %@ (%@)",
675                    [self _uidsJoinedForFetchCmd:_uids],
676                    [self _partsJoinedForFetchCmd:_parts]];
677   
678   fetchres = [self processCommand:cmd];
679   result   = [[self->normer normalizeFetchResponse:fetchres] retain];
680   [pool release];
681   
682   return [result autorelease];
683 }
684
685 - (NSDictionary *)fetchUid:(unsigned)_uid parts:(NSArray *)_parts {
686   // TODO: describe what exactly this can return!
687   NSAutoreleasePool *pool;
688   NSString          *cmd;
689   NSDictionary      *result;
690   id fetchres;
691   
692   pool   = [[NSAutoreleasePool alloc] init];
693   cmd    = [NSString stringWithFormat:@"uid fetch %d (%@)", _uid,
694                      [self _partsJoinedForFetchCmd:_parts]];
695   fetchres = [self processCommand:cmd];
696   result   = [[self->normer normalizeFetchResponse:fetchres] retain];
697   
698   [pool release];
699   return [result autorelease];
700 }
701
702 - (NSDictionary *)fetchFrom:(unsigned)_from to:(unsigned)_to
703   parts:(NSArray *)_parts
704 {
705   // TODO: optimize
706   NSAutoreleasePool *pool;
707   NSMutableString   *cmd;
708   NSDictionary      *result; 
709   NGHashMap         *rawResult;
710  
711   if (_to == 0)
712     return [self noop];
713   
714   if (_from == 0)
715     _from = 1;
716
717   pool = [[NSAutoreleasePool alloc] init];
718   {
719     unsigned i, count;
720     
721     cmd = [NSMutableString stringWithCapacity:256];
722     [cmd appendString:@"fetch "];
723     [cmd appendFormat:@"%d:%d (", _from, _to];
724     for (i = 0, count = [_parts count]; i < count; i++) {
725       if (i != 0) [cmd appendString:@" "];
726       [cmd appendString:[_parts objectAtIndex:i]];
727     }
728     [cmd appendString:@")"];
729     
730     if (fetchDebug) NSLog(@"%s: process: %@", __PRETTY_FUNCTION__, cmd);
731     rawResult = [self processCommand:cmd];
732     /*
733       RawResult is a dict containing keys:
734         ResponseResult: dict    eg: {descripted=Completed;result=ok;tagId=8;}
735         fetch:          array of record dicts (eg "rfc822.header" key)
736     */
737     
738     if (fetchDebug) NSLog(@"%s: normalize: %@", __PRETTY_FUNCTION__,rawResult);
739     result = [[self->normer normalizeFetchResponse:rawResult] retain];
740     if (fetchDebug) NSLog(@"%s: normalized: %@", __PRETTY_FUNCTION__, result);
741   }
742   [pool release];
743   if (fetchDebug) NSLog(@"%s: pool done.", __PRETTY_FUNCTION__);
744   return [result autorelease];
745 }
746
747 - (NSDictionary *)storeUid:(unsigned)_uid add:(NSNumber *)_add
748   flags:(NSArray *)_flags
749 {
750   NSString *icmd, *iflags;
751   
752   iflags = [_flags2ImapFlags(self, _flags) componentsJoinedByString:@" "];
753   icmd   = [NSString stringWithFormat:@"uid store %d %cFLAGS (%@)",
754                      _uid, [_add boolValue] ? '+' : '-',
755                      iflags];
756   return [self->normer normalizeResponse:[self processCommand:icmd]];
757 }
758
759 - (NSDictionary *)storeFrom:(unsigned)_from to:(unsigned)_to
760   add:(NSNumber *)_add
761   flags:(NSArray *)_flags
762 {
763   NSString *cmd;
764   NSString *flagstr;
765
766   if (_to == 0)
767     return [self noop];
768   if (_from == 0)
769     _from = 1;
770
771   flagstr = [_flags2ImapFlags(self, _flags) componentsJoinedByString:@" "];
772   cmd = [NSString stringWithFormat:@"store %d:%d %cFLAGS (%@)",
773                     _from, _to, [_add boolValue] ? '+' : '-', flagstr];
774   
775   return [self->normer normalizeResponse:[self processCommand:cmd]];
776 }
777
778 - (NSDictionary *)copyFrom:(unsigned)_from to:(unsigned)_to
779   toFolder:(NSString *)_folder
780 {
781   NSString *cmd;
782
783   if (_to == 0)
784     return [self noop];
785   if (_from == 0)
786     _from = 1;
787   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
788     return nil;
789   
790   cmd = [NSString stringWithFormat:@"copy %d:%d \"%@\"", _from, _to, _folder];
791   return [self->normer normalizeResponse:[self processCommand:cmd]];
792 }
793
794 - (NSDictionary *)copyUid:(unsigned)_uid toFolder:(NSString *)_folder {
795   NSString *cmd;
796   
797   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
798     return nil;
799   
800   cmd = [NSString stringWithFormat:@"uid copy %d \"%@\"", _uid, _folder];
801   
802   return [self->normer normalizeResponse:[self processCommand:cmd]];
803 }
804
805 - (NSDictionary *)getQuotaRoot:(NSString *)_folder {
806   NSString *cmd;
807
808   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
809     return nil;
810   
811   cmd = [NSString stringWithFormat:@"getquotaroot \"%@\"", _folder];
812   return [self->normer normalizeQuotaResponse:[self processCommand:cmd]];
813 }
814
815 - (NSDictionary *)append:(NSData *)_message toFolder:(NSString *)_folder
816   withFlags:(NSArray *)_flags
817 {
818   NSArray   *flags;
819   NGHashMap *result;
820   NSString  *message, *icmd;
821
822   flags   = _flags2ImapFlags(self, _flags);
823   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
824     return nil;
825   
826
827   /* Remove bare newlines */
828   {
829     char       *new;
830     const char *old;
831     int         cntOld   = 0;
832     int         cntNew   = 0;
833     int         len      = 0;
834     
835     old = [_message bytes];
836     len = [_message length];
837     
838     new = calloc(len * 2 + 4, sizeof(char));
839     
840     while (cntOld < (len - 1)) {
841       if (old[cntOld] == '\n') {
842         new[cntNew++] = '\r';
843         new[cntNew++] = '\n';
844       }
845       else if (old[cntOld] != '\r') {
846         new[cntNew++] = old[cntOld];
847       }
848       cntOld++;
849     }
850     if (old[cntOld] == '\n') {
851       new[cntNew++] = '\r';
852       new[cntNew++] = '\n';
853     }
854     else if (old[cntOld] != '\r') {
855       new[cntNew++] = old[cntOld];
856     } 
857     message = [(NSString *)[NSString alloc] 
858                   initWithCString:new length:cntNew];
859     if (new) free(new); new = NULL;
860   }
861   
862   icmd = [NSString stringWithFormat:@"append \"%@\" (%@) {%d}",
863                      _folder,
864                      [flags componentsJoinedByString:@" "],
865                      [message cStringLength]];
866   result = [self processCommand:icmd
867                  withTag:YES withNotification:NO];
868   
869   if ([[result objectForKey:@"ContinuationResponse"] boolValue])
870     result = [self processCommand:message withTag:NO];
871
872   [message release]; message = nil;
873
874   return [self->normer normalizeResponse:result];
875 }
876
877 - (void)_handleSearchExprIssue:(NSString *)reason qualifier:(EOQualifier *)_q {
878   NSString     *descr;
879   NSException  *exception = nil;                                             
880   NSDictionary *ui;
881   
882   if (PreventExceptions != 0)
883     return;
884   
885   if (_q == nil) _q = (id)[NSNull null];                
886   
887   descr = @"Could not process qualifier for imap search "; 
888   descr = [descr stringByAppendingString:reason];           
889   
890   exception = [[NGImap4SearchException alloc] initWithFormat:@"%@", descr];    
891   ui = [NSDictionary dictionaryWithObject:_q forKey:@"qualifier"];
892   [exception setUserInfo:ui];
893   [self->context setLastException:exception];
894   [exception release];
895 }
896
897 - (NSString *)_searchExprForQual:(EOQualifier *)_qualifier {
898   id result;
899   
900   if (_qualifier == nil)
901     return @" ALL";
902   
903   result = [_qualifier imap4SearchString];
904   if ([result isKindOfClass:[NSException class]]) {
905     [self _handleSearchExprIssue:[(NSException *)result reason]
906           qualifier:_qualifier];
907     return nil;
908   }
909   return result;
910 }
911
912 /* siehe draft-ietf-imapext-thread-12.txt
913    returns an array of uids in sort order */
914
915 - (NSDictionary *)threadBySubject:(BOOL)_bySubject
916   charset:(NSString *)_charSet
917 {
918   NSString *threadStr;
919   NSString *threadAlg;
920
921   if (_bySubject)
922     threadAlg = @"REFERENCES";
923   else
924     threadAlg = @"ORDEREDSUBJECT";
925     
926   
927   if (![_charSet length])
928     _charSet = @"UTF-8";
929
930   threadStr = [NSString stringWithFormat:@"UID THREAD %@ %@ ALL",
931                       threadAlg, _charSet];
932
933   return [self->normer normalizeThreadResponse:
934                 [self processCommand:threadStr]];
935 }
936
937 - (NSDictionary *)sort:(NSArray *)_sortOrderings
938   qualifier:(EOQualifier *)_qual
939 {
940   /* siehe draft-ietf-imapext-sort */
941   /* returns an array of uids in sort order */
942   NSMutableString *sortStr;
943   NSEnumerator    *enumerator;
944   EOSortOrdering  *so;
945   BOOL            isFirst = YES;
946
947   sortStr = [NSMutableString stringWithCapacity:128];
948   
949   if ([_sortOrderings count] == 0)
950     return [NSDictionary dictionaryWithObjectsAndKeys:@"result", NoNumber,nil];
951   
952   enumerator = [_sortOrderings objectEnumerator];
953
954   [sortStr appendString:@"UID SORT ("];
955   
956   while ((so = [enumerator nextObject])) {
957     SEL      sel;
958     NSString *key;
959     
960     key = [[so key] uppercaseString];
961     
962     if ([key length] == 0)
963       continue;
964
965     if (![AllowedSortKeys containsObject:key]) {
966       [self logWithFormat:@"ERROR[%s] key %@ is not allowed here!",
967             __PRETTY_FUNCTION__, key];
968       continue;
969     }
970     if (isFirst)
971       isFirst = NO;
972     else
973       [sortStr appendString:@" "];
974     sel = [so selector];
975
976     if (sel_eq(sel, EOCompareDescending) ||
977         sel_eq(sel, EOCompareCaseInsensitiveDescending)) {
978       [sortStr appendString:@"REVERSE "];
979     }
980     [sortStr appendString:[so key]];
981   }
982   if (isFirst) { /* found no valid key use date sorting */
983     [sortStr appendString:@"DATE"];
984 #if 0    
985     return [NSDictionary dictionaryWithObjectsAndKeys:
986                            NoNumber, @"result", nil];
987 #endif    
988   }
989   [sortStr appendString:@") "];
990   // TODO: make that the other way around! should be an ivar in the client
991   [sortStr appendString:[[self context] sortEncoding]];
992   [sortStr appendString:[self _searchExprForQual:_qual]];
993   
994   return [self->normer normalizeSortResponse:[self processCommand:sortStr]];
995 }
996
997 - (NSDictionary *)searchWithQualifier:(EOQualifier *)_qualifier {
998   NSString *s;
999   
1000   s = [self _searchExprForQual:_qualifier];
1001   if ([s length] == 0) {
1002     // TODO: should set last-exception?
1003     [self logWithFormat:@"ERROR(%s): could not process search qualifier: %@",
1004           __PRETTY_FUNCTION__, _qualifier];
1005     return nil;
1006   }
1007   
1008   s = [@"search" stringByAppendingString:s];
1009   return [self->normer normalizeSearchResponse:[self processCommand:s]];
1010 }
1011
1012 /* Private Methods */
1013
1014 - (NSException *)_processCommandParserException:(NSException *)_exception {
1015   NSLog(@"ERROR(%s): catched IMAP4 parser exception %@: %@",
1016         __PRETTY_FUNCTION__, [_exception name], [_exception reason]);
1017   [self closeConnection];
1018   [self->context setLastException:_exception];
1019   return nil;
1020 }
1021 - (NSException *)_processUnknownCommandParserException:(NSException *)_ex {
1022   NSLog(@"ERROR(%s): catched non-IMAP4 parsing exception %@: %@",
1023         __PRETTY_FUNCTION__, [_ex name], [_ex reason]);
1024   return nil;
1025 }
1026
1027 - (NSException *)_handleShutdownDuringCommandException:(NSException *)_ex {
1028   NSLog(@"ERROR(%s): IMAP4 socket was shut down by server %@: %@",
1029         __PRETTY_FUNCTION__, [_ex name], [_ex reason]);
1030   [self closeConnection];
1031   [self->context setLastException:_ex];
1032   return nil;
1033 }
1034
1035 - (BOOL)_isShutdownException:(NSException *)_ex {
1036   return [[_ex name] isEqualToString:@"NGSocketShutdownDuringReadException"];
1037 }
1038
1039 - (BOOL)_isLoginCommand:(NSString *)_command {
1040   return [_command hasPrefix:@"login"];
1041 }
1042
1043 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
1044   withNotification:(BOOL)_notification logText:(NSString *)_txt
1045 {
1046   NGHashMap    *map;
1047   BOOL         tryReconnect;
1048   int          reconnectCnt;
1049   NSException  *exception;
1050
1051   struct timeval tv;
1052   double         ti = 0.0;
1053
1054   if (ProfileImapEnabled == 1) {
1055     gettimeofday(&tv, NULL);
1056     ti =  (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
1057     fprintf(stderr, "{");
1058   }
1059   tryReconnect = NO;
1060   reconnectCnt = 0;
1061   map          = nil;
1062   exception    = nil;
1063
1064   do {
1065     tryReconnect  = NO;
1066     [self->context resetLastException];
1067     NS_DURING {
1068       NSException *e = nil; // TODO: try to remove exception handler
1069       
1070       [self sendCommand:_command withTag:_tag logText:_txt];
1071       map = [self->parser parseResponseForTagId:self->tagId exception:&e];
1072       [e raise];
1073       tryReconnect = NO;
1074     }
1075     NS_HANDLER {
1076       if ([localException isKindOfClass:[NGImap4ParserException class]]) {
1077         [[self _processCommandParserException:localException] raise];
1078       }
1079       else if ([self _isShutdownException:localException]) {
1080         [[self _handleShutdownDuringCommandException:localException] raise];
1081       }
1082       else {
1083         [[self _processUnknownCommandParserException:localException] raise];
1084         if (reconnectCnt == 0) {
1085           if (![self _isLoginCommand:_command]) {
1086             reconnectCnt++;
1087             tryReconnect = YES;
1088             exception    = localException;
1089           }
1090         }
1091         [self closeConnection];
1092         [self->context setLastException:localException];
1093       }
1094     }
1095     NS_ENDHANDLER;
1096
1097     if (tryReconnect) {
1098       [self reconnect];
1099     }
1100     else if ([map objectForKey:@"bye"] && ![_command hasPrefix:@"logout"]) {
1101       if (reconnectCnt == 0) {
1102         reconnectCnt++;
1103         tryReconnect = YES;
1104         [self reconnect];
1105       }
1106     }
1107   } while (tryReconnect);
1108
1109   if ([self->context lastException]) {
1110     if (exception) {
1111       [self->context setLastException:exception];
1112     }
1113     return nil;
1114   }
1115   if (_notification) [self sendResponseNotification:map];
1116
1117   if (ProfileImapEnabled == 1) {
1118     gettimeofday(&tv, NULL);
1119     ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
1120     fprintf(stderr, "}[%s] <Send Command [%s]> : time needed: %4.4fs\n",
1121            __PRETTY_FUNCTION__, [_command cString], ti < 0.0 ? -1.0 : ti);    
1122   }
1123   return map;
1124 }
1125
1126 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
1127   withNotification:(BOOL)_notification
1128 {
1129   return [self processCommand:_command withTag:_tag
1130                withNotification:_notification
1131                logText:_command];
1132 }
1133
1134 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag {
1135   return [self processCommand:_command withTag:_tag withNotification:YES
1136                logText:_command];
1137 }
1138
1139 - (NGHashMap *)processCommand:(NSString *)_command {
1140   return [self processCommand:_command withTag:YES withNotification:YES
1141                logText:_command];
1142 }
1143
1144 - (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt {
1145   return [self processCommand:_command withTag:YES withNotification:YES
1146                logText:_txt];
1147 }
1148
1149 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag
1150   logText:(NSString *)_txt
1151 {
1152   NSString      *command;
1153   NGCTextStream *txtStream;
1154
1155   txtStream = [self textStream];
1156
1157   if (_tag) {
1158     self->tagId++;
1159
1160     command = [NSString stringWithFormat:@"%d %@", self->tagId, _command];
1161     if (self->debug) {
1162       _txt = [NSString stringWithFormat:@"%d %@", self->tagId, _txt];
1163     }
1164   }
1165   else
1166     command = _command;
1167
1168   if (self->debug) {
1169     if ([_txt length] > 5000) {
1170       fprintf(stderr, "C[%p]: %s...\n", self, [[_txt substringToIndex:5000]
1171                                                   cString]);
1172     }
1173     else {
1174       fprintf(stderr, "C[%p]: %s\n", self, [_txt cString]);
1175     }
1176   }
1177   
1178   if (![txtStream writeString:command])
1179     [self->context setLastException:[txtStream lastException]];
1180   else if (![txtStream writeString:@"\r\n"])
1181     [self->context setLastException:[txtStream lastException]];
1182   else if (![txtStream flush])
1183     [self->context setLastException:[txtStream lastException]];
1184 }
1185   
1186 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag {
1187   [self sendCommand:_command withTag:_tag logText:_command];
1188 }
1189
1190 - (void)sendCommand:(NSString *)_command {
1191   [self sendCommand:_command withTag:YES logText:_command];
1192 }
1193
1194 static inline NSArray *_flags2ImapFlags(NGImap4Client *self, NSArray *_flags) {
1195   NSEnumerator *enumerator;
1196   NSArray      *result;
1197   id           obj;
1198   id           *objs;
1199   unsigned     cnt;
1200
1201   objs = calloc([_flags count] + 2, sizeof(id));
1202   cnt  = 0;
1203   enumerator = [_flags objectEnumerator];
1204   while ((obj = [enumerator nextObject])) {
1205     objs[cnt] = [@"\\" stringByAppendingString:obj];
1206     cnt++;
1207   }
1208   result = [NSArray arrayWithObjects:objs count:cnt];
1209   if (objs) free(objs);
1210   return result;
1211 }
1212
1213 - (NSString *)_folder2ImapFolder:(NSString *)_folder {
1214   NSArray *array;
1215   
1216   if (self->delimiter == nil) {
1217     NSDictionary *res;
1218
1219     res = [self list:@"" pattern:@""];
1220
1221     if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
1222       return nil;
1223   }
1224
1225   array = [_folder pathComponents];
1226
1227   if ([array count] > 0) {
1228     NSString *o;
1229
1230     o = [array objectAtIndex:0];
1231     if (([o isEqualToString:@"/"]) || ([o length] == 0))
1232       array = [array subarrayWithRange:NSMakeRange(1, [array count] - 1)];
1233     
1234     o = [array lastObject];
1235     if (([o length] == 0) || ([o isEqualToString:@"/"]))
1236       array = [array subarrayWithRange:NSMakeRange(0, [array count] - 1)];
1237   }
1238   return [[array componentsJoinedByString:self->delimiter]
1239                  stringByEncodingImap4FolderName];
1240 }
1241
1242 - (NSString *)_imapFolder2Folder:(NSString *)_folder {
1243   NSArray *array;
1244   
1245   array = [NSArray arrayWithObject:@""];
1246
1247   if ([self delimiter] == nil) {
1248     NSDictionary *res;
1249     
1250     res = [self list:@"" pattern:@""]; // fill the delimiter ivar?
1251     if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
1252       return nil;
1253   }
1254   
1255   array = [array arrayByAddingObjectsFromArray:
1256                    [_folder componentsSeparatedByString:[self delimiter]]];
1257   
1258   return [[NSString pathWithComponents:array] stringByDecodingImap4FolderName];
1259 }
1260
1261 - (void)setContext:(NGImap4Context *)_ctx {
1262   self->context = _ctx;
1263 }
1264 - (NGImap4Context *)context {
1265   return self->context;
1266 }
1267
1268 /* ConnectionRegistration */
1269
1270 - (void)removeFromConnectionRegister {
1271   unsigned cnt;
1272   
1273   for (cnt = 0; cnt < MaxImapClients; cnt++) {
1274     if (ImapClients[cnt] == self)
1275       ImapClients[cnt] = nil;
1276   }
1277 }
1278
1279 - (void)registerConnection {
1280   int cnt;
1281
1282   cnt =  CountClient % MaxImapClients;
1283
1284   if (ImapClients[cnt]) {
1285     [(NGImap4Context *)ImapClients[cnt] closeConnection];
1286   }
1287   ImapClients[cnt] = self;
1288   CountClient++;
1289 }
1290
1291 - (id<NGExtendedTextStream>)textStream {
1292   if (self->text == nil) {
1293     if ([self->context lastException] == nil)
1294       [self reconnect];
1295   }
1296   return self->text;
1297 }
1298
1299 /* description */
1300
1301 - (NSString *)description {
1302   NSMutableString *ms;
1303
1304   ms = [NSMutableString stringWithCapacity:128];
1305   [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
1306   [ms appendFormat:@" socket=%@", [self socket]];
1307   [ms appendString:@">"];
1308   return ms;
1309 }
1310
1311 @end /* NGImap4Client; */