]> err.no Git - sope/blob - sope-mime/NGImap4/NGSieveClient.m
fixed OGo bug #1899
[sope] / sope-mime / NGImap4 / NGSieveClient.m
1 /*
2   Copyright (C) 2000-2007 SKYRIX Software AG
3   Copyright (C) 2007      Helge Hess
4
5   This file is part of SOPE.
6
7   SOPE is free software; you can redistribute it and/or modify it under
8   the terms of the GNU Lesser General Public License as published by the
9   Free Software Foundation; either version 2, or (at your option) any
10   later version.
11
12   SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
13   WARRANTY; without even the implied warranty of MERCHANTABILITY or
14   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
15   License for more details.
16
17   You should have received a copy of the GNU Lesser General Public
18   License along with SOPE; see the file COPYING.  If not, write to the
19   Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
20   02111-1307, USA.
21 */
22
23 #include <unistd.h>
24
25 #include "NGSieveClient.h"
26 #include "NGImap4Support.h"
27 #include "NGImap4ResponseParser.h"
28 #include "NSString+Imap4.h"
29 #include "imCommon.h"
30 #include <sys/time.h>
31
32 @interface NGSieveClient(Private)
33
34 - (NGHashMap *)processCommand:(id)_command;
35 - (NGHashMap *)processCommand:(id)_command logText:(id)_txt;
36
37 - (NSException *)sendCommand:(id)_command;
38 - (NSException *)sendCommand:(id)_command logText:(id)_txt;
39 - (NSException *)sendCommand:(id)_command logText:(id)_txt attempts:(int)_c;
40
41 - (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map;
42 - (NSMutableDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map;
43 - (NSDictionary *)login;
44
45 /* parsing */
46
47 - (NSString *)readStringToCRLF;
48 - (NSString *)readString;
49
50 @end
51
52 /*
53   An implementation of an Imap4 client
54   
55   A folder name always looks like an absolute filename (/inbox/blah)
56   
57   NOTE: Sieve is just the filtering language ...
58   
59   This should be ACAP?
60     http://asg.web.cmu.edu/rfc/rfc2244.html
61
62   ---snip---
63 "IMPLEMENTATION" "Cyrus timsieved v2.1.15-IPv6-Debian-2.1.15-0woody.1.0"
64 "SASL" "PLAIN"
65 "SIEVE" "fileinto reject envelope vacation imapflags notify subaddress relational regex"
66 "STARTTLS"
67 OK
68   ---snap---
69 */
70
71 @implementation NGSieveClient
72
73 static int      defaultSievePort   = 2000;
74 static NSNumber *YesNumber         = nil;
75 static NSNumber *NoNumber          = nil;
76 static BOOL     ProfileImapEnabled = NO;
77 static BOOL     LOG_PASSWORD       = NO;
78 static BOOL     debugImap4         = NO;
79
80 + (void)initialize {
81   static BOOL didInit = NO;
82   NSUserDefaults *ud;
83   if (didInit) return;
84   didInit = YES;
85   
86   ud = [NSUserDefaults standardUserDefaults];
87   LOG_PASSWORD       = [ud boolForKey:@"SieveLogPassword"];
88   ProfileImapEnabled = [ud boolForKey:@"ProfileImapEnabled"];
89   debugImap4         = [ud boolForKey:@"ImapDebugEnabled"];
90   
91   YesNumber = [[NSNumber numberWithBool:YES] retain];
92   NoNumber  = [[NSNumber numberWithBool:NO] retain];
93 }
94
95 + (id)clientWithURL:(id)_url {
96   return [[(NGSieveClient *)[self alloc] initWithURL:_url] autorelease];
97 }
98
99 + (id)clientWithAddress:(id<NGSocketAddress>)_address {
100   NGSieveClient *client;
101   
102   client = [self alloc];
103   return [[client initWithAddress:_address] autorelease];
104 }
105
106 + (id)clientWithHost:(id)_host {
107   return [[[self alloc] initWithHost:_host] autorelease];
108 }
109
110 - (id)initWithNSURL:(NSURL *)_url {
111   NGInternetSocketAddress *a;
112   int port;
113   
114   if ((port = [[_url port] intValue]) == 0)
115     port = defaultSievePort;
116   
117   a = [NGInternetSocketAddress addressWithPort:port 
118                                onHost:[_url host]];
119   if ((self = [self initWithAddress:a])) {
120     self->login    = [[_url user]     copy];
121     self->password = [[_url password] copy];
122   }
123   return self;
124 }
125 - (id)initWithURL:(id)_url {
126   if (_url == nil) {
127     [self release];
128     return nil;
129   }
130   
131   if (![_url isKindOfClass:[NSURL class]])
132     _url = [NSURL URLWithString:[_url stringValue]];
133   
134   return [self initWithNSURL:_url];
135 }
136
137 - (id)initWithHost:(id)_host {
138   NGInternetSocketAddress *a;
139   
140   a = [NGInternetSocketAddress addressWithPort:defaultSievePort onHost:_host];
141   return [self initWithAddress:a];
142 }
143
144 - (id)initWithAddress:(id<NGSocketAddress>)_address { // di
145   if ((self = [super init])) {
146     self->address = [_address retain];
147     self->debug   = debugImap4;
148   }
149   return self;
150 }
151
152 - (void)dealloc {
153   [self->lastException release];
154   [self->address  release];
155   [self->io       release];
156   [self->socket   release];
157   [self->parser   release];
158   [self->login    release];
159   [self->password release];
160   [super dealloc];
161 }
162
163 /* equality */
164
165 - (BOOL)isEqual:(id)_obj {
166   if (_obj == self)
167     return YES;
168   if ([_obj isKindOfClass:[NGSieveClient class]])
169     return [self isEqualToSieveClient:_obj];
170   return NO;
171 }
172
173 - (BOOL)isEqualToSieveClient:(NGSieveClient *)_obj {
174   if (_obj == self) return YES;
175   if (_obj == nil)  return NO;
176   return [[_obj address] isEqual:self->address];
177 }
178
179 /* accessors */
180
181 - (id<NGActiveSocket>)socket {
182   return self->socket;
183 }
184
185 - (id<NGSocketAddress>)address {
186   return self->address;
187 }
188
189 /* exceptions */
190
191 - (void)setLastException:(NSException *)_ex {
192   ASSIGN(self->lastException, _ex);
193 }
194 - (NSException *)lastException {
195   return self->lastException;
196 }
197 - (void)resetLastException {
198   [self->lastException release];
199   self->lastException = nil;
200 }
201
202 /* connection */
203
204 - (void)resetStreams {
205   [self->socket release]; self->socket = nil;
206   [self->io     release]; self->io     = nil;
207   [self->parser release]; self->parser = nil;
208 }
209
210 - (NSDictionary *)openConnection {
211   struct timeval tv;
212   double         ti = 0.0;
213   
214   if (ProfileImapEnabled) {
215     gettimeofday(&tv, NULL);
216     ti =  (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
217   }
218   
219   [self resetStreams];
220   
221   self->socket =
222     [[NGActiveSocket socketConnectedToAddress:self->address] retain];
223   if (self->socket == nil) {
224     [self logWithFormat:@"ERROR: could not connect: %@", self->address];
225     return nil;
226   }
227
228   self->io     = [NGBufferedStream alloc]; // keep gcc happy
229   self->io     = [self->io initWithSource:(id)self->socket];
230   self->parser = [[NGImap4ResponseParser alloc] initWithStream:self->socket];
231
232   /* receive greeting from server without tag-id */
233
234   if (ProfileImapEnabled) {
235     gettimeofday(&tv, NULL);
236     ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
237     fprintf(stderr, "[%s] <openConnection> : time needed: %4.4fs\n",
238            __PRETTY_FUNCTION__, ti < 0.0 ? -1.0 : ti);    
239   }
240   return [self normalizeOpenConnectionResponse:
241                [self->parser parseSieveResponse]];
242 }
243
244 - (NSNumber *)isConnected {
245   /*
246     Check whether stream is already open (could be closed due to a server
247     timeout)
248   */
249   // TODO: why does that return an object?
250   if (self->socket == nil)
251     return [NSNumber numberWithBool:NO];
252   
253   return [NSNumber numberWithBool:[(NGActiveSocket *)self->socket isAlive]];
254 }
255
256 - (void)closeConnection {
257   [self->socket close];
258   [self->socket release]; self->socket = nil;
259   [self->parser release]; self->parser = nil;
260 }
261
262 - (NSDictionary *)login:(NSString *)_login password:(NSString *)_passwd {
263   /* login with plaintext password authenticating */
264   
265   if ((_login == nil) || (_passwd == nil))
266     return nil;
267   
268   [self->login    release]; self->login    = nil;
269   [self->password release]; self->password = nil;
270   
271   self->login    = [_login  copy];
272   self->password = [_passwd copy];
273   return [self login];
274 }
275
276 - (void)reconnect {
277   [self closeConnection];  
278   [self openConnection];
279   [self login];
280 }
281
282 - (NSDictionary *)login {
283   NGHashMap *map  = nil;
284   NSData    *auth;
285   char      *buf;
286   int       bufLen, logLen;
287   
288   if (![self->socket isConnected]) {
289     NSDictionary *con;
290     
291     if ((con = [self openConnection]) == nil)
292       return nil;
293     if (![[con objectForKey:@"result"] boolValue])
294       return con;
295   }
296   
297   logLen = [self->login cStringLength];
298   bufLen = (logLen * 2) + [self->password cStringLength] +2;
299   
300   buf = calloc(bufLen + 2, sizeof(char));
301   
302   /*
303     Format:
304       authenticate-id
305       authorize-id
306       password
307   */
308   sprintf(buf, "%s %s %s", 
309           [self->login cString], [self->login cString],
310           [self->password cString]);
311   
312   buf[logLen] = '\0';
313   buf[logLen * 2 + 1] = '\0';
314   
315   auth = [NSData dataWithBytesNoCopy:buf length:bufLen];
316   auth = [auth dataByEncodingBase64WithLineLength:4096 /* 'unlimited' */];
317   
318   if (LOG_PASSWORD) {
319     NSString *s;
320     
321     s = [NSString stringWithFormat:@"AUTHENTICATE \"PLAIN\" {%d+}\r\n%s",
322                   [auth length], [auth bytes]];
323     map = [self processCommand:s];
324   }
325   else {
326     NSString *s;
327     
328     s = [NSString stringWithFormat:@"AUTHENTICATE \"PLAIN\" {%d+}\r\n%s",
329                   [auth length], [auth bytes]];
330     map = [self processCommand:s
331                 logText:@"AUTHENTICATE \"PLAIN\" {%d+}\r\nLOGIN:PASSWORD\r\n"];
332   }
333
334   if (map == nil) {
335     [self logWithFormat:@"ERROR: got no result from command."];
336     return nil;
337   }
338   
339   return [self normalizeResponse:map];
340 }
341
342 /* logout from the connected host and close the connection */
343
344 - (NSDictionary *)logout {
345   NGHashMap *map;
346
347   map = [self processCommand:@"logout"]; // TODO: check for success!
348   [self closeConnection];
349   return [self normalizeResponse:map];
350 }
351
352 - (NSString *)getScript:(NSString *)_scriptName {
353   NSException *ex;
354   NSString *script, *s;
355   
356   s = [@"GETSCRIPT \"" stringByAppendingString:_scriptName];
357   s = [s stringByAppendingString:@"\""];
358   ex = [self sendCommand:s logText:s attempts:3];
359   if (ex != nil) {
360     [self logWithFormat:@"ERROR: could not get script: %@", ex];
361     [self setLastException:ex];
362     return nil;
363   }
364   
365   /* read script string */
366   
367   if ((script = [[self readString] autorelease]) == nil)
368     return nil;
369   
370   if ([script hasPrefix:@"O "] || [script hasPrefix:@"NO "]) {
371     // TODO: not exactly correct, script could begin with this signature
372     // Note: readString read 'NO ...', but the first char is consumed
373     
374     [self logWithFormat:@"ERROR: status line reports: '%@'", script];
375     return nil;
376   }
377   
378   NSLog(@"str: %@", script);
379   
380   /* read response code */
381   
382   if ((s = [self readStringToCRLF]) == nil) {
383     [self logWithFormat:@"ERROR: could not parse status line."];
384     return nil;
385   }
386   if (![s isNotEmpty]) { // remainder of previous string
387     [s release];
388     if ((s = [self readStringToCRLF]) == nil) {
389       [self logWithFormat:@"ERROR: could not parse status line."];
390       return nil;
391     }
392   }
393   
394   if (![s hasPrefix:@"OK"]) {
395     [self logWithFormat:@"ERROR: status line reports: '%@'", s];
396     [s release];
397     return nil;
398   }
399   [s release];
400   
401   return script;
402 }
403
404 - (BOOL)isValidScriptName:(NSString *)_name {
405   return [_name isNotEmpty];
406 }
407
408 - (NSDictionary *)putScript:(NSString *)_name script:(NSString *)_script {
409   // TODO: script should be send in UTF-8!
410   NGHashMap *map;
411   NSString  *s;
412   
413   if (![self isValidScriptName:_name]) {
414     [self logWithFormat:@"%s: missing script-name", __PRETTY_FUNCTION__];
415     return nil;
416   }
417   if (![_script isNotEmpty]) {
418     [self logWithFormat:@"%s: missing script", __PRETTY_FUNCTION__];
419     return nil;
420   }
421   
422   s = @"PUTSCRIPT \"";
423   s = [s stringByAppendingString:_name];
424   s = [s stringByAppendingString:@"\" "];
425   s = [s stringByAppendingFormat:@"{%d+}\r\n%@", [_script length], _script];
426   s = [s stringByAppendingString:@"\r\n"];
427   map = [self processCommand:s];
428   return [self normalizeResponse:map];
429 }
430
431 - (NSDictionary *)setActiveScript:(NSString *)_name {
432   NGHashMap *map;
433   
434   if (![self isValidScriptName:_name]) {
435     NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__);
436     return nil;
437   }
438   map = [self processCommand:
439               [NSString stringWithFormat:@"SETACTIVE \"%@\"\r\n", _name]];
440   return [self normalizeResponse:map];
441 }
442
443 - (NSDictionary *)deleteScript:(NSString *)_name {
444   NGHashMap *map;
445   NSString  *s;
446
447   if (![self isValidScriptName:_name]) {
448     NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__);
449     return nil;
450   }
451   
452   s = [NSString stringWithFormat:@"DELETESCRIPT \"%@\"\r\n", _name];
453   map = [self processCommand:s];
454   return [self normalizeResponse:map];
455 }
456
457 - (NSDictionary *)listScripts {
458   NSMutableDictionary *md;
459   NSException *ex;
460   NSString *line;
461   
462   ex = [self sendCommand:@"LISTSCRIPTS" logText:@"LISTSCRIPTS" attempts:3];
463   if (ex != nil) {
464     [self logWithFormat:@"ERROR: could not list scripts: %@", ex];
465     [self setLastException:ex];
466     return nil;
467   }
468   
469   /* read response */
470   
471   md = [NSMutableDictionary dictionaryWithCapacity:16];
472   while ((line = [self readStringToCRLF]) != nil) {
473     if ([line hasPrefix:@"OK"])
474       break;
475     
476     if ([line hasPrefix:@"NO"]) {
477       md = nil;
478       break;
479     }
480     
481     if ([line hasPrefix:@"{"]) {
482       [self logWithFormat:@"unsupported list response line: '%@'", line];
483     }
484     else if ([line hasPrefix:@"\""]) {
485       NSString *s;
486       NSRange  r;
487       BOOL     isActive;
488       
489       s = [line substringFromIndex:1];
490       r = [s rangeOfString:@"\""];
491       
492       if (r.length == 0) {
493         [self logWithFormat:@"missing closing quote in line: '%@'", line];
494         [line release]; line = nil;
495         continue;
496       }
497       
498       s = [s substringToIndex:r.location];
499       isActive = [line rangeOfString:@"ACTIVE"].length == 0 ? NO : YES;
500       
501       [md setObject:isActive ? @"ACTIVE" : @"" forKey:s];
502     }
503     else {
504       [self logWithFormat:@"unexpected list response line (%d): '%@'", 
505             [line length], line];
506     }
507     
508     [line release]; line = nil;
509   }
510   
511   [line release]; line = nil;
512   
513   return md;
514 }
515
516
517 - (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map {
518   /*
519     Filter for all responses
520        result  : NSNumber (response result)
521        exists  : NSNumber (number of exists mails in selectet folder
522        recent  : NSNumber (number of recent mails in selectet folder
523        expunge : NSArray  (message sequence number of expunged mails
524                           in selectet folder)
525   */
526   id keys[3], values[3];
527   NSParameterAssert(_map != nil);
528   
529   keys[0] = @"RawResponse"; values[0] = _map;
530   keys[1] = @"result";
531   values[1] = [[_map objectForKey:@"ok"] boolValue] ? YesNumber : NoNumber;
532   return [NSMutableDictionary dictionaryWithObjects:values
533                               forKeys:keys count:2];
534 }
535
536 - (NSMutableDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map {
537   /* filter for open connection */
538   NSMutableDictionary *result;
539   NSString *tmp;
540   
541   result = [self normalizeResponse:_map];
542   
543   if (![[[_map objectEnumeratorForKey:@"ok"] nextObject] boolValue])
544     return result;
545
546   if ((tmp = [_map objectForKey:@"implementation"]))
547     [result setObject:tmp forKey:@"server"];
548   if ((tmp = [_map objectForKey:@"sieve"]))
549     [result setObject:tmp forKey:@"capabilities"];
550   return result;
551 }
552
553 /* Private Methods */
554
555 - (BOOL)handleProcessException:(NSException *)_exception
556   repetitionCount:(int)_cnt
557 {
558   if (_cnt > 3) {
559     [_exception raise];
560     return NO;
561   }
562   
563   if ([_exception isKindOfClass:[NGIOException class]]) {
564     [self logWithFormat:
565             @"WARNING: got exception try to restore connection: %@", 
566             _exception];
567     return YES;
568   }
569   if ([_exception isKindOfClass:[NGImap4ParserException class]]) {
570     [self logWithFormat:
571             @"WARNING: Got Parser-Exception try to restore connection: %@",
572             _exception];
573     return YES;
574   }
575   
576   [_exception raise];
577   return NO;
578 }
579
580 - (void)waitPriorReconnectWithRepetitionCount:(int)_cnt {
581   unsigned timeout;
582   
583   timeout = _cnt * 4;
584   [self logWithFormat:@"reconnect to %@, sleeping %d seconds ...",
585           self->address, timeout];
586   sleep(timeout);
587   [self logWithFormat:@"reconnect ..."];
588 }
589
590 - (NGHashMap *)processCommand:(id)_command logText:(id)_txt {
591   NGHashMap *map          = nil;
592   BOOL      repeatCommand = NO;
593   int       repeatCnt     = 0;
594   struct timeval tv;
595   double         ti = 0.0;
596   
597   if (ProfileImapEnabled) {
598     gettimeofday(&tv, NULL);
599     ti =  (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
600     fprintf(stderr, "{");
601   }
602   do { /* TODO: shouldn't that be a while loop? */
603     if (repeatCommand) {
604       if (repeatCnt > 1)
605         [self waitPriorReconnectWithRepetitionCount:repeatCnt];
606       
607       repeatCnt++;
608       [self reconnect];
609       repeatCommand = NO;
610     }
611     
612     NS_DURING {
613       NSException *ex;
614       
615       if ((ex = [self sendCommand:_command logText:_txt]) != nil) {
616         repeatCommand = [self handleProcessException:ex 
617                               repetitionCount:repeatCnt];
618       }
619       else
620         map = [self->parser parseSieveResponse];
621     }
622     NS_HANDLER {
623       repeatCommand = [self handleProcessException:localException
624                             repetitionCount:repeatCnt];
625     }
626     NS_ENDHANDLER;    
627   }
628   while (repeatCommand);
629   
630   if (ProfileImapEnabled) {
631     gettimeofday(&tv, NULL);
632     ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
633     fprintf(stderr, "}[%s] <Send Command> : time needed: %4.4fs\n",
634            __PRETTY_FUNCTION__, ti < 0.0 ? -1.0 : ti);    
635   }
636   
637   return map;
638 }
639
640 - (NGHashMap *)processCommand:(id)_command {
641   return [self processCommand:_command logText:_command];
642 }
643
644 - (NSException *)sendCommand:(id)_command logText:(id)_txt {
645   NSString *command = nil;
646   
647   if ((command = _command) == nil) /* missing command */
648     return nil; // TODO: return exception?
649   
650   /* log */
651
652   if (self->debug) {
653     if ([_txt isKindOfClass:[NSData class]]) {
654       fprintf(stderr, "C: ");
655       fwrite([_txt bytes], [_txt length], 1, stderr);
656       fputc('\n', stderr);
657     }
658     else
659       fprintf(stderr, "C: %s\n", [_txt cString]);
660   }
661
662   /* write */
663   
664   if (![_command isKindOfClass:[NSData class]])
665     _command = [command dataUsingEncoding:NSUTF8StringEncoding];
666   
667   if (![self->io safeWriteData:_command])
668     return [self->io lastException];
669   if (![self->io writeBytes:"\r\n" count:2])
670     return [self->io lastException];
671   if (![self->io flush])
672     return [self->io lastException];
673   
674   return nil;
675 }
676
677 - (NSException *)sendCommand:(id)_command {
678   return [self sendCommand:_command logText:_command];
679 }
680
681 - (NSException *)sendCommand:(id)_command logText:(id)_txt attempts:(int)_c {
682   NSException *ex;
683   BOOL tryAgain;
684   int  repeatCnt;
685   
686   for (tryAgain = YES, repeatCnt = 0, ex = nil; tryAgain; repeatCnt++) {
687     if (repeatCnt > 0) {
688       if (repeatCnt > 1) /* one repeat goes without delay */
689         [self waitPriorReconnectWithRepetitionCount:repeatCnt];
690       [self reconnect];
691       tryAgain = NO;
692     }
693     
694     NS_DURING
695       ex = [self sendCommand:_command logText:_txt];
696     NS_HANDLER
697       ex = [localException retain];
698     NS_ENDHANDLER;
699     
700     if (ex == nil) /* everything is fine */
701       break;
702     
703     if (repeatCnt > _c) /* reached max attempts */
704       break;
705     
706     /* try again for certain exceptions */
707     tryAgain = [self handleProcessException:ex repetitionCount:repeatCnt];
708   }
709   
710   return ex;
711 }
712
713 /* low level */
714
715 - (int)readByte {
716   unsigned char c;
717   
718   if (![self->io readBytes:&c count:1]) {
719     [self setLastException:[self->io lastException]];
720     return -1;
721   }
722   return c;
723 }
724
725 - (NSString *)readLiteral {
726   /* 
727      Assumes 1st char is consumed, returns a retained string.
728      
729      Parses: "{" number [ "+" ] "}" CRLF *OCTET
730   */
731   unsigned char countBuf[16];
732   int      i;
733   unsigned byteCount;
734   unsigned char *octets;
735   
736   /* read count */
737   
738   for (i = 0; i < 14; i++) {
739     int c;
740     
741     if ((c = [self readByte]) == -1)
742       return nil;
743     if (c == '}')
744       break;
745     
746     countBuf[i] = c;
747   }
748   countBuf[i] = '\0';
749   byteCount = i > 0 ? atoi((char *)countBuf) : 0;
750   
751   /* read CRLF */
752   
753   i = [self readByte];
754   if (i != '\n') {
755     if (i == '\r' && i != -1)
756       i = [self readByte];
757     if (i == -1)
758       return nil;
759   }
760   
761   /* read octet */
762   
763   if (byteCount == 0)
764     return @"";
765   
766   octets = malloc(byteCount + 4);
767   if (![self->io safeReadBytes:octets count:byteCount]) {
768     [self setLastException:[self->io lastException]];
769     return nil;
770   }
771   octets[byteCount] = '\0';
772   
773   return [[NSString alloc] initWithUTF8String:(char *)octets];
774 }
775
776 - (NSString *)readQuoted {
777   /* 
778      assumes 1st char is consumed, returns a retained string
779
780      Note: quoted strings are limited to 1KB!
781   */
782   unsigned char buf[1032];
783   int i, c;
784   
785   i = 0;
786   do {
787     c      = [self readByte];
788     buf[i] = c;
789     i++;
790   }
791   while ((c != -1) && (c != '"'));
792   buf[i] = '\0';
793   
794   if (c == -1)
795     return nil;
796   
797   return [[NSString alloc] initWithUTF8String:(char *)buf];
798 }
799
800 - (NSString *)readStringToCRLF {
801   unsigned char buf[1032];
802   int i, c;
803   
804   i = 0;
805   do {
806     c = [self readByte];
807     if (c == '\n' || c == '\r')
808       break;
809     
810     buf[i] = c;
811     i++;
812   }
813   while ((c != -1) && (c != '\r') && (c != '\n') && (i < 1024));
814   buf[i] = '\0';
815   
816   if (c == -1)
817     return nil;
818   
819   /* consume CRLF */
820   if (c == '\r') {
821     if ((c = [self readByte]) != '\n') {
822       if (c == -1)
823         return nil;
824       [self logWithFormat:@"WARNING(%s): expected LF after CR, got: '%c'",
825               __PRETTY_FUNCTION__, c];
826       return nil;
827     }
828   }
829   
830   return [[NSString alloc] initWithUTF8String:(char *)buf];
831 }
832
833 - (NSString *)readString {
834   /* Note: returns a retained string */
835   int c1;
836   
837   if ((c1 = [self readByte]) == -1)
838     return nil;
839   
840   if (c1 == '"')
841     return [self readQuoted];
842   if (c1 == '{')
843     return [self readLiteral];
844
845   // Note: this does not return the first char!
846   return [self readStringToCRLF];
847 }
848
849 - (NSString *)readSieveName {
850   return [self readString];
851 }
852
853 /* description */
854
855 - (NSString *)description {
856   NSMutableString *ms;
857
858   ms = [NSMutableString stringWithCapacity:128];
859   [ms appendFormat:@"<0x%p[%@]:", self, NSStringFromClass([self class])];
860   
861   if (self->socket != nil)
862     [ms appendFormat:@" socket=%@", [self socket]];
863   else
864     [ms appendFormat:@" address=%@", self->address];
865
866   [ms appendString:@">"];
867   return ms;
868 }
869
870 @end /* NGSieveClient */