]> err.no Git - sope/blob - sope-mime/NGImap4/NGImap4Client.m
71bf137b067d84623b921738cd2121a65da58efe
[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 [@" " stringByAppendingString: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 qualifier:(EOQualifier *)_qual
1047   encoding:(NSString *)_encoding
1048 {
1049   /* 
1050      http://www.ietf.org/internet-drafts/draft-ietf-imapext-sort-17.txt
1051
1052      The _sortSpec can be:
1053      - a simple 'raw' IMAP4 sort string
1054      - an EOSortOrdering
1055      - an array of EOSortOrderings
1056      
1057      The result dict contains the following keys:
1058       'result'      - a boolean
1059       'expunge'     - array            (of what?)
1060       'sort'        - array of uids in sort order
1061       'RawResponse' - the raw IMAP4 response
1062     
1063      If no sortable key was found, the sort will run against 'DATE'.
1064      => TODO: this is inconsistent. If none are passed in, false will be
1065               returned
1066      
1067      Eg: UID SORT ( DATE REVERSE SUBJECT ) UTF-8 TODO
1068   */
1069   NSString *tmp;
1070   
1071   if ([_sortSpec isKindOfClass:[NSArray class]])
1072     tmp = [self _generateIMAP4SortOrderings:_sortSpec];
1073   else if ([_sortSpec isKindOfClass:[EOSortOrdering class]])
1074     tmp = [self _generateIMAP4SortOrdering:_sortSpec];
1075   else
1076     tmp = [_sortSpec stringValue];
1077   
1078   if ([tmp length] == 0) { /* found no valid key use date sorting */
1079     [self logWithFormat:@"Note: no key found for sorting, using 'DATE': %@",
1080             _sortSpec];
1081     tmp = @"DATE";
1082   }
1083   
1084   return [self primarySort:tmp 
1085                qualifierString:[self _searchExprForQual:_qual]
1086                encoding:_encoding];
1087 }
1088 - (NSDictionary *)sort:(NSArray *)_sortOrderings
1089   qualifier:(EOQualifier *)_qual
1090 {
1091   // DEPRECATED, should not use context!
1092   return [self sort:_sortOrderings qualifier:_qual
1093                encoding:[[self context] sortEncoding]];
1094 }
1095
1096 - (NSDictionary *)searchWithQualifier:(EOQualifier *)_qualifier {
1097   NSString *s;
1098   
1099   s = [self _searchExprForQual:_qualifier];
1100   if ([s length] == 0) {
1101     // TODO: should set last-exception?
1102     [self logWithFormat:@"ERROR(%s): could not process search qualifier: %@",
1103           __PRETTY_FUNCTION__, _qualifier];
1104     return nil;
1105   }
1106   
1107   s = [@"search" stringByAppendingString:s];
1108   return [self->normer normalizeSearchResponse:[self processCommand:s]];
1109 }
1110
1111 /* Private Methods */
1112
1113 - (NSException *)_processCommandParserException:(NSException *)_exception {
1114   NSLog(@"ERROR(%s): catched IMAP4 parser exception %@: %@",
1115         __PRETTY_FUNCTION__, [_exception name], [_exception reason]);
1116   [self closeConnection];
1117   [self->context setLastException:_exception];
1118   return nil;
1119 }
1120 - (NSException *)_processUnknownCommandParserException:(NSException *)_ex {
1121   NSLog(@"ERROR(%s): catched non-IMAP4 parsing exception %@: %@",
1122         __PRETTY_FUNCTION__, [_ex name], [_ex reason]);
1123   return nil;
1124 }
1125
1126 - (NSException *)_handleShutdownDuringCommandException:(NSException *)_ex {
1127   NSLog(@"ERROR(%s): IMAP4 socket was shut down by server %@: %@",
1128         __PRETTY_FUNCTION__, [_ex name], [_ex reason]);
1129   [self closeConnection];
1130   [self->context setLastException:_ex];
1131   return nil;
1132 }
1133
1134 - (BOOL)_isShutdownException:(NSException *)_ex {
1135   return [[_ex name] isEqualToString:@"NGSocketShutdownDuringReadException"];
1136 }
1137
1138 - (BOOL)_isLoginCommand:(NSString *)_command {
1139   return [_command hasPrefix:@"login"];
1140 }
1141
1142 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
1143   withNotification:(BOOL)_notification logText:(NSString *)_txt
1144 {
1145   NGHashMap    *map;
1146   BOOL         tryReconnect;
1147   int          reconnectCnt;
1148   NSException  *exception;
1149
1150   struct timeval tv;
1151   double         ti = 0.0;
1152
1153   if (ProfileImapEnabled == 1) {
1154     gettimeofday(&tv, NULL);
1155     ti =  (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
1156     fprintf(stderr, "{");
1157   }
1158   tryReconnect = NO;
1159   reconnectCnt = 0;
1160   map          = nil;
1161   exception    = nil;
1162
1163   do {
1164     tryReconnect  = NO;
1165     [self->context resetLastException];
1166     NS_DURING {
1167       NSException *e = nil; // TODO: try to remove exception handler
1168       
1169       [self sendCommand:_command withTag:_tag logText:_txt];
1170       map = [self->parser parseResponseForTagId:self->tagId exception:&e];
1171       [e raise];
1172       tryReconnect = NO;
1173     }
1174     NS_HANDLER {
1175       if ([localException isKindOfClass:[NGImap4ParserException class]]) {
1176         [[self _processCommandParserException:localException] raise];
1177       }
1178       else if ([self _isShutdownException:localException]) {
1179         [[self _handleShutdownDuringCommandException:localException] raise];
1180       }
1181       else {
1182         [[self _processUnknownCommandParserException:localException] raise];
1183         if (reconnectCnt == 0) {
1184           if (![self _isLoginCommand:_command]) {
1185             reconnectCnt++;
1186             tryReconnect = YES;
1187             exception    = localException;
1188           }
1189         }
1190         [self closeConnection];
1191         [self->context setLastException:localException];
1192       }
1193     }
1194     NS_ENDHANDLER;
1195
1196     if (tryReconnect) {
1197       [self reconnect];
1198     }
1199     else if ([map objectForKey:@"bye"] && ![_command hasPrefix:@"logout"]) {
1200       if (reconnectCnt == 0) {
1201         reconnectCnt++;
1202         tryReconnect = YES;
1203         [self reconnect];
1204       }
1205     }
1206   } while (tryReconnect);
1207
1208   if ([self->context lastException]) {
1209     if (exception) {
1210       [self->context setLastException:exception];
1211     }
1212     return nil;
1213   }
1214   if (_notification) [self sendResponseNotification:map];
1215
1216   if (ProfileImapEnabled == 1) {
1217     gettimeofday(&tv, NULL);
1218     ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
1219     fprintf(stderr, "}[%s] <Send Command [%s]> : time needed: %4.4fs\n",
1220            __PRETTY_FUNCTION__, [_command cString], ti < 0.0 ? -1.0 : ti);    
1221   }
1222   return map;
1223 }
1224
1225 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag
1226   withNotification:(BOOL)_notification
1227 {
1228   return [self processCommand:_command withTag:_tag
1229                withNotification:_notification
1230                logText:_command];
1231 }
1232
1233 - (NGHashMap *)processCommand:(NSString *)_command withTag:(BOOL)_tag {
1234   return [self processCommand:_command withTag:_tag withNotification:YES
1235                logText:_command];
1236 }
1237
1238 - (NGHashMap *)processCommand:(NSString *)_command {
1239   return [self processCommand:_command withTag:YES withNotification:YES
1240                logText:_command];
1241 }
1242
1243 - (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt {
1244   return [self processCommand:_command withTag:YES withNotification:YES
1245                logText:_txt];
1246 }
1247
1248 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag
1249   logText:(NSString *)_txt
1250 {
1251   NSString      *command;
1252   NGCTextStream *txtStream;
1253
1254   txtStream = [self textStream];
1255
1256   if (_tag) {
1257     self->tagId++;
1258
1259     command = [NSString stringWithFormat:@"%d %@", self->tagId, _command];
1260     if (self->debug) {
1261       _txt = [NSString stringWithFormat:@"%d %@", self->tagId, _txt];
1262     }
1263   }
1264   else
1265     command = _command;
1266
1267   if (self->debug) {
1268     if ([_txt length] > 5000) {
1269       fprintf(stderr, "C[%p]: %s...\n", self, [[_txt substringToIndex:5000]
1270                                                   cString]);
1271     }
1272     else {
1273       fprintf(stderr, "C[%p]: %s\n", self, [_txt cString]);
1274     }
1275   }
1276   
1277   if (![txtStream writeString:command])
1278     [self->context setLastException:[txtStream lastException]];
1279   else if (![txtStream writeString:@"\r\n"])
1280     [self->context setLastException:[txtStream lastException]];
1281   else if (![txtStream flush])
1282     [self->context setLastException:[txtStream lastException]];
1283 }
1284   
1285 - (void)sendCommand:(NSString *)_command withTag:(BOOL)_tag {
1286   [self sendCommand:_command withTag:_tag logText:_command];
1287 }
1288
1289 - (void)sendCommand:(NSString *)_command {
1290   [self sendCommand:_command withTag:YES logText:_command];
1291 }
1292
1293 - (NSArray *)_flags2ImapFlags:(NSArray *)_flags {
1294   /* adds backslashes in front of the flags */
1295   NSEnumerator *enumerator;
1296   NSArray      *result;
1297   id           obj;
1298   id           *objs;
1299   unsigned     cnt;
1300   
1301   objs = calloc([_flags count] + 2, sizeof(id));
1302   cnt  = 0;
1303   enumerator = [_flags objectEnumerator];
1304   while ((obj = [enumerator nextObject])) {
1305     objs[cnt] = [@"\\" stringByAppendingString:obj];
1306     cnt++;
1307   }
1308   result = [NSArray arrayWithObjects:objs count:cnt];
1309   if (objs != NULL) free(objs);
1310   return result;
1311 }
1312 static inline NSArray *_flags2ImapFlags(NGImap4Client *self, NSArray *_flags) {
1313   return [self _flags2ImapFlags:_flags];
1314 }
1315
1316 - (NSString *)_folder2ImapFolder:(NSString *)_folder {
1317   NSArray *array;
1318   
1319   if (self->delimiter == nil) {
1320     NSDictionary *res;
1321
1322     res = [self list:@"" pattern:@""];
1323
1324     if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
1325       return nil;
1326   }
1327
1328   array = [_folder pathComponents];
1329
1330   if ([array count] > 0) {
1331     NSString *o;
1332
1333     o = [array objectAtIndex:0];
1334     if (([o isEqualToString:@"/"]) || ([o length] == 0))
1335       array = [array subarrayWithRange:NSMakeRange(1, [array count] - 1)];
1336     
1337     o = [array lastObject];
1338     if (([o length] == 0) || ([o isEqualToString:@"/"]))
1339       array = [array subarrayWithRange:NSMakeRange(0, [array count] - 1)];
1340   }
1341   return [[array componentsJoinedByString:self->delimiter]
1342                  stringByEncodingImap4FolderName];
1343 }
1344
1345 - (NSString *)_imapFolder2Folder:(NSString *)_folder {
1346   NSArray *array;
1347   
1348   array = [NSArray arrayWithObject:@""];
1349
1350   if ([self delimiter] == nil) {
1351     NSDictionary *res;
1352     
1353     res = [self list:@"" pattern:@""]; // fill the delimiter ivar?
1354     if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
1355       return nil;
1356   }
1357   
1358   array = [array arrayByAddingObjectsFromArray:
1359                    [_folder componentsSeparatedByString:[self delimiter]]];
1360   
1361   return [[NSString pathWithComponents:array] stringByDecodingImap4FolderName];
1362 }
1363
1364 - (void)setContext:(NGImap4Context *)_ctx {
1365   self->context = _ctx;
1366 }
1367 - (NGImap4Context *)context {
1368   return self->context;
1369 }
1370
1371 /* ConnectionRegistration */
1372
1373 - (void)removeFromConnectionRegister {
1374   unsigned cnt;
1375   
1376   for (cnt = 0; cnt < MaxImapClients; cnt++) {
1377     if (ImapClients[cnt] == self)
1378       ImapClients[cnt] = nil;
1379   }
1380 }
1381
1382 - (void)registerConnection {
1383   int cnt;
1384
1385   cnt =  CountClient % MaxImapClients;
1386
1387   if (ImapClients[cnt]) {
1388     [(NGImap4Context *)ImapClients[cnt] closeConnection];
1389   }
1390   ImapClients[cnt] = self;
1391   CountClient++;
1392 }
1393
1394 - (NGCTextStream *)textStream {
1395   if (self->text == nil) {
1396     if ([self->context lastException] == nil)
1397       [self reconnect];
1398   }
1399   return (NGCTextStream *)self->text;
1400 }
1401
1402 /* description */
1403
1404 - (NSString *)description {
1405   NSMutableString *ms;
1406   id tmp;
1407
1408   ms = [NSMutableString stringWithCapacity:128];
1409   [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
1410   
1411   if (self->login != nil)
1412     [ms appendFormat:@" login=%@%s", self->login, self->password?"(pwd)":""];
1413   
1414   if ((tmp = [self socket]) != nil)
1415     [ms appendFormat:@" socket=%@", tmp];
1416   else if (self->address)
1417     [ms appendFormat:@" address=%@", self->address];
1418   
1419   [ms appendString:@">"];
1420   return ms;
1421 }
1422
1423 @end /* NGImap4Client; */