]> err.no Git - sope/blob - sope-mime/NGImap4/NGImap4Client.m
fixed copyrights for 2005
[sope] / sope-mime / NGImap4 / NGImap4Client.m
1 /*
2   Copyright (C) 2000-2005 SKYRIX Software AG
3
4   This file is part of SOPE.
5
6   SOPE is free software; you can redistribute it and/or modify it under
7   the terms of the GNU Lesser General Public License as published by the
8   Free Software Foundation; either version 2, or (at your option) any
9   later version.
10
11   SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
12   WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14   License for more details.
15
16   You should have received a copy of the GNU Lesser General Public
17   License along with SOPE; see the file COPYING.  If not, write to the
18   Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
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; // remember ptr to old folder name
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; // release old folder name
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   /*
698     eg: 'UID FETCH 1189,1325,1326 ([TODO])'
699   */
700   NSAutoreleasePool *pool;
701   NSString          *cmd;
702   NSDictionary      *result;
703   NSString          *uidsStr, *partsStr;
704   id fetchres;
705   
706   pool = [[NSAutoreleasePool alloc] init];
707   
708   uidsStr  = [self _uidsJoinedForFetchCmd:_uids];
709   partsStr = [self _partsJoinedForFetchCmd:_parts];
710   cmd  = [NSString stringWithFormat:@"uid fetch %@ (%@)", uidsStr, partsStr];
711   
712   fetchres = [self processCommand:cmd];
713   result   = [[self->normer normalizeFetchResponse:fetchres] retain];
714   [pool release];
715   
716   return [result autorelease];
717 }
718
719 - (NSDictionary *)fetchUid:(unsigned)_uid parts:(NSArray *)_parts {
720   // TODO: describe what exactly this can return!
721   NSAutoreleasePool *pool;
722   NSString          *cmd;
723   NSDictionary      *result;
724   id fetchres;
725   
726   pool   = [[NSAutoreleasePool alloc] init];
727   cmd    = [NSString stringWithFormat:@"uid fetch %d (%@)", _uid,
728                      [self _partsJoinedForFetchCmd:_parts]];
729   fetchres = [self processCommand:cmd];
730   result   = [[self->normer normalizeFetchResponse:fetchres] retain];
731   
732   [pool release];
733   return [result autorelease];
734 }
735
736 - (NSDictionary *)fetchFrom:(unsigned)_from to:(unsigned)_to
737   parts:(NSArray *)_parts
738 {
739   // TODO: optimize
740   NSAutoreleasePool *pool;
741   NSMutableString   *cmd;
742   NSDictionary      *result; 
743   NGHashMap         *rawResult;
744  
745   if (_to == 0)
746     return [self noop];
747   
748   if (_from == 0)
749     _from = 1;
750
751   pool = [[NSAutoreleasePool alloc] init];
752   {
753     unsigned i, count;
754     
755     cmd = [NSMutableString stringWithCapacity:256];
756     [cmd appendString:@"fetch "];
757     [cmd appendFormat:@"%d:%d (", _from, _to];
758     for (i = 0, count = [_parts count]; i < count; i++) {
759       if (i != 0) [cmd appendString:@" "];
760       [cmd appendString:[_parts objectAtIndex:i]];
761     }
762     [cmd appendString:@")"];
763     
764     if (fetchDebug) NSLog(@"%s: process: %@", __PRETTY_FUNCTION__, cmd);
765     rawResult = [self processCommand:cmd];
766     /*
767       RawResult is a dict containing keys:
768         ResponseResult: dict    eg: {descripted=Completed;result=ok;tagId=8;}
769         fetch:          array of record dicts (eg "rfc822.header" key)
770     */
771     
772     if (fetchDebug) NSLog(@"%s: normalize: %@", __PRETTY_FUNCTION__,rawResult);
773     result = [[self->normer normalizeFetchResponse:rawResult] retain];
774     if (fetchDebug) NSLog(@"%s: normalized: %@", __PRETTY_FUNCTION__, result);
775   }
776   [pool release];
777   if (fetchDebug) NSLog(@"%s: pool done.", __PRETTY_FUNCTION__);
778   return [result autorelease];
779 }
780
781 - (NSDictionary *)storeUid:(unsigned)_uid add:(NSNumber *)_add
782   flags:(NSArray *)_flags
783 {
784   NSString *icmd, *iflags;
785   
786   iflags = [_flags2ImapFlags(self, _flags) componentsJoinedByString:@" "];
787   icmd   = [NSString stringWithFormat:@"uid store %d %cFLAGS (%@)",
788                      _uid, [_add boolValue] ? '+' : '-',
789                      iflags];
790   return [self->normer normalizeResponse:[self processCommand:icmd]];
791 }
792
793 - (NSDictionary *)storeFrom:(unsigned)_from to:(unsigned)_to
794   add:(NSNumber *)_add
795   flags:(NSArray *)_flags
796 {
797   NSString *cmd;
798   NSString *flagstr;
799
800   if (_to == 0)
801     return [self noop];
802   if (_from == 0)
803     _from = 1;
804
805   flagstr = [_flags2ImapFlags(self, _flags) componentsJoinedByString:@" "];
806   cmd = [NSString stringWithFormat:@"store %d:%d %cFLAGS (%@)",
807                     _from, _to, [_add boolValue] ? '+' : '-', flagstr];
808   
809   return [self->normer normalizeResponse:[self processCommand:cmd]];
810 }
811
812 - (NSDictionary *)copyFrom:(unsigned)_from to:(unsigned)_to
813   toFolder:(NSString *)_folder
814 {
815   NSString *cmd;
816
817   if (_to == 0)
818     return [self noop];
819   if (_from == 0)
820     _from = 1;
821   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
822     return nil;
823   
824   cmd = [NSString stringWithFormat:@"copy %d:%d \"%@\"", _from, _to, _folder];
825   return [self->normer normalizeResponse:[self processCommand:cmd]];
826 }
827
828 - (NSDictionary *)copyUid:(unsigned)_uid toFolder:(NSString *)_folder {
829   NSString *cmd;
830   
831   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
832     return nil;
833   
834   cmd = [NSString stringWithFormat:@"uid copy %d \"%@\"", _uid, _folder];
835   
836   return [self->normer normalizeResponse:[self processCommand:cmd]];
837 }
838
839 - (NSDictionary *)getQuotaRoot:(NSString *)_folder {
840   NSString *cmd;
841
842   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
843     return nil;
844   
845   cmd = [NSString stringWithFormat:@"getquotaroot \"%@\"", _folder];
846   return [self->normer normalizeQuotaResponse:[self processCommand:cmd]];
847 }
848
849 - (NSDictionary *)append:(NSData *)_message toFolder:(NSString *)_folder
850   withFlags:(NSArray *)_flags
851 {
852   NSArray   *flags;
853   NGHashMap *result;
854   NSString  *message, *icmd;
855
856   flags   = _flags2ImapFlags(self, _flags);
857   if ((_folder = [self _folder2ImapFolder:_folder]) == nil)
858     return nil;
859   
860
861   /* Remove bare newlines */
862   {
863     char       *new;
864     const char *old;
865     int         cntOld   = 0;
866     int         cntNew   = 0;
867     int         len      = 0;
868     
869     old = [_message bytes];
870     len = [_message length];
871     
872     new = calloc(len * 2 + 4, sizeof(char));
873     
874     while (cntOld < (len - 1)) {
875       if (old[cntOld] == '\n') {
876         new[cntNew++] = '\r';
877         new[cntNew++] = '\n';
878       }
879       else if (old[cntOld] != '\r') {
880         new[cntNew++] = old[cntOld];
881       }
882       cntOld++;
883     }
884     if (old[cntOld] == '\n') {
885       new[cntNew++] = '\r';
886       new[cntNew++] = '\n';
887     }
888     else if (old[cntOld] != '\r') {
889       new[cntNew++] = old[cntOld];
890     } 
891     message = [(NSString *)[NSString alloc] 
892                   initWithCString:new length:cntNew];
893     if (new) free(new); new = NULL;
894   }
895   
896   icmd = [NSString stringWithFormat:@"append \"%@\" (%@) {%d}",
897                      _folder,
898                      [flags componentsJoinedByString:@" "],
899                      [message cStringLength]];
900   result = [self processCommand:icmd
901                  withTag:YES withNotification:NO];
902   
903   if ([[result objectForKey:@"ContinuationResponse"] boolValue])
904     result = [self processCommand:message withTag:NO];
905
906   [message release]; message = nil;
907
908   return [self->normer normalizeResponse:result];
909 }
910
911 - (void)_handleSearchExprIssue:(NSString *)reason qualifier:(EOQualifier *)_q {
912   NSString     *descr;
913   NSException  *exception = nil;                                             
914   NSDictionary *ui;
915   
916   if (PreventExceptions != 0)
917     return;
918   
919   if (_q == nil) _q = (id)[NSNull null];                
920   
921   descr = @"Could not process qualifier for imap search "; 
922   descr = [descr stringByAppendingString:reason];           
923   
924   exception = [[NGImap4SearchException alloc] initWithFormat:@"%@", descr];    
925   ui = [NSDictionary dictionaryWithObject:_q forKey:@"qualifier"];
926   [exception setUserInfo:ui];
927   [self->context setLastException:exception];
928   [exception release];
929 }
930
931 - (NSString *)_searchExprForQual:(EOQualifier *)_qualifier {
932   /*
933     samples:
934       ' ALL'
935       ' SINCE 1-Feb-1994'
936       ' TEXT "why SOPE rocks"'
937   */
938   id result;
939   
940   if (_qualifier == nil)
941     return @" ALL";
942   
943   result = [_qualifier imap4SearchString];
944   if ([result isKindOfClass:[NSException class]]) {
945     [self _handleSearchExprIssue:[(NSException *)result reason]
946           qualifier:_qualifier];
947     return nil;
948   }
949   return result;
950 }
951
952 - (NSDictionary *)threadBySubject:(BOOL)_bySubject
953   charset:(NSString *)_charSet
954 {
955   /*
956     http://www.ietf.org/proceedings/03mar/I-D/draft-ietf-imapext-thread-12.txt
957
958     Returns an array of uids in sort order.
959
960     Parameters:
961       _bySubject - if yes, use "REFERENCES" else "ORDEREDSUBJECT" (TODO: ?!)
962       _charSet   - default: "UTF-8"
963     
964     Generates:
965       UID THREAD REFERENCES|ORDEREDSUBJECT UTF-8 ALL
966   */
967   NSString *threadStr;
968   NSString *threadAlg;
969   
970   threadAlg = (_bySubject)
971     ? @"REFERENCES"
972     : @"ORDEREDSUBJECT";
973   
974   if ([_charSet length] == 0)
975     _charSet = @"UTF-8";
976   
977   threadStr = [NSString stringWithFormat:@"UID THREAD %@ %@ ALL",
978                       threadAlg, _charSet];
979   
980   return [self->normer normalizeThreadResponse:
981                 [self processCommand:threadStr]];
982 }
983
984 - (NSString *)_generateIMAP4SortOrdering:(EOSortOrdering *)_sortOrdering {
985   SEL      sel;
986   NSString *key;
987     
988   key = [_sortOrdering key];
989   if ([key length] == 0)
990     return nil;
991     
992   if (![AllowedSortKeys containsObject:[key uppercaseString]]) {
993     [self logWithFormat:@"ERROR[%s] key %@ is not allowed here!",
994             __PRETTY_FUNCTION__, key];
995     return nil;
996   }
997
998   sel = [_sortOrdering selector];
999   if (sel_eq(sel, EOCompareDescending) ||
1000       sel_eq(sel, EOCompareCaseInsensitiveDescending)) {
1001     return [@"REVERSE " stringByAppendingString:key];
1002   }
1003   // TODO: check other selectors whether they make sense instead of silent acc.
1004   
1005   return key;
1006 }
1007
1008 - (NSString *)_generateIMAP4SortOrderings:(NSArray *)_sortOrderings {
1009   /*
1010     turn EOSortOrdering into an IMAP4 value for "SORT()"
1011     
1012     eg: "DATE REVERSE SUBJECT"
1013     
1014     It also checks a set of allowed sort-keys (don't know why)
1015   */
1016   NSMutableString *sortStr;
1017   NSEnumerator    *soe;
1018   EOSortOrdering  *so;
1019   BOOL            isFirst = YES;
1020   
1021   if ([_sortOrderings count] == 0)
1022     return nil;
1023   
1024   sortStr = [NSMutableString stringWithCapacity:128];
1025   soe     = [_sortOrderings objectEnumerator];
1026   while ((so = [soe nextObject])) {
1027     NSString *s;
1028
1029     s = [self _generateIMAP4SortOrdering:so];
1030     if (s == nil)
1031       continue;
1032     
1033     if (isFirst)
1034       isFirst = NO;
1035     else
1036       [sortStr appendString:@" "];
1037     
1038     [sortStr appendString:s];
1039   }
1040   return isFirst ? nil : sortStr;
1041 }
1042
1043 - (NSDictionary *)primarySort:(NSString *)_sort
1044   qualifierString:(NSString *)_qualString
1045   encoding:(NSString *)_encoding
1046 {
1047   /* 
1048      http://www.ietf.org/internet-drafts/draft-ietf-imapext-sort-17.txt
1049      
1050      The result dict contains the following keys:
1051       'result'      - a boolean
1052       'expunge'     - array            (of what?)
1053       'sort'        - array of uids in sort order
1054       'RawResponse' - the raw IMAP4 response
1055      
1056      Eg: UID SORT ( DATE REVERSE SUBJECT ) UTF-8 TODO
1057   */
1058   NSMutableString *sortStr;
1059
1060   if (![_encoding   isNotNull]) _encoding   = @"UTF-8";
1061   if (![_qualString isNotNull]) _qualString = @" ALL";
1062   
1063   sortStr = [NSMutableString stringWithCapacity:128];
1064   
1065   [sortStr appendString:@"UID SORT ("];
1066   if (_sort != nil) [sortStr appendString:_sort];
1067   [sortStr appendString:@") "];
1068   
1069   [sortStr appendString:_encoding];   /* eg 'UTF-8' */
1070   
1071   /* Note: this is _space sensitive_! to many spaces lead to error! */
1072   [sortStr appendString:_qualString]; /* eg ' ALL' or ' TEXT "abc"' */
1073   
1074   return [self->normer normalizeSortResponse:[self processCommand:sortStr]];
1075 }
1076
1077 - (NSDictionary *)sort:(id)_sortSpec
1078   qualifier:(EOQualifier *)_qual
1079   encoding:(NSString *)_encoding
1080 {
1081   /* 
1082      http://www.ietf.org/internet-drafts/draft-ietf-imapext-sort-17.txt
1083
1084      The _sortSpec can be:
1085      - a simple 'raw' IMAP4 sort string
1086      - an EOSortOrdering
1087      - an array of EOSortOrderings
1088      
1089      The result dict contains the following keys:
1090       'result'      - a boolean
1091       'expunge'     - array            (of what?)
1092       'sort'        - array of uids in sort order
1093       'RawResponse' - the raw IMAP4 response
1094     
1095      If no sortable key was found, the sort will run against 'DATE'.
1096      => TODO: this is inconsistent. If none are passed in, false will be
1097               returned
1098      
1099      Eg: UID SORT ( DATE REVERSE SUBJECT ) UTF-8 TODO
1100   */
1101   NSString *tmp;
1102   
1103   if ([_sortSpec isKindOfClass:[NSArray class]])
1104     tmp = [self _generateIMAP4SortOrderings:_sortSpec];
1105   else if ([_sortSpec isKindOfClass:[EOSortOrdering class]])
1106     tmp = [self _generateIMAP4SortOrdering:_sortSpec];
1107   else
1108     tmp = [_sortSpec stringValue];
1109   
1110   if ([tmp length] == 0) { /* found no valid key use date sorting */
1111     [self logWithFormat:@"Note: no key found for sorting, using 'DATE': %@",
1112             _sortSpec];
1113     tmp = @"DATE";
1114   }
1115   
1116   return [self primarySort:tmp 
1117                qualifierString:[self _searchExprForQual:_qual]
1118                encoding:_encoding];
1119 }
1120 - (NSDictionary *)sort:(NSArray *)_sortOrderings
1121   qualifier:(EOQualifier *)_qual
1122 {
1123   // DEPRECATED, should not use context!
1124   return [self sort:_sortOrderings qualifier:_qual
1125                encoding:[[self context] sortEncoding]];
1126 }
1127
1128 - (NSDictionary *)searchWithQualifier:(EOQualifier *)_qualifier {
1129   NSString *s;
1130   
1131   s = [self _searchExprForQual:_qualifier];
1132   if ([s length] == 0) {
1133     // TODO: should set last-exception?
1134     [self logWithFormat:@"ERROR(%s): could not process search qualifier: %@",
1135           __PRETTY_FUNCTION__, _qualifier];
1136     return nil;
1137   }
1138   
1139   s = [@"search" stringByAppendingString:s];
1140   return [self->normer normalizeSearchResponse:[self processCommand:s]];
1141 }
1142
1143 /* Private Methods */
1144
1145 - (NSException *)_processCommandParserException:(NSException *)_exception {
1146   NSLog(@"ERROR(%s): catched IMAP4 parser exception %@: %@",
1147         __PRETTY_FUNCTION__, [_exception name], [_exception reason]);
1148   [self closeConnection];
1149   [self->context setLastException:_exception];
1150   return nil;
1151 }
1152 - (NSException *)_processUnknownCommandParserException:(NSException *)_ex {
1153   NSLog(@"ERROR(%s): catched non-IMAP4 parsing exception %@: %@",
1154         __PRETTY_FUNCTION__, [_ex name], [_ex reason]);
1155   return nil;
1156 }
1157
1158 - (NSException *)_handleShutdownDuringCommandException:(NSException *)_ex {
1159   NSLog(@"ERROR(%s): IMAP4 socket was shut down by server %@: %@",
1160         __PRETTY_FUNCTION__, [_ex name], [_ex reason]);
1161   [self closeConnection];
1162   [self->context setLastException:_ex];
1163   return nil;
1164 }
1165
1166 - (BOOL)_isShutdownException:(NSException *)_ex {
1167   return [[_ex name] isEqualToString:@"NGSocketShutdownDuringReadException"];
1168 }
1169
1170 - (BOOL)_isLoginCommand:(NSString *)_command {
1171   return [_command hasPrefix:@"login"];
1172 }
1173
1174 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
1175   withNotification:(BOOL)_notification logText:(NSString *)_txt
1176 {
1177   NGHashMap    *map;
1178   BOOL         tryReconnect;
1179   int          reconnectCnt;
1180   NSException  *exception;
1181
1182   struct timeval tv;
1183   double         ti = 0.0;
1184
1185   if (ProfileImapEnabled == 1) {
1186     gettimeofday(&tv, NULL);
1187     ti =  (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
1188     fprintf(stderr, "{");
1189   }
1190   tryReconnect = NO;
1191   reconnectCnt = 0;
1192   map          = nil;
1193   exception    = nil;
1194
1195   do {
1196     tryReconnect  = NO;
1197     [self->context resetLastException];
1198     NS_DURING {
1199       NSException *e = nil; // TODO: try to remove exception handler
1200       
1201       [self sendCommand:_command withTag:_tag logText:_txt];
1202       map = [self->parser parseResponseForTagId:self->tagId exception:&e];
1203       [e raise];
1204       tryReconnect = NO;
1205     }
1206     NS_HANDLER {
1207       if ([localException isKindOfClass:[NGImap4ParserException class]]) {
1208         [[self _processCommandParserException:localException] raise];
1209       }
1210       else if ([self _isShutdownException:localException]) {
1211         [[self _handleShutdownDuringCommandException:localException] raise];
1212       }
1213       else {
1214         [[self _processUnknownCommandParserException:localException] raise];
1215         if (reconnectCnt == 0) {
1216           if (![self _isLoginCommand:_command]) {
1217             reconnectCnt++;
1218             tryReconnect = YES;
1219             exception    = localException;
1220           }
1221         }
1222         [self closeConnection];
1223         [self->context setLastException:localException];
1224       }
1225     }
1226     NS_ENDHANDLER;
1227
1228     if (tryReconnect) {
1229       [self reconnect];
1230     }
1231     else if ([map objectForKey:@"bye"] && ![_command hasPrefix:@"logout"]) {
1232       if (reconnectCnt == 0) {
1233         reconnectCnt++;
1234         tryReconnect = YES;
1235         [self reconnect];
1236       }
1237     }
1238   } while (tryReconnect);
1239
1240   if ([self->context lastException]) {
1241     if (exception) {
1242       [self->context setLastException:exception];
1243     }
1244     return nil;
1245   }
1246   if (_notification) [self sendResponseNotification:map];
1247
1248   if (ProfileImapEnabled == 1) {
1249     gettimeofday(&tv, NULL);
1250     ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
1251     fprintf(stderr, "}[%s] <Send Command [%s]> : time needed: %4.4fs\n",
1252            __PRETTY_FUNCTION__, [_command cString], ti < 0.0 ? -1.0 : ti);    
1253   }
1254   return map;
1255 }
1256
1257 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
1258   withNotification:(BOOL)_notification
1259 {
1260   return [self processCommand:_command withTag:_tag
1261                withNotification:_notification
1262                logText:_command];
1263 }
1264
1265 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag {
1266   return [self processCommand:_command withTag:_tag withNotification:YES
1267                logText:_command];
1268 }
1269
1270 - (NGHashMap *)processCommand:(NSString *)_command {
1271   return [self processCommand:_command withTag:YES withNotification:YES
1272                logText:_command];
1273 }
1274
1275 - (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt {
1276   return [self processCommand:_command withTag:YES withNotification:YES
1277                logText:_txt];
1278 }
1279
1280 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag
1281   logText:(NSString *)_txt
1282 {
1283   NSString      *command;
1284   NGCTextStream *txtStream;
1285
1286   txtStream = [self textStream];
1287
1288   if (_tag) {
1289     self->tagId++;
1290
1291     command = [NSString stringWithFormat:@"%d %@", self->tagId, _command];
1292     if (self->debug) {
1293       _txt = [NSString stringWithFormat:@"%d %@", self->tagId, _txt];
1294     }
1295   }
1296   else
1297     command = _command;
1298
1299   if (self->debug) {
1300     if ([_txt length] > 5000) {
1301       fprintf(stderr, "C[%p]: %s...\n", self, [[_txt substringToIndex:5000]
1302                                                   cString]);
1303     }
1304     else {
1305       fprintf(stderr, "C[%p]: %s\n", self, [_txt cString]);
1306     }
1307   }
1308   
1309   if (![txtStream writeString:command])
1310     [self->context setLastException:[txtStream lastException]];
1311   else if (![txtStream writeString:@"\r\n"])
1312     [self->context setLastException:[txtStream lastException]];
1313   else if (![txtStream flush])
1314     [self->context setLastException:[txtStream lastException]];
1315 }
1316   
1317 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag {
1318   [self sendCommand:_command withTag:_tag logText:_command];
1319 }
1320
1321 - (void)sendCommand:(NSString *)_command {
1322   [self sendCommand:_command withTag:YES logText:_command];
1323 }
1324
1325 - (NSArray *)_flags2ImapFlags:(NSArray *)_flags {
1326   /* adds backslashes in front of the flags */
1327   NSEnumerator *enumerator;
1328   NSArray      *result;
1329   id           obj;
1330   id           *objs;
1331   unsigned     cnt;
1332   
1333   objs = calloc([_flags count] + 2, sizeof(id));
1334   cnt  = 0;
1335   enumerator = [_flags objectEnumerator];
1336   while ((obj = [enumerator nextObject])) {
1337     objs[cnt] = [@"\\" stringByAppendingString:obj];
1338     cnt++;
1339   }
1340   result = [NSArray arrayWithObjects:objs count:cnt];
1341   if (objs != NULL) free(objs);
1342   return result;
1343 }
1344 static inline NSArray *_flags2ImapFlags(NGImap4Client *self, NSArray *_flags) {
1345   return [self _flags2ImapFlags:_flags];
1346 }
1347
1348 - (NSString *)_folder2ImapFolder:(NSString *)_folder {
1349   NSArray *array;
1350   
1351   if (self->delimiter == nil) {
1352     NSDictionary *res;
1353
1354     res = [self list:@"" pattern:@""];
1355
1356     if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
1357       return nil;
1358   }
1359
1360   array = [_folder pathComponents];
1361
1362   if ([array count] > 0) {
1363     NSString *o;
1364
1365     o = [array objectAtIndex:0];
1366     if (([o isEqualToString:@"/"]) || ([o length] == 0))
1367       array = [array subarrayWithRange:NSMakeRange(1, [array count] - 1)];
1368     
1369     o = [array lastObject];
1370     if (([o length] == 0) || ([o isEqualToString:@"/"]))
1371       array = [array subarrayWithRange:NSMakeRange(0, [array count] - 1)];
1372   }
1373   return [[array componentsJoinedByString:self->delimiter]
1374                  stringByEncodingImap4FolderName];
1375 }
1376
1377 - (NSString *)_imapFolder2Folder:(NSString *)_folder {
1378   NSArray *array;
1379   
1380   array = [NSArray arrayWithObject:@""];
1381
1382   if ([self delimiter] == nil) {
1383     NSDictionary *res;
1384     
1385     res = [self list:@"" pattern:@""]; // fill the delimiter ivar?
1386     if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
1387       return nil;
1388   }
1389   
1390   array = [array arrayByAddingObjectsFromArray:
1391                    [_folder componentsSeparatedByString:[self delimiter]]];
1392   
1393   return [[NSString pathWithComponents:array] stringByDecodingImap4FolderName];
1394 }
1395
1396 - (void)setContext:(NGImap4Context *)_ctx {
1397   self->context = _ctx;
1398 }
1399 - (NGImap4Context *)context {
1400   return self->context;
1401 }
1402
1403 /* ConnectionRegistration */
1404
1405 - (void)removeFromConnectionRegister {
1406   unsigned cnt;
1407   
1408   for (cnt = 0; cnt < MaxImapClients; cnt++) {
1409     if (ImapClients[cnt] == self)
1410       ImapClients[cnt] = nil;
1411   }
1412 }
1413
1414 - (void)registerConnection {
1415   int cnt;
1416
1417   cnt =  CountClient % MaxImapClients;
1418
1419   if (ImapClients[cnt]) {
1420     [(NGImap4Context *)ImapClients[cnt] closeConnection];
1421   }
1422   ImapClients[cnt] = self;
1423   CountClient++;
1424 }
1425
1426 - (id<NGExtendedTextStream>)textStream {
1427   if (self->text == nil) {
1428     if ([self->context lastException] == nil)
1429       [self reconnect];
1430   }
1431   return self->text;
1432 }
1433
1434 /* description */
1435
1436 - (NSString *)description {
1437   NSMutableString *ms;
1438   id tmp;
1439
1440   ms = [NSMutableString stringWithCapacity:128];
1441   [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
1442   
1443   if (self->login != nil)
1444     [ms appendFormat:@" login=%@%s", self->login, self->password?"(pwd)":""];
1445   
1446   if ((tmp = [self socket]) != nil)
1447     [ms appendFormat:@" socket=%@", tmp];
1448   else if (self->address)
1449     [ms appendFormat:@" address=%@", self->address];
1450   
1451   [ms appendString:@">"];
1452   return ms;
1453 }
1454
1455 @end /* NGImap4Client; */