]> err.no Git - sope/blob - sope-mime/NGImap4/NGImap4Client.m
improvements in NGImap4Client
[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   // TODO: why is that an NSNummber?
351   /* 
352      Check whether stream is already open (could be closed because 
353      server-timeout) 
354   */
355   return (self->socket == nil)
356     ? NoNumber 
357     : ([(NGActiveSocket *)self->socket isAlive] ? YesNumber : NoNumber);
358 }
359
360 - (NSException *)_handleTextReleaseException:(NSException *)_ex {
361   [self logWithFormat:@"got exception during stream dealloc: %@", _ex];
362   return nil;
363 }
364 - (NSException *)_handleSocketCloseException:(NSException *)_ex {
365   [self logWithFormat:@"got exception during socket close: %@", _ex];
366   return nil;
367 }
368 - (NSException *)_handleSocketReleaseException:(NSException *)_ex {
369   [self logWithFormat:@"got exception during socket deallocation: %@", _ex];
370   return nil;
371 }
372 - (void)closeConnection {
373   /* close a connection */
374   
375   // TODO: this is a bit weird, probably because of the flush
376   //       maybe just call -close on the text stream?
377   NS_DURING
378     [self->text release]; 
379   NS_HANDLER
380     [[self _handleTextReleaseException:localException] raise];
381   NS_ENDHANDLER;
382   self->text = nil;
383   
384   NS_DURING
385     [self->socket close];
386   NS_HANDLER
387     [[self _handleSocketCloseException:localException] raise];
388   NS_ENDHANDLER;
389   
390   NS_DURING
391     [self->socket release];
392   NS_HANDLER
393     [[self _handleSocketReleaseException:localException] raise];
394   NS_ENDHANDLER;
395   self->socket = nil;
396   
397   [self->parser    release]; self->parser    = nil;
398   [self->delimiter release]; self->delimiter = nil;
399   [self removeFromConnectionRegister];
400 }
401
402 // ResponseNotifications
403
404 - (void)registerForResponseNotification:(id<NGImap4ResponseReceiver>)_obj {
405   [self->responseReceiver addObject:[NSValue valueWithNonretainedObject:_obj]];
406 }
407
408 - (void)removeFromResponseNotification:(id<NGImap4ResponseReceiver>)_obj {
409   [self->responseReceiver removeObject:
410        [NSValue valueWithNonretainedObject:_obj]];
411 }
412
413 - (void)sendResponseNotification:(NGHashMap *)_map {
414   NSValue                     *val;
415   id<NGImap4ResponseReceiver> obj;
416   NSEnumerator                *enumerator;
417   NSDictionary                *resp;
418
419   resp       = [self->normer normalizeResponse:_map];
420   enumerator = [self->responseReceiver objectEnumerator];
421
422   while ((val = [enumerator nextObject])) {
423     obj = [val nonretainedObjectValue];
424     [obj responseNotificationFrom:self response:resp];
425   }
426 }
427
428 /* commands */
429
430 - (NSDictionary *)login:(NSString *)_login password:(NSString *)_passwd {
431   /* login with plaintext password authenticating */
432
433   if ((_login == nil) || (_passwd == nil))
434     return nil;
435
436   [self->login     release]; self->login    = nil;
437   [self->password  release]; self->password = nil;
438   [self->serverGID release]; self->serverGID = nil;
439   
440   self->login    = [_login copy];
441   self->password = [[_passwd stringByEscapingImap4Password] copy];
442   
443   return [self login];
444 }
445
446 - (void)reconnect {
447   if ([self->context lastException] != nil)
448     return;
449     
450   [self closeConnection];  
451   self->tagId = 0;
452   [self openConnection];
453
454   if ([self->context lastException] != nil)
455     return;
456   
457   [self login];
458 }
459
460 - (NSDictionary *)login {
461   /*
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
466     On success:
467       'result'      - a boolean => true
468       'expunge'     - an array (containing what?)
469       'RawResponse' - the raw IMAP4 response
470   */
471   NGHashMap *map;
472   NSString  *s, *log;
473
474   if (self->isLogin )
475     return nil;
476   
477   self->isLogin = YES;
478   
479   s = [NSString stringWithFormat:@"login \"%@\" \"%@\"",
480                   self->login, self->password];
481   log = [NSString stringWithFormat:@"login %@ <%@>",
482                     self->login,
483                     (self->password != nil) ? @"PASSWORD" : @"NO PASSWORD"];
484   map = [self processCommand:s logText:log];
485   
486   if (self->selectedFolder != nil)
487     [self select:self->selectedFolder];
488   
489   self->isLogin = NO;
490   
491   return [self->normer normalizeResponse:map];
492 }
493
494 - (NSDictionary *)logout {
495   /* logout from the connected host and close the connection */
496   NGHashMap *map;
497
498   map = [self processCommand:@"logout"];
499   [self closeConnection];
500   
501   return [self->normer normalizeResponse:map];
502 }
503
504 /* Authenticated State */
505
506 - (NSDictionary *)list:(NSString *)_folder pattern:(NSString *)_pattern {
507   /*
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 ''.
513
514     The result dict contains the following keys:
515       'result'      - a boolean
516       'list'        - a dictionary (key is folder name, value is flags)
517       'RawResponse' - the raw IMAP4 response
518   */
519   NSAutoreleasePool *pool;
520   NGHashMap         *map;
521   NSDictionary      *result;
522   NSString *s;
523   
524   pool = [[NSAutoreleasePool alloc] init];
525   
526   if (_folder  == nil) _folder  = @"";
527   if (_pattern == nil) _pattern = @"";
528   
529   if ([_folder length] > 0) {
530     if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
531       return nil;
532   }
533   
534
535   if ([_pattern length] > 0)
536     if (!(_pattern = [self _folder2ImapFolder:_pattern]))
537       return nil;
538   
539   s = [NSString stringWithFormat:@"list \"%@\" \"%@\"", _folder, _pattern];
540   map = [self processCommand:s];
541   
542   if (self->delimiter == nil) {
543     NSDictionary *rdel;
544     
545     rdel = [[map objectEnumeratorForKey:@"list"] nextObject];
546     self->delimiter = [[rdel objectForKey:@"delimiter"] copy];
547   }
548   
549   result = [[self->normer normalizeListResponse:map] copy];
550   [pool release];
551   return [result autorelease];
552 }
553
554 - (NSDictionary *)capability {
555   id capres;
556   capres = [self processCommand:@"capability"];
557   return [self->normer normalizeCapabilityRespone:capres];
558 }
559
560 - (NSDictionary *)lsub:(NSString *)_folder pattern:(NSString *)_pattern {
561   /*
562     The method build statements like 'LSUB "_folder" "_pattern"'.
563     The returnvalue is the same like the list:pattern: method
564   */
565   NGHashMap *map;
566   NSString  *s;
567
568   if (_folder == nil)
569     _folder = @"";
570
571   if ([_folder length] > 0) {
572     if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
573       return nil;
574   }
575   if (_pattern == nil)
576     _pattern = @"";
577
578   if ([_pattern length] > 0) {
579     if ((_pattern = [self _folder2ImapFolder:_pattern]) == nil)
580       return nil;
581   }
582   
583   s = [NSString stringWithFormat:@"lsub \"%@\" \"%@\"", _folder, _pattern];
584   map = [self processCommand:s];
585
586   if (self->delimiter == nil) {
587     NSDictionary *rdel;
588     
589     rdel = [[map objectEnumeratorForKey:@"LIST"] nextObject];
590     self->delimiter = [[rdel objectForKey:@"delimiter"] copy];
591   }
592   return [self->normer normalizeListResponse:map];
593 }
594
595 - (NSDictionary *)select:(NSString *)_folder {
596   /*
597     Select a folder (required for a lot of methods).
598     eg: 'SELECT "INBOX"'
599     
600     The result dict contains the following keys:
601       'result'      - a boolean
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
608    */
609   NSString *s;
610   id tmp;
611
612   tmp = self->selectedFolder;
613
614   if ([_folder length] == 0)
615     return nil;
616   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
617     return nil;
618
619   self->selectedFolder = [_folder copy];
620   
621   [tmp release]; tmp = nil;
622
623   s = [NSString stringWithFormat:@"select \"%@\"", self->selectedFolder];
624   return [self->normer normalizeSelectResponse:[self processCommand:s]];
625 }
626
627 - (NSDictionary *)status:(NSString *)_folder flags:(NSArray *)_flags {
628   NSString *cmd;
629   
630   if (_folder == nil)
631     return nil;
632   if ((_flags == nil) || ([_flags count] == 0))
633     return nil;
634   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
635     return nil;
636   
637   cmd     = [NSString stringWithFormat:@"status \"%@\" (%@)",
638                       _folder, [_flags componentsJoinedByString:@" "]];
639   return [self->normer normalizeStatusResponse:[self processCommand:cmd]];
640 }
641
642 - (NSDictionary *)noop {
643   // at any state
644   return [self->normer normalizeResponse:[self processCommand:@"noop"]];
645 }
646
647 - (NSDictionary *)rename:(NSString *)_folder to:(NSString *)_newName {
648   NSString *cmd;
649   
650   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
651     return nil;
652   if ((_newName = [self _folder2ImapFolder:_newName]) == nil)
653     return nil;
654   
655   cmd = [NSString stringWithFormat:@"rename \"%@\" \"%@\"", _folder, _newName];
656   
657   return [self->normer normalizeResponse:[self processCommand:cmd]];
658 }
659
660 - (NSDictionary *)_performCommand:(NSString *)_op onFolder:(NSString *)_fname {
661   NSString *command;
662   
663   if ((_fname = [self _folder2ImapFolder:_fname]) == nil)
664     return nil;
665   
666   // eg: 'delete "blah"'
667   command = [NSString stringWithFormat:@"%@ \"%@\"", _op, _fname];
668   
669   return [self->normer normalizeResponse:[self processCommand:command]];
670 }
671
672 - (NSDictionary *)delete:(NSString *)_name {
673   return [self _performCommand:@"delete" onFolder:_name];
674 }
675 - (NSDictionary *)create:(NSString *)_name {
676   return [self _performCommand:@"create" onFolder:_name];
677 }
678 - (NSDictionary *)subscribe:(NSString *)_name {
679   return [self _performCommand:@"subscribe" onFolder:_name];
680 }
681 - (NSDictionary *)unsubscribe:(NSString *)_name {
682   return [self _performCommand:@"unsubscribe" onFolder:_name];
683 }
684
685 - (NSDictionary *)expunge {
686   return [self->normer normalizeResponse:[self processCommand:@"expunge"]];
687 }
688
689 - (NSString *)_uidsJoinedForFetchCmd:(NSArray *)_uids {
690   return [_uids componentsJoinedByString:@","];
691 }
692 - (NSString *)_partsJoinedForFetchCmd:(NSArray *)_parts {
693   return [_parts componentsJoinedByString:@" "];
694 }
695
696 - (NSDictionary *)fetchUids:(NSArray *)_uids parts:(NSArray *)_parts {
697   NSAutoreleasePool *pool;
698   NSString          *cmd;
699   NSDictionary      *result;
700   id fetchres;
701   
702   pool = [[NSAutoreleasePool alloc] init];
703   cmd  = [NSString stringWithFormat:@"uid fetch %@ (%@)",
704                    [self _uidsJoinedForFetchCmd:_uids],
705                    [self _partsJoinedForFetchCmd:_parts]];
706   
707   fetchres = [self processCommand:cmd];
708   result   = [[self->normer normalizeFetchResponse:fetchres] retain];
709   [pool release];
710   
711   return [result autorelease];
712 }
713
714 - (NSDictionary *)fetchUid:(unsigned)_uid parts:(NSArray *)_parts {
715   // TODO: describe what exactly this can return!
716   NSAutoreleasePool *pool;
717   NSString          *cmd;
718   NSDictionary      *result;
719   id fetchres;
720   
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];
726   
727   [pool release];
728   return [result autorelease];
729 }
730
731 - (NSDictionary *)fetchFrom:(unsigned)_from to:(unsigned)_to
732   parts:(NSArray *)_parts
733 {
734   // TODO: optimize
735   NSAutoreleasePool *pool;
736   NSMutableString   *cmd;
737   NSDictionary      *result; 
738   NGHashMap         *rawResult;
739  
740   if (_to == 0)
741     return [self noop];
742   
743   if (_from == 0)
744     _from = 1;
745
746   pool = [[NSAutoreleasePool alloc] init];
747   {
748     unsigned i, count;
749     
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]];
756     }
757     [cmd appendString:@")"];
758     
759     if (fetchDebug) NSLog(@"%s: process: %@", __PRETTY_FUNCTION__, cmd);
760     rawResult = [self processCommand:cmd];
761     /*
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)
765     */
766     
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);
770   }
771   [pool release];
772   if (fetchDebug) NSLog(@"%s: pool done.", __PRETTY_FUNCTION__);
773   return [result autorelease];
774 }
775
776 - (NSDictionary *)storeUid:(unsigned)_uid add:(NSNumber *)_add
777   flags:(NSArray *)_flags
778 {
779   NSString *icmd, *iflags;
780   
781   iflags = [_flags2ImapFlags(self, _flags) componentsJoinedByString:@" "];
782   icmd   = [NSString stringWithFormat:@"uid store %d %cFLAGS (%@)",
783                      _uid, [_add boolValue] ? '+' : '-',
784                      iflags];
785   return [self->normer normalizeResponse:[self processCommand:icmd]];
786 }
787
788 - (NSDictionary *)storeFrom:(unsigned)_from to:(unsigned)_to
789   add:(NSNumber *)_add
790   flags:(NSArray *)_flags
791 {
792   NSString *cmd;
793   NSString *flagstr;
794
795   if (_to == 0)
796     return [self noop];
797   if (_from == 0)
798     _from = 1;
799
800   flagstr = [_flags2ImapFlags(self, _flags) componentsJoinedByString:@" "];
801   cmd = [NSString stringWithFormat:@"store %d:%d %cFLAGS (%@)",
802                     _from, _to, [_add boolValue] ? '+' : '-', flagstr];
803   
804   return [self->normer normalizeResponse:[self processCommand:cmd]];
805 }
806
807 - (NSDictionary *)copyFrom:(unsigned)_from to:(unsigned)_to
808   toFolder:(NSString *)_folder
809 {
810   NSString *cmd;
811
812   if (_to == 0)
813     return [self noop];
814   if (_from == 0)
815     _from = 1;
816   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
817     return nil;
818   
819   cmd = [NSString stringWithFormat:@"copy %d:%d \"%@\"", _from, _to, _folder];
820   return [self->normer normalizeResponse:[self processCommand:cmd]];
821 }
822
823 - (NSDictionary *)copyUid:(unsigned)_uid toFolder:(NSString *)_folder {
824   NSString *cmd;
825   
826   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
827     return nil;
828   
829   cmd = [NSString stringWithFormat:@"uid copy %d \"%@\"", _uid, _folder];
830   
831   return [self->normer normalizeResponse:[self processCommand:cmd]];
832 }
833
834 - (NSDictionary *)getQuotaRoot:(NSString *)_folder {
835   NSString *cmd;
836
837   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
838     return nil;
839   
840   cmd = [NSString stringWithFormat:@"getquotaroot \"%@\"", _folder];
841   return [self->normer normalizeQuotaResponse:[self processCommand:cmd]];
842 }
843
844 - (NSDictionary *)append:(NSData *)_message toFolder:(NSString *)_folder
845   withFlags:(NSArray *)_flags
846 {
847   NSArray   *flags;
848   NGHashMap *result;
849   NSString  *message, *icmd;
850
851   flags   = _flags2ImapFlags(self, _flags);
852   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
853     return nil;
854   
855
856   /* Remove bare newlines */
857   {
858     char       *new;
859     const char *old;
860     int         cntOld   = 0;
861     int         cntNew   = 0;
862     int         len      = 0;
863     
864     old = [_message bytes];
865     len = [_message length];
866     
867     new = calloc(len * 2 + 4, sizeof(char));
868     
869     while (cntOld < (len - 1)) {
870       if (old[cntOld] == '\n') {
871         new[cntNew++] = '\r';
872         new[cntNew++] = '\n';
873       }
874       else if (old[cntOld] != '\r') {
875         new[cntNew++] = old[cntOld];
876       }
877       cntOld++;
878     }
879     if (old[cntOld] == '\n') {
880       new[cntNew++] = '\r';
881       new[cntNew++] = '\n';
882     }
883     else if (old[cntOld] != '\r') {
884       new[cntNew++] = old[cntOld];
885     } 
886     message = [(NSString *)[NSString alloc] 
887                   initWithCString:new length:cntNew];
888     if (new) free(new); new = NULL;
889   }
890   
891   icmd = [NSString stringWithFormat:@"append \"%@\" (%@) {%d}",
892                      _folder,
893                      [flags componentsJoinedByString:@" "],
894                      [message cStringLength]];
895   result = [self processCommand:icmd
896                  withTag:YES withNotification:NO];
897   
898   if ([[result objectForKey:@"ContinuationResponse"] boolValue])
899     result = [self processCommand:message withTag:NO];
900
901   [message release]; message = nil;
902
903   return [self->normer normalizeResponse:result];
904 }
905
906 - (void)_handleSearchExprIssue:(NSString *)reason qualifier:(EOQualifier *)_q {
907   NSString     *descr;
908   NSException  *exception = nil;                                             
909   NSDictionary *ui;
910   
911   if (PreventExceptions != 0)
912     return;
913   
914   if (_q == nil) _q = (id)[NSNull null];                
915   
916   descr = @"Could not process qualifier for imap search "; 
917   descr = [descr stringByAppendingString:reason];           
918   
919   exception = [[NGImap4SearchException alloc] initWithFormat:@"%@", descr];    
920   ui = [NSDictionary dictionaryWithObject:_q forKey:@"qualifier"];
921   [exception setUserInfo:ui];
922   [self->context setLastException:exception];
923   [exception release];
924 }
925
926 - (NSString *)_searchExprForQual:(EOQualifier *)_qualifier {
927   /*
928     samples:
929       ' ALL'
930       ' SINCE 1-Feb-1994'
931       ' TEXT "why SOPE rocks"'
932   */
933   id result;
934   
935   if (_qualifier == nil)
936     return @" ALL";
937   
938   result = [_qualifier imap4SearchString];
939   if ([result isKindOfClass:[NSException class]]) {
940     [self _handleSearchExprIssue:[(NSException *)result reason]
941           qualifier:_qualifier];
942     return nil;
943   }
944   return result;
945 }
946
947 - (NSDictionary *)threadBySubject:(BOOL)_bySubject
948   charset:(NSString *)_charSet
949 {
950   /*
951     http://www.ietf.org/proceedings/03mar/I-D/draft-ietf-imapext-thread-12.txt
952
953     Returns an array of uids in sort order.
954
955     Parameters:
956       _bySubject - if yes, use "REFERENCES" else "ORDEREDSUBJECT" (TODO: ?!)
957       _charSet   - default: "UTF-8"
958     
959     Generates:
960       UID THREAD REFERENCES|ORDEREDSUBJECT UTF-8 ALL
961   */
962   NSString *threadStr;
963   NSString *threadAlg;
964   
965   threadAlg = (_bySubject)
966     ? @"REFERENCES"
967     : @"ORDEREDSUBJECT";
968   
969   if ([_charSet length] == 0)
970     _charSet = @"UTF-8";
971   
972   threadStr = [NSString stringWithFormat:@"UID THREAD %@ %@ ALL",
973                       threadAlg, _charSet];
974   
975   return [self->normer normalizeThreadResponse:
976                 [self processCommand:threadStr]];
977 }
978
979 - (NSString *)_generateIMAP4SortOrdering:(EOSortOrdering *)_sortOrdering {
980   SEL      sel;
981   NSString *key;
982     
983   key = [_sortOrdering key];
984   if ([key length] == 0)
985     return nil;
986     
987   if (![AllowedSortKeys containsObject:[key uppercaseString]]) {
988     [self logWithFormat:@"ERROR[%s] key %@ is not allowed here!",
989             __PRETTY_FUNCTION__, key];
990     return nil;
991   }
992
993   sel = [_sortOrdering selector];
994   if (sel_eq(sel, EOCompareDescending) ||
995       sel_eq(sel, EOCompareCaseInsensitiveDescending)) {
996     return [@"REVERSE " stringByAppendingString:key];
997   }
998   // TODO: check other selectors whether they make sense instead of silent acc.
999   
1000   return key;
1001 }
1002
1003 - (NSString *)_generateIMAP4SortOrderings:(NSArray *)_sortOrderings {
1004   /*
1005     turn EOSortOrdering into an IMAP4 value for "SORT()"
1006     
1007     eg: "DATE REVERSE SUBJECT"
1008     
1009     It also checks a set of allowed sort-keys (don't know why)
1010   */
1011   NSMutableString *sortStr;
1012   NSEnumerator    *soe;
1013   EOSortOrdering  *so;
1014   BOOL            isFirst = YES;
1015   
1016   if ([_sortOrderings count] == 0)
1017     return nil;
1018   
1019   sortStr = [NSMutableString stringWithCapacity:128];
1020   soe     = [_sortOrderings objectEnumerator];
1021   while ((so = [soe nextObject])) {
1022     NSString *s;
1023
1024     s = [self _generateIMAP4SortOrdering:so];
1025     if (s == nil)
1026       continue;
1027     
1028     if (isFirst)
1029       isFirst = NO;
1030     else
1031       [sortStr appendString:@" "];
1032     
1033     [sortStr appendString:s];
1034   }
1035   return isFirst ? nil : sortStr;
1036 }
1037
1038 - (NSDictionary *)primarySort:(NSString *)_sort
1039   qualifierString:(NSString *)_qualString
1040   encoding:(NSString *)_encoding
1041 {
1042   /* 
1043      http://www.ietf.org/internet-drafts/draft-ietf-imapext-sort-17.txt
1044      
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
1050      
1051      Eg: UID SORT ( DATE REVERSE SUBJECT ) UTF-8 TODO
1052   */
1053   NSMutableString *sortStr;
1054
1055   if (![_encoding   isNotNull]) _encoding   = @"UTF-8";
1056   if (![_qualString isNotNull]) _qualString = @" ALL";
1057   
1058   sortStr = [NSMutableString stringWithCapacity:128];
1059   
1060   [sortStr appendString:@"UID SORT ("];
1061   if (_sort != nil) [sortStr appendString:_sort];
1062   [sortStr appendString:@") "];
1063   
1064   [sortStr appendString:_encoding];   /* eg 'UTF-8' */
1065   
1066   /* Note: this is _space sensitive_! to many spaces lead to error! */
1067   [sortStr appendString:_qualString]; /* eg ' ALL' or ' TEXT "abc"' */
1068   
1069   return [self->normer normalizeSortResponse:[self processCommand:sortStr]];
1070 }
1071
1072 - (NSDictionary *)sort:(id)_sortSpec
1073   qualifier:(EOQualifier *)_qual
1074   encoding:(NSString *)_encoding
1075 {
1076   /* 
1077      http://www.ietf.org/internet-drafts/draft-ietf-imapext-sort-17.txt
1078
1079      The _sortSpec can be:
1080      - a simple 'raw' IMAP4 sort string
1081      - an EOSortOrdering
1082      - an array of EOSortOrderings
1083      
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
1089     
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
1092               returned
1093      
1094      Eg: UID SORT ( DATE REVERSE SUBJECT ) UTF-8 TODO
1095   */
1096   NSString *tmp;
1097   
1098   if ([_sortSpec isKindOfClass:[NSArray class]])
1099     tmp = [self _generateIMAP4SortOrderings:_sortSpec];
1100   else if ([_sortSpec isKindOfClass:[EOSortOrdering class]])
1101     tmp = [self _generateIMAP4SortOrdering:_sortSpec];
1102   else
1103     tmp = [_sortSpec stringValue];
1104   
1105   if ([tmp length] == 0) { /* found no valid key use date sorting */
1106     [self logWithFormat:@"Note: no key found for sorting, using 'DATE': %@",
1107             _sortSpec];
1108     tmp = @"DATE";
1109   }
1110   
1111   return [self primarySort:tmp 
1112                qualifierString:[self _searchExprForQual:_qual]
1113                encoding:_encoding];
1114 }
1115 - (NSDictionary *)sort:(NSArray *)_sortOrderings
1116   qualifier:(EOQualifier *)_qual
1117 {
1118   // DEPRECATED, should not use context!
1119   return [self sort:_sortOrderings qualifier:_qual
1120                encoding:[[self context] sortEncoding]];
1121 }
1122
1123 - (NSDictionary *)searchWithQualifier:(EOQualifier *)_qualifier {
1124   NSString *s;
1125   
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];
1131     return nil;
1132   }
1133   
1134   s = [@"search" stringByAppendingString:s];
1135   return [self->normer normalizeSearchResponse:[self processCommand:s]];
1136 }
1137
1138 /* Private Methods */
1139
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];
1145   return nil;
1146 }
1147 - (NSException *)_processUnknownCommandParserException:(NSException *)_ex {
1148   NSLog(@"ERROR(%s): catched non-IMAP4 parsing exception %@: %@",
1149         __PRETTY_FUNCTION__, [_ex name], [_ex reason]);
1150   return nil;
1151 }
1152
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];
1158   return nil;
1159 }
1160
1161 - (BOOL)_isShutdownException:(NSException *)_ex {
1162   return [[_ex name] isEqualToString:@"NGSocketShutdownDuringReadException"];
1163 }
1164
1165 - (BOOL)_isLoginCommand:(NSString *)_command {
1166   return [_command hasPrefix:@"login"];
1167 }
1168
1169 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
1170   withNotification:(BOOL)_notification logText:(NSString *)_txt
1171 {
1172   NGHashMap    *map;
1173   BOOL         tryReconnect;
1174   int          reconnectCnt;
1175   NSException  *exception;
1176
1177   struct timeval tv;
1178   double         ti = 0.0;
1179
1180   if (ProfileImapEnabled == 1) {
1181     gettimeofday(&tv, NULL);
1182     ti =  (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
1183     fprintf(stderr, "{");
1184   }
1185   tryReconnect = NO;
1186   reconnectCnt = 0;
1187   map          = nil;
1188   exception    = nil;
1189
1190   do {
1191     tryReconnect  = NO;
1192     [self->context resetLastException];
1193     NS_DURING {
1194       NSException *e = nil; // TODO: try to remove exception handler
1195       
1196       [self sendCommand:_command withTag:_tag logText:_txt];
1197       map = [self->parser parseResponseForTagId:self->tagId exception:&e];
1198       [e raise];
1199       tryReconnect = NO;
1200     }
1201     NS_HANDLER {
1202       if ([localException isKindOfClass:[NGImap4ParserException class]]) {
1203         [[self _processCommandParserException:localException] raise];
1204       }
1205       else if ([self _isShutdownException:localException]) {
1206         [[self _handleShutdownDuringCommandException:localException] raise];
1207       }
1208       else {
1209         [[self _processUnknownCommandParserException:localException] raise];
1210         if (reconnectCnt == 0) {
1211           if (![self _isLoginCommand:_command]) {
1212             reconnectCnt++;
1213             tryReconnect = YES;
1214             exception    = localException;
1215           }
1216         }
1217         [self closeConnection];
1218         [self->context setLastException:localException];
1219       }
1220     }
1221     NS_ENDHANDLER;
1222
1223     if (tryReconnect) {
1224       [self reconnect];
1225     }
1226     else if ([map objectForKey:@"bye"] && ![_command hasPrefix:@"logout"]) {
1227       if (reconnectCnt == 0) {
1228         reconnectCnt++;
1229         tryReconnect = YES;
1230         [self reconnect];
1231       }
1232     }
1233   } while (tryReconnect);
1234
1235   if ([self->context lastException]) {
1236     if (exception) {
1237       [self->context setLastException:exception];
1238     }
1239     return nil;
1240   }
1241   if (_notification) [self sendResponseNotification:map];
1242
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);    
1248   }
1249   return map;
1250 }
1251
1252 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
1253   withNotification:(BOOL)_notification
1254 {
1255   return [self processCommand:_command withTag:_tag
1256                withNotification:_notification
1257                logText:_command];
1258 }
1259
1260 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag {
1261   return [self processCommand:_command withTag:_tag withNotification:YES
1262                logText:_command];
1263 }
1264
1265 - (NGHashMap *)processCommand:(NSString *)_command {
1266   return [self processCommand:_command withTag:YES withNotification:YES
1267                logText:_command];
1268 }
1269
1270 - (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt {
1271   return [self processCommand:_command withTag:YES withNotification:YES
1272                logText:_txt];
1273 }
1274
1275 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag
1276   logText:(NSString *)_txt
1277 {
1278   NSString      *command;
1279   NGCTextStream *txtStream;
1280
1281   txtStream = [self textStream];
1282
1283   if (_tag) {
1284     self->tagId++;
1285
1286     command = [NSString stringWithFormat:@"%d %@", self->tagId, _command];
1287     if (self->debug) {
1288       _txt = [NSString stringWithFormat:@"%d %@", self->tagId, _txt];
1289     }
1290   }
1291   else
1292     command = _command;
1293
1294   if (self->debug) {
1295     if ([_txt length] > 5000) {
1296       fprintf(stderr, "C[%p]: %s...\n", self, [[_txt substringToIndex:5000]
1297                                                   cString]);
1298     }
1299     else {
1300       fprintf(stderr, "C[%p]: %s\n", self, [_txt cString]);
1301     }
1302   }
1303   
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]];
1310 }
1311   
1312 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag {
1313   [self sendCommand:_command withTag:_tag logText:_command];
1314 }
1315
1316 - (void)sendCommand:(NSString *)_command {
1317   [self sendCommand:_command withTag:YES logText:_command];
1318 }
1319
1320 static inline NSArray *_flags2ImapFlags(NGImap4Client *self, NSArray *_flags) {
1321   NSEnumerator *enumerator;
1322   NSArray      *result;
1323   id           obj;
1324   id           *objs;
1325   unsigned     cnt;
1326
1327   objs = calloc([_flags count] + 2, sizeof(id));
1328   cnt  = 0;
1329   enumerator = [_flags objectEnumerator];
1330   while ((obj = [enumerator nextObject])) {
1331     objs[cnt] = [@"\\" stringByAppendingString:obj];
1332     cnt++;
1333   }
1334   result = [NSArray arrayWithObjects:objs count:cnt];
1335   if (objs) free(objs);
1336   return result;
1337 }
1338
1339 - (NSString *)_folder2ImapFolder:(NSString *)_folder {
1340   NSArray *array;
1341   
1342   if (self->delimiter == nil) {
1343     NSDictionary *res;
1344
1345     res = [self list:@"" pattern:@""];
1346
1347     if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
1348       return nil;
1349   }
1350
1351   array = [_folder pathComponents];
1352
1353   if ([array count] > 0) {
1354     NSString *o;
1355
1356     o = [array objectAtIndex:0];
1357     if (([o isEqualToString:@"/"]) || ([o length] == 0))
1358       array = [array subarrayWithRange:NSMakeRange(1, [array count] - 1)];
1359     
1360     o = [array lastObject];
1361     if (([o length] == 0) || ([o isEqualToString:@"/"]))
1362       array = [array subarrayWithRange:NSMakeRange(0, [array count] - 1)];
1363   }
1364   return [[array componentsJoinedByString:self->delimiter]
1365                  stringByEncodingImap4FolderName];
1366 }
1367
1368 - (NSString *)_imapFolder2Folder:(NSString *)_folder {
1369   NSArray *array;
1370   
1371   array = [NSArray arrayWithObject:@""];
1372
1373   if ([self delimiter] == nil) {
1374     NSDictionary *res;
1375     
1376     res = [self list:@"" pattern:@""]; // fill the delimiter ivar?
1377     if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
1378       return nil;
1379   }
1380   
1381   array = [array arrayByAddingObjectsFromArray:
1382                    [_folder componentsSeparatedByString:[self delimiter]]];
1383   
1384   return [[NSString pathWithComponents:array] stringByDecodingImap4FolderName];
1385 }
1386
1387 - (void)setContext:(NGImap4Context *)_ctx {
1388   self->context = _ctx;
1389 }
1390 - (NGImap4Context *)context {
1391   return self->context;
1392 }
1393
1394 /* ConnectionRegistration */
1395
1396 - (void)removeFromConnectionRegister {
1397   unsigned cnt;
1398   
1399   for (cnt = 0; cnt < MaxImapClients; cnt++) {
1400     if (ImapClients[cnt] == self)
1401       ImapClients[cnt] = nil;
1402   }
1403 }
1404
1405 - (void)registerConnection {
1406   int cnt;
1407
1408   cnt =  CountClient % MaxImapClients;
1409
1410   if (ImapClients[cnt]) {
1411     [(NGImap4Context *)ImapClients[cnt] closeConnection];
1412   }
1413   ImapClients[cnt] = self;
1414   CountClient++;
1415 }
1416
1417 - (id<NGExtendedTextStream>)textStream {
1418   if (self->text == nil) {
1419     if ([self->context lastException] == nil)
1420       [self reconnect];
1421   }
1422   return self->text;
1423 }
1424
1425 /* description */
1426
1427 - (NSString *)description {
1428   NSMutableString *ms;
1429   id tmp;
1430
1431   ms = [NSMutableString stringWithCapacity:128];
1432   [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
1433   
1434   if (self->login != nil)
1435     [ms appendFormat:@" login=%@%s", self->login, self->password?"(pwd)":""];
1436   
1437   if ((tmp = [self socket]) != nil)
1438     [ms appendFormat:@" socket=%@", tmp];
1439   else if (self->address)
1440     [ms appendFormat:@" address=%@", self->address];
1441   
1442   [ms appendString:@">"];
1443   return ms;
1444 }
1445
1446 @end /* NGImap4Client; */