2 Copyright (C) 2000-2004 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
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
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.
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
24 #include "NGSieveClient.h"
25 #include "NGImap4Support.h"
26 #include "NGImap4ResponseParser.h"
27 #include "NSString+Imap4.h"
31 @interface NGSieveClient(Private)
33 - (NGHashMap *)processCommand:(id)_command;
34 - (NGHashMap *)processCommand:(id)_command logText:(id)_txt;
36 - (NSException *)sendCommand:(id)_command;
37 - (NSException *)sendCommand:(id)_command logText:(id)_txt;
38 - (NSException *)sendCommand:(id)_command logText:(id)_txt attempts:(int)_c;
40 - (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map;
41 - (NSMutableDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map;
42 - (NSDictionary *)login;
46 - (NSString *)readStringToCRLF;
47 - (NSString *)readString;
52 An implementation of an Imap4 client
54 A folder name always looks like an absolute filename (/inbox/blah)
56 NOTE: Sieve is just the filtering language ...
59 http://asg.web.cmu.edu/rfc/rfc2244.html
62 "IMPLEMENTATION" "Cyrus timsieved v2.1.15-IPv6-Debian-2.1.15-0woody.1.0"
64 "SIEVE" "fileinto reject envelope vacation imapflags notify subaddress relational regex"
70 @implementation NGSieveClient
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;
80 static BOOL didInit = NO;
85 ud = [NSUserDefaults standardUserDefaults];
86 LOG_PASSWORD = [ud boolForKey:@"SieveLogPassword"];
87 ProfileImapEnabled = [ud boolForKey:@"ProfileImapEnabled"];
88 debugImap4 = [ud boolForKey:@"ImapDebugEnabled"];
90 YesNumber = [[NSNumber numberWithBool:YES] retain];
91 NoNumber = [[NSNumber numberWithBool:NO] retain];
94 + (id)clientWithURL:(id)_url {
95 return [[[self alloc] initWithURL:_url] autorelease];
98 + (id)clientWithAddress:(id<NGSocketAddress>)_address {
99 NGSieveClient *client;
101 client = [self alloc];
102 return [[client initWithAddress:_address] autorelease];
105 + (id)clientWithHost:(id)_host {
106 return [[[self alloc] initWithHost:_host] autorelease];
109 - (id)initWithNSURL:(NSURL *)_url {
110 NGInternetSocketAddress *a;
113 if ((port = [[_url port] intValue]) == 0)
114 port = defaultSievePort;
116 a = [NGInternetSocketAddress addressWithPort:port
118 if ((self = [self initWithAddress:a])) {
119 self->login = [[_url user] copy];
120 self->password = [[_url password] copy];
124 - (id)initWithURL:(id)_url {
130 if (![_url isKindOfClass:[NSURL class]])
131 _url = [NSURL URLWithString:[_url stringValue]];
133 return [self initWithNSURL:_url];
136 - (id)initWithHost:(id)_host {
137 NGInternetSocketAddress *a;
139 a = [NGInternetSocketAddress addressWithPort:defaultSievePort onHost:_host];
140 return [self initWithAddress:a];
143 - (id)initWithAddress:(id<NGSocketAddress>)_address { // di
144 if ((self = [super init])) {
145 self->address = [_address retain];
146 self->debug = debugImap4;
152 [self->lastException release];
153 [self->address release];
155 [self->socket release];
156 [self->parser release];
157 [self->login release];
158 [self->password release];
164 - (BOOL)isEqual:(id)_obj {
167 if ([_obj isKindOfClass:[NGSieveClient class]])
168 return [self isEqualToSieveClient:_obj];
172 - (BOOL)isEqualToSieveClient:(NGSieveClient *)_obj {
173 if (_obj == self) return YES;
174 if (_obj == nil) return NO;
175 return [[_obj address] isEqual:self->address];
180 - (id<NGActiveSocket>)socket {
184 - (id<NGSocketAddress>)address {
185 return self->address;
190 - (void)setLastException:(NSException *)_ex {
191 ASSIGN(self->lastException, _ex);
193 - (NSException *)lastException {
194 return self->lastException;
196 - (void)resetLastException {
197 [self->lastException release];
198 self->lastException = nil;
203 - (void)resetStreams {
204 [self->socket release]; self->socket = nil;
205 [self->io release]; self->io = nil;
206 [self->parser release]; self->parser = nil;
209 - (NSDictionary *)openConnection {
213 if (ProfileImapEnabled) {
214 gettimeofday(&tv, NULL);
215 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
221 [[NGActiveSocket socketConnectedToAddress:self->address] retain];
222 if (self->socket == nil) {
223 [self logWithFormat:@"ERROR: could not connect: %@", self->address];
227 self->io = [[NGBufferedStream alloc] initWithSource:self->socket];
228 self->parser = [[NGImap4ResponseParser alloc] initWithStream:self->socket];
230 /* receive greeting from server without tag-id */
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);
238 return [self normalizeOpenConnectionResponse:
239 [self->parser parseSieveResponse]];
242 - (NSNumber *)isConnected {
244 Check whether stream is already open (could be closed due to a server
247 // TODO: why does that return an object?
248 if (self->socket == nil)
249 return [NSNumber numberWithBool:NO];
251 return [NSNumber numberWithBool:[(NGActiveSocket *)self->socket isAlive]];
254 - (void)closeConnection {
255 [self->socket close];
256 [self->socket release]; self->socket = nil;
257 [self->parser release]; self->parser = nil;
260 - (NSDictionary *)login:(NSString *)_login password:(NSString *)_passwd {
261 /* login with plaintext password authenticating */
263 if ((_login == nil) || (_passwd == nil))
266 [self->login release]; self->login = nil;
267 [self->password release]; self->password = nil;
269 self->login = [_login copy];
270 self->password = [_passwd copy];
275 [self closeConnection];
276 [self openConnection];
280 - (NSDictionary *)login {
281 NGHashMap *map = nil;
286 if (![self->socket isConnected]) {
289 if ((con = [self openConnection]) == nil)
291 if (![[con objectForKey:@"result"] boolValue])
295 logLen = [self->login cStringLength];
296 bufLen = (logLen * 2) + [self->password cStringLength] +2;
298 buf = calloc(bufLen + 2, sizeof(char));
306 sprintf(buf, "%s %s %s",
307 [self->login cString], [self->login cString],
308 [self->password cString]);
311 buf[logLen * 2 + 1] = '\0';
313 auth = [NSData dataWithBytesNoCopy:buf length:bufLen];
314 auth = [auth dataByEncodingBase64];
319 s = [NSString stringWithFormat:@"AUTHENTICATE \"PLAIN\" {%d+}\r\n%s",
320 [auth length], [auth bytes]];
321 map = [self processCommand:s];
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"];
333 [self logWithFormat:@"ERROR: got no result from command."];
337 return [self normalizeResponse:map];
340 /* logout from the connected host and close the connection */
342 - (NSDictionary *)logout {
345 map = [self processCommand:@"logout"]; // TODO: check for success!
346 [self closeConnection];
347 return [self normalizeResponse:map];
350 - (NSString *)getScript:(NSString *)_scriptName {
352 NSString *script, *s;
354 s = [@"GETSCRIPT \"" stringByAppendingString:_scriptName];
355 s = [s stringByAppendingString:@"\""];
356 ex = [self sendCommand:s logText:s attempts:3];
358 [self logWithFormat:@"ERROR: could not get script: %@", ex];
359 [self setLastException:ex];
363 /* read script string */
365 if ((script = [[self readString] autorelease]) == nil)
368 /* read response code */
370 if ((s = [self readStringToCRLF]) == nil) {
371 [self logWithFormat:@"ERROR: could not parse status line."];
374 if ([s length] == 0) { // remainder of previous string
376 if ((s = [self readStringToCRLF]) == nil) {
377 [self logWithFormat:@"ERROR: could not parse status line."];
382 if (![s hasPrefix:@"OK"]) {
383 [self logWithFormat:@"ERROR: status line reports: '%@'", s];
392 - (BOOL)isValidScriptName:(NSString *)_name {
393 return ([_name length] == 0) ? NO : YES;
396 - (NSDictionary *)putScript:(NSString *)_name script:(NSString *)_script {
397 // TODO: script should be send in UTF-8!
401 if (![self isValidScriptName:_name]) {
402 [self logWithFormat:@"%s: missing script-name", __PRETTY_FUNCTION__];
405 if ([_script length] == 0) {
406 [self logWithFormat:@"%s: missing script", __PRETTY_FUNCTION__];
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];
419 - (NSDictionary *)setActiveScript:(NSString *)_name {
422 if (![self isValidScriptName:_name]) {
423 NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__);
426 map = [self processCommand:
427 [NSString stringWithFormat:@"SETACTIVE \"%@\"\r\n", _name]];
428 return [self normalizeResponse:map];
431 - (NSDictionary *)deleteScript:(NSString *)_name {
435 if (![self isValidScriptName:_name]) {
436 NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__);
440 s = [NSString stringWithFormat:@"DELETESCRIPT \"%@\"\r\n", _name];
441 map = [self processCommand:s];
442 return [self normalizeResponse:map];
445 - (NSDictionary *)listScripts {
446 NSMutableDictionary *md;
450 ex = [self sendCommand:@"LISTSCRIPTS" logText:@"LISTSCRIPTS" attempts:3];
452 [self logWithFormat:@"ERROR: could not list scripts: %@", ex];
453 [self setLastException:ex];
459 md = [NSMutableDictionary dictionaryWithCapacity:16];
460 while ((line = [self readStringToCRLF]) != nil) {
461 if ([line hasPrefix:@"OK"])
464 if ([line hasPrefix:@"NO"]) {
469 if ([line hasPrefix:@"{"]) {
470 [self logWithFormat:@"unsupported list response line: '%@'", line];
472 else if ([line hasPrefix:@"\""]) {
477 s = [line substringFromIndex:1];
478 r = [s rangeOfString:@"\""];
481 [self logWithFormat:@"missing closing quote in line: '%@'", line];
482 [line release]; line = nil;
486 s = [s substringToIndex:r.location];
487 isActive = [line rangeOfString:@"ACTIVE"].length == 0 ? NO : YES;
489 [md setObject:isActive ? @"ACTIVE" : @"" forKey:s];
492 [self logWithFormat:@"unexpected list response line (%d): '%@'",
493 [line length], line];
496 [line release]; line = nil;
499 [line release]; line = nil;
505 - (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map {
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
514 id keys[3], values[3];
515 NSParameterAssert(_map != nil);
517 keys[0] = @"RawResponse"; values[0] = _map;
519 values[1] = [[_map objectForKey:@"ok"] boolValue] ? YesNumber : NoNumber;
520 return [NSMutableDictionary dictionaryWithObjects:values
521 forKeys:keys count:2];
524 - (NSDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map {
525 /* filter for open connection */
526 NSMutableDictionary *result;
529 result = [self normalizeResponse:_map];
531 if (![[[_map objectEnumeratorForKey:@"ok"] nextObject] boolValue])
534 if ((tmp = [_map objectForKey:@"implementation"]))
535 [result setObject:tmp forKey:@"server"];
536 if ((tmp = [_map objectForKey:@"sieve"]))
537 [result setObject:tmp forKey:@"capabilities"];
541 /* Private Methods */
543 - (BOOL)handleProcessException:(NSException *)_exception
544 repetitionCount:(int)_cnt
551 if ([_exception isKindOfClass:[NGIOException class]]) {
553 @"WARNING: got exception try to restore connection: %@",
557 if ([_exception isKindOfClass:[NGImap4ParserException class]]) {
559 @"WARNING: Got Parser-Exception try to restore connection: %@",
568 - (void)waitPriorReconnectWithRepetitionCount:(int)_cnt {
572 [self logWithFormat:@"reconnect to %@, sleeping %d seconds ...",
573 self->address, timeout];
575 [self logWithFormat:@"reconnect ..."];
578 - (NGHashMap *)processCommand:(id)_command logText:(id)_txt {
579 NGHashMap *map = nil;
580 BOOL repeatCommand = NO;
585 if (ProfileImapEnabled) {
586 gettimeofday(&tv, NULL);
587 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
588 fprintf(stderr, "{");
590 do { /* TODO: shouldn't that be a while loop? */
593 [self waitPriorReconnectWithRepetitionCount:repeatCnt];
603 if ((ex = [self sendCommand:_command logText:_txt]) != nil) {
604 repeatCommand = [self handleProcessException:ex
605 repetitionCount:repeatCnt];
608 map = [self->parser parseSieveResponse];
611 repeatCommand = [self handleProcessException:localException
612 repetitionCount:repeatCnt];
616 while (repeatCommand);
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);
628 - (NGHashMap *)processCommand:(id)_command {
629 return [self processCommand:_command logText:_command];
632 - (NSException *)sendCommand:(id)_command logText:(id)_txt {
633 NSString *command = nil;
635 if ((command = _command) == nil) /* missing command */
636 return nil; // TODO: return exception?
641 if ([_txt isKindOfClass:[NSData class]]) {
642 fprintf(stderr, "C: ");
643 fwrite([_txt bytes], [_txt length], 1, stderr);
647 fprintf(stderr, "C: %s\n", [_txt cString]);
652 if (![_command isKindOfClass:[NSData class]])
653 _command = [command dataUsingEncoding:NSUTF8StringEncoding];
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];
665 - (NSException *)sendCommand:(id)_command {
666 return [self sendCommand:_command logText:_command];
669 - (NSException *)sendCommand:(id)_command logText:(id)_txt attempts:(int)_c {
674 for (tryAgain = YES, repeatCnt = 0, ex = nil; tryAgain; repeatCnt++) {
676 if (repeatCnt > 1) /* one repeat goes without delay */
677 [self waitPriorReconnectWithRepetitionCount:repeatCnt];
683 ex = [self sendCommand:_command logText:_txt];
685 ex = [localException retain];
688 if (ex == nil) /* everything is fine */
691 if (repeatCnt > _c) /* reached max attempts */
694 /* try again for certain exceptions */
695 tryAgain = [self handleProcessException:ex repetitionCount:repeatCnt];
706 if (![self->io readBytes:&c count:1]) {
707 [self setLastException:[self->io lastException]];
713 - (NSString *)readLiteral {
715 Assumes 1st char is consumed, returns a retained string.
717 Parses: "{" number [ "+" ] "}" CRLF *OCTET
719 unsigned char countBuf[16];
722 unsigned char *octets;
726 for (i = 0; i < 14; i++) {
729 if ((c = [self readByte]) == -1)
737 byteCount = i > 0 ? atoi(countBuf) : 0;
743 if (i == '\r' && i != -1)
754 octets = malloc(byteCount + 4);
755 if (![self->io safeReadBytes:octets count:byteCount]) {
756 [self setLastException:[self->io lastException]];
759 octets[byteCount] = '\0';
761 return [[NSString alloc] initWithUTF8String:octets];
764 - (NSString *)readQuoted {
766 assumes 1st char is consumed, returns a retained string
768 Note: quoted strings are limited to 1KB!
770 unsigned char buf[1032];
779 while ((c != -1) && (c != '"'));
785 return [[NSString alloc] initWithUTF8String:buf];
788 - (NSString *)readStringToCRLF {
789 unsigned char buf[1032];
795 if (c == '\n' || c == '\r')
801 while ((c != -1) && (c != '\r') && (c != '\n') && (i < 1024));
809 if ((c = [self readByte]) != '\n') {
812 [self logWithFormat:@"WARNING(%s): expected LF after CR, got: '%c'",
813 __PRETTY_FUNCTION__, c];
818 return [[NSString alloc] initWithUTF8String:buf];
821 - (NSString *)readString {
822 /* Note: returns a retained string */
825 if ((c1 = [self readByte]) == -1)
829 return [self readQuoted];
831 return [self readLiteral];
833 return [self readStringToCRLF];
836 - (NSString *)readSieveName {
837 return [self readString];
842 - (NSString *)description {
845 ms = [NSMutableString stringWithCapacity:128];
846 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
848 if (self->socket != nil)
849 [ms appendFormat:@" socket=%@", [self socket]];
851 [ms appendFormat:@" address=%@", self->address];
853 [ms appendString:@">"];
857 @end /* NGSieveClient */