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