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