]> err.no Git - sope/blob - sope-mime/NGImap4/NGSieveClient.m
minor Sieve cleanups
[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   /*
301     Format:
302       authenticate-id
303       authorize-id
304       password
305   */
306   sprintf(buf, "%s %s %s", 
307           [self->login cString], [self->login cString],
308           [self->password cString]);
309   
310   buf[logLen] = '\0';
311   buf[logLen * 2 + 1] = '\0';
312   
313   auth = [NSData dataWithBytesNoCopy:buf length:bufLen];
314   auth = [auth dataByEncodingBase64];
315   
316   if (LOG_PASSWORD) {
317     NSString *s;
318     
319     s = [NSString stringWithFormat:@"AUTHENTICATE \"PLAIN\" {%d+}\r\n%s",
320                   [auth length], [auth bytes]];
321     map = [self processCommand:s];
322   }
323   else {
324     NSString *s;
325     
326     s = [NSString stringWithFormat:@"AUTHENTICATE \"PLAIN\" {%d+}\r\n%s",
327                   [auth length], [auth bytes]];
328     map = [self processCommand:s
329                 logText:@"AUTHENTICATE \"PLAIN\" {%d+}\r\nLOGIN:PASSWORD\r\n"];
330   }
331
332   if (map == nil) {
333     [self logWithFormat:@"ERROR: got no result from command."];
334     return nil;
335   }
336   
337   return [self normalizeResponse:map];
338 }
339
340 /* logout from the connected host and close the connection */
341
342 - (NSDictionary *)logout {
343   NGHashMap *map;
344
345   map = [self processCommand:@"logout"]; // TODO: check for success!
346   [self closeConnection];
347   return [self normalizeResponse:map];
348 }
349
350 - (NSString *)getScript:(NSString *)_scriptName {
351   NSException *ex;
352   NSString *script, *s;
353   
354   s = [@"GETSCRIPT \"" stringByAppendingString:_scriptName];
355   s = [s stringByAppendingString:@"\""];
356   ex = [self sendCommand:s logText:s attempts:3];
357   if (ex != nil) {
358     [self logWithFormat:@"ERROR: could not get script: %@", ex];
359     [self setLastException:ex];
360     return nil;
361   }
362   
363   /* read script string */
364   
365   if ((script = [[self readString] autorelease]) == nil)
366     return nil;
367   
368   /* read response code */
369   
370   if ((s = [self readStringToCRLF]) == nil) {
371     [self logWithFormat:@"ERROR: could not parse status line."];
372     return nil;
373   }
374   if ([s length] == 0) { // remainder of previous string
375     [s release];
376     if ((s = [self readStringToCRLF]) == nil) {
377       [self logWithFormat:@"ERROR: could not parse status line."];
378       return nil;
379     }
380   }
381   
382   if (![s hasPrefix:@"OK"]) {
383     [self logWithFormat:@"ERROR: status line reports: '%@'", s];
384     [s release];
385     return nil;
386   }
387   [s release];
388   
389   return script;
390 }
391
392 - (BOOL)isValidScriptName:(NSString *)_name {
393   return ([_name length] == 0) ? NO : YES;
394 }
395
396 - (NSDictionary *)putScript:(NSString *)_name script:(NSString *)_script {
397   // TODO: script should be send in UTF-8!
398   NGHashMap *map;
399   NSString  *s;
400   
401   if (![self isValidScriptName:_name]) {
402     [self logWithFormat:@"%s: missing script-name", __PRETTY_FUNCTION__];
403     return nil;
404   }
405   if ([_script length] == 0) {
406     [self logWithFormat:@"%s: missing script", __PRETTY_FUNCTION__];
407     return nil;
408   }
409   
410   s = @"PUTSCRIPT \"";
411   s = [s stringByAppendingString:_name];
412   s = [s stringByAppendingString:@"\" {"];
413   s = [s stringByAppendingFormat:@"{%d+}\r\n%@", [_script length], _script];
414   s = [s stringByAppendingString:@"\r\n"];
415   map = [self processCommand:s];
416   return [self normalizeResponse:map];
417 }
418
419 - (NSDictionary *)setActiveScript:(NSString *)_name {
420   NGHashMap *map;
421   
422   if (![self isValidScriptName:_name]) {
423     NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__);
424     return nil;
425   }
426   map = [self processCommand:
427               [NSString stringWithFormat:@"SETACTIVE \"%@\"\r\n", _name]];
428   return [self normalizeResponse:map];
429 }
430
431 - (NSDictionary *)deleteScript:(NSString *)_name {
432   NGHashMap *map;
433   NSString  *s;
434
435   if (![self isValidScriptName:_name]) {
436     NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__);
437     return nil;
438   }
439   
440   s = [NSString stringWithFormat:@"DELETESCRIPT \"%@\"\r\n", _name];
441   map = [self processCommand:s];
442   return [self normalizeResponse:map];
443 }
444
445 - (NSDictionary *)listScripts {
446   NSMutableDictionary *md;
447   NSException *ex;
448   NSString *line;
449   
450   ex = [self sendCommand:@"LISTSCRIPTS" logText:@"LISTSCRIPTS" attempts:3];
451   if (ex != nil) {
452     [self logWithFormat:@"ERROR: could not list scripts: %@", ex];
453     [self setLastException:ex];
454     return nil;
455   }
456   
457   /* read response */
458   
459   md = [NSMutableDictionary dictionaryWithCapacity:16];
460   while ((line = [self readStringToCRLF]) != nil) {
461     if ([line hasPrefix:@"OK"])
462       break;
463     
464     if ([line hasPrefix:@"NO"]) {
465       md = nil;
466       break;
467     }
468     
469     if ([line hasPrefix:@"{"]) {
470       [self logWithFormat:@"unsupported list response line: '%@'", line];
471     }
472     else if ([line hasPrefix:@"\""]) {
473       NSString *s;
474       NSRange  r;
475       BOOL     isActive;
476       
477       s = [line substringFromIndex:1];
478       r = [s rangeOfString:@"\""];
479       
480       if (r.length == 0) {
481         [self logWithFormat:@"missing closing quote in line: '%@'", line];
482         [line release]; line = nil;
483         continue;
484       }
485       
486       s = [s substringToIndex:r.location];
487       isActive = [line rangeOfString:@"ACTIVE"].length == 0 ? NO : YES;
488       
489       [md setObject:isActive ? @"ACTIVE" : @"" forKey:s];
490     }
491     else {
492       [self logWithFormat:@"unexpected list response line (%d): '%@'", 
493             [line length], line];
494     }
495     
496     [line release]; line = nil;
497   }
498   
499   [line release]; line = nil;
500   
501   return md;
502 }
503
504
505 - (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map {
506   /*
507     Filter for all responses
508        result  : NSNumber (response result)
509        exists  : NSNumber (number of exists mails in selectet folder
510        recent  : NSNumber (number of recent mails in selectet folder
511        expunge : NSArray  (message sequence number of expunged mails
512                           in selectet folder)
513   */
514   id keys[3], values[3];
515   NSParameterAssert(_map != nil);
516   
517   keys[0] = @"RawResponse"; values[0] = _map;
518   keys[1] = @"result";
519   values[1] = [[_map objectForKey:@"ok"] boolValue] ? YesNumber : NoNumber;
520   return [NSMutableDictionary dictionaryWithObjects:values
521                               forKeys:keys count:2];
522 }
523
524 - (NSDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map {
525   /* filter for open connection */
526   NSMutableDictionary *result;
527   NSString *tmp;
528   
529   result = [self normalizeResponse:_map];
530   
531   if (![[[_map objectEnumeratorForKey:@"ok"] nextObject] boolValue])
532     return result;
533
534   if ((tmp = [_map objectForKey:@"implementation"]))
535     [result setObject:tmp forKey:@"server"];
536   if ((tmp = [_map objectForKey:@"sieve"]))
537     [result setObject:tmp forKey:@"capabilities"];
538   return result;
539 }
540
541 /* Private Methods */
542
543 - (BOOL)handleProcessException:(NSException *)_exception
544   repetitionCount:(int)_cnt
545 {
546   if (_cnt > 3) {
547     [_exception raise];
548     return NO;
549   }
550   
551   if ([_exception isKindOfClass:[NGIOException class]]) {
552     [self logWithFormat:
553             @"WARNING: got exception try to restore connection: %@", 
554             _exception];
555     return YES;
556   }
557   if ([_exception isKindOfClass:[NGImap4ParserException class]]) {
558     [self logWithFormat:
559             @"WARNING: Got Parser-Exception try to restore connection: %@",
560             _exception];
561     return YES;
562   }
563   
564   [_exception raise];
565   return NO;
566 }
567
568 - (void)waitPriorReconnectWithRepetitionCount:(int)_cnt {
569   unsigned timeout;
570   
571   timeout = _cnt * 4;
572   [self logWithFormat:@"reconnect to %@, sleeping %d seconds ...",
573           self->address, timeout];
574   sleep(timeout);
575   [self logWithFormat:@"reconnect ..."];
576 }
577
578 - (NGHashMap *)processCommand:(id)_command logText:(id)_txt {
579   NGHashMap *map          = nil;
580   BOOL      repeatCommand = NO;
581   int       repeatCnt     = 0;
582   struct timeval tv;
583   double         ti = 0.0;
584   
585   if (ProfileImapEnabled) {
586     gettimeofday(&tv, NULL);
587     ti =  (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
588     fprintf(stderr, "{");
589   }
590   do { /* TODO: shouldn't that be a while loop? */
591     if (repeatCommand) {
592       if (repeatCnt > 1)
593         [self waitPriorReconnectWithRepetitionCount:repeatCnt];
594       
595       repeatCnt++;
596       [self reconnect];
597       repeatCommand = NO;
598     }
599     
600     NS_DURING {
601       NSException *ex;
602       
603       if ((ex = [self sendCommand:_command logText:_txt]) != nil) {
604         repeatCommand = [self handleProcessException:ex 
605                               repetitionCount:repeatCnt];
606       }
607       else
608         map = [self->parser parseSieveResponse];
609     }
610     NS_HANDLER {
611       repeatCommand = [self handleProcessException:localException
612                             repetitionCount:repeatCnt];
613     }
614     NS_ENDHANDLER;    
615   }
616   while (repeatCommand);
617   
618   if (ProfileImapEnabled) {
619     gettimeofday(&tv, NULL);
620     ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
621     fprintf(stderr, "}[%s] <Send Command> : time needed: %4.4fs\n",
622            __PRETTY_FUNCTION__, ti < 0.0 ? -1.0 : ti);    
623   }
624   
625   return map;
626 }
627
628 - (NGHashMap *)processCommand:(id)_command {
629   return [self processCommand:_command logText:_command];
630 }
631
632 - (NSException *)sendCommand:(id)_command logText:(id)_txt {
633   NSString *command = nil;
634   
635   if ((command = _command) == nil) /* missing command */
636     return nil; // TODO: return exception?
637   
638   /* log */
639
640   if (self->debug) {
641     if ([_txt isKindOfClass:[NSData class]]) {
642       fprintf(stderr, "C: ");
643       fwrite([_txt bytes], [_txt length], 1, stderr);
644       fputc('\n', stderr);
645     }
646     else
647       fprintf(stderr, "C: %s\n", [_txt cString]);
648   }
649
650   /* write */
651   
652   if (![_command isKindOfClass:[NSData class]])
653     _command = [command dataUsingEncoding:NSUTF8StringEncoding];
654   
655   if (![self->io safeWriteData:_command])
656     return [self->io lastException];
657   if (![self->io writeBytes:"\r\n" count:2])
658     return [self->io lastException];
659   if (![self->io flush])
660     return [self->io lastException];
661   
662   return nil;
663 }
664
665 - (NSException *)sendCommand:(id)_command {
666   return [self sendCommand:_command logText:_command];
667 }
668
669 - (NSException *)sendCommand:(id)_command logText:(id)_txt attempts:(int)_c {
670   NSException *ex;
671   BOOL tryAgain;
672   int  repeatCnt;
673   
674   for (tryAgain = YES, repeatCnt = 0, ex = nil; tryAgain; repeatCnt++) {
675     if (repeatCnt > 0) {
676       if (repeatCnt > 1) /* one repeat goes without delay */
677         [self waitPriorReconnectWithRepetitionCount:repeatCnt];
678       [self reconnect];
679       tryAgain = NO;
680     }
681     
682     NS_DURING
683       ex = [self sendCommand:_command logText:_txt];
684     NS_HANDLER
685       ex = [localException retain];
686     NS_ENDHANDLER;
687     
688     if (ex == nil) /* everything is fine */
689       break;
690     
691     if (repeatCnt > _c) /* reached max attempts */
692       break;
693     
694     /* try again for certain exceptions */
695     tryAgain = [self handleProcessException:ex repetitionCount:repeatCnt];
696   }
697   
698   return ex;
699 }
700
701 /* low level */
702
703 - (int)readByte {
704   unsigned char c;
705   
706   if (![self->io readBytes:&c count:1]) {
707     [self setLastException:[self->io lastException]];
708     return -1;
709   }
710   return c;
711 }
712
713 - (NSString *)readLiteral {
714   /* 
715      Assumes 1st char is consumed, returns a retained string.
716      
717      Parses: "{" number [ "+" ] "}" CRLF *OCTET
718   */
719   unsigned char countBuf[16];
720   int      i;
721   unsigned byteCount;
722   unsigned char *octets;
723   
724   /* read count */
725   
726   for (i = 0; i < 14; i++) {
727     int c;
728     
729     if ((c = [self readByte]) == -1)
730       return nil;
731     if (c == '}')
732       break;
733     
734     countBuf[i] = c;
735   }
736   countBuf[i] = '\0';
737   byteCount = i > 0 ? atoi(countBuf) : 0;
738   
739   /* read CRLF */
740   
741   i = [self readByte];
742   if (i != '\n') {
743     if (i == '\r' && i != -1)
744       i = [self readByte];
745     if (i == -1)
746       return nil;
747   }
748   
749   /* read octet */
750   
751   if (byteCount == 0)
752     return @"";
753   
754   octets = malloc(byteCount + 4);
755   if (![self->io safeReadBytes:octets count:byteCount]) {
756     [self setLastException:[self->io lastException]];
757     return nil;
758   }
759   octets[byteCount] = '\0';
760   
761   return [[NSString alloc] initWithUTF8String:octets];
762 }
763
764 - (NSString *)readQuoted {
765   /* 
766      assumes 1st char is consumed, returns a retained string
767
768      Note: quoted strings are limited to 1KB!
769   */
770   unsigned char buf[1032];
771   int i, c;
772   
773   i = 0;
774   do {
775     c      = [self readByte];
776     buf[i] = c;
777     i++;
778   }
779   while ((c != -1) && (c != '"'));
780   buf[i] = '\0';
781   
782   if (c == -1)
783     return nil;
784   
785   return [[NSString alloc] initWithUTF8String:buf];
786 }
787
788 - (NSString *)readStringToCRLF {
789   unsigned char buf[1032];
790   int i, c;
791   
792   i = 0;
793   do {
794     c = [self readByte];
795     if (c == '\n' || c == '\r')
796       break;
797     
798     buf[i] = c;
799     i++;
800   }
801   while ((c != -1) && (c != '\r') && (c != '\n') && (i < 1024));
802   buf[i] = '\0';
803   
804   if (c == -1)
805     return nil;
806   
807   /* consume CRLF */
808   if (c == '\r') {
809     if ((c = [self readByte]) != '\n') {
810       if (c == -1)
811         return nil;
812       [self logWithFormat:@"WARNING(%s): expected LF after CR, got: '%c'",
813               __PRETTY_FUNCTION__, c];
814       return nil;
815     }
816   }
817   
818   return [[NSString alloc] initWithUTF8String:buf];
819 }
820
821 - (NSString *)readString {
822   /* Note: returns a retained string */
823   int c1;
824   
825   if ((c1 = [self readByte]) == -1)
826     return nil;
827   
828   if (c1 == '"')
829     return [self readQuoted];
830   if (c1 == '{')
831     return [self readLiteral];
832   
833   return [self readStringToCRLF];
834 }
835
836 - (NSString *)readSieveName {
837   return [self readString];
838 }
839
840 /* description */
841
842 - (NSString *)description {
843   NSMutableString *ms;
844
845   ms = [NSMutableString stringWithCapacity:128];
846   [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
847   
848   if (self->socket != nil)
849     [ms appendFormat:@" socket=%@", [self socket]];
850   else
851     [ms appendFormat:@" address=%@", self->address];
852
853   [ms appendString:@">"];
854   return ms;
855 }
856
857 @end /* NGSieveClient */