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