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 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
372 [self logWithFormat:@"ERROR: status line reports: '%@'", script];
376 NSLog(@"str: %@", script);
378 /* read response code */
380 if ((s = [self readStringToCRLF]) == nil) {
381 [self logWithFormat:@"ERROR: could not parse status line."];
384 if ([s length] == 0) { // remainder of previous string
386 if ((s = [self readStringToCRLF]) == nil) {
387 [self logWithFormat:@"ERROR: could not parse status line."];
392 if (![s hasPrefix:@"OK"]) {
393 [self logWithFormat:@"ERROR: status line reports: '%@'", s];
402 - (BOOL)isValidScriptName:(NSString *)_name {
403 return ([_name length] == 0) ? NO : YES;
406 - (NSDictionary *)putScript:(NSString *)_name script:(NSString *)_script {
407 // TODO: script should be send in UTF-8!
411 if (![self isValidScriptName:_name]) {
412 [self logWithFormat:@"%s: missing script-name", __PRETTY_FUNCTION__];
415 if ([_script length] == 0) {
416 [self logWithFormat:@"%s: missing script", __PRETTY_FUNCTION__];
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];
429 - (NSDictionary *)setActiveScript:(NSString *)_name {
432 if (![self isValidScriptName:_name]) {
433 NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__);
436 map = [self processCommand:
437 [NSString stringWithFormat:@"SETACTIVE \"%@\"\r\n", _name]];
438 return [self normalizeResponse:map];
441 - (NSDictionary *)deleteScript:(NSString *)_name {
445 if (![self isValidScriptName:_name]) {
446 NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__);
450 s = [NSString stringWithFormat:@"DELETESCRIPT \"%@\"\r\n", _name];
451 map = [self processCommand:s];
452 return [self normalizeResponse:map];
455 - (NSDictionary *)listScripts {
456 NSMutableDictionary *md;
460 ex = [self sendCommand:@"LISTSCRIPTS" logText:@"LISTSCRIPTS" attempts:3];
462 [self logWithFormat:@"ERROR: could not list scripts: %@", ex];
463 [self setLastException:ex];
469 md = [NSMutableDictionary dictionaryWithCapacity:16];
470 while ((line = [self readStringToCRLF]) != nil) {
471 if ([line hasPrefix:@"OK"])
474 if ([line hasPrefix:@"NO"]) {
479 if ([line hasPrefix:@"{"]) {
480 [self logWithFormat:@"unsupported list response line: '%@'", line];
482 else if ([line hasPrefix:@"\""]) {
487 s = [line substringFromIndex:1];
488 r = [s rangeOfString:@"\""];
491 [self logWithFormat:@"missing closing quote in line: '%@'", line];
492 [line release]; line = nil;
496 s = [s substringToIndex:r.location];
497 isActive = [line rangeOfString:@"ACTIVE"].length == 0 ? NO : YES;
499 [md setObject:isActive ? @"ACTIVE" : @"" forKey:s];
502 [self logWithFormat:@"unexpected list response line (%d): '%@'",
503 [line length], line];
506 [line release]; line = nil;
509 [line release]; line = nil;
515 - (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map {
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
524 id keys[3], values[3];
525 NSParameterAssert(_map != nil);
527 keys[0] = @"RawResponse"; values[0] = _map;
529 values[1] = [[_map objectForKey:@"ok"] boolValue] ? YesNumber : NoNumber;
530 return [NSMutableDictionary dictionaryWithObjects:values
531 forKeys:keys count:2];
534 - (NSDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map {
535 /* filter for open connection */
536 NSMutableDictionary *result;
539 result = [self normalizeResponse:_map];
541 if (![[[_map objectEnumeratorForKey:@"ok"] nextObject] boolValue])
544 if ((tmp = [_map objectForKey:@"implementation"]))
545 [result setObject:tmp forKey:@"server"];
546 if ((tmp = [_map objectForKey:@"sieve"]))
547 [result setObject:tmp forKey:@"capabilities"];
551 /* Private Methods */
553 - (BOOL)handleProcessException:(NSException *)_exception
554 repetitionCount:(int)_cnt
561 if ([_exception isKindOfClass:[NGIOException class]]) {
563 @"WARNING: got exception try to restore connection: %@",
567 if ([_exception isKindOfClass:[NGImap4ParserException class]]) {
569 @"WARNING: Got Parser-Exception try to restore connection: %@",
578 - (void)waitPriorReconnectWithRepetitionCount:(int)_cnt {
582 [self logWithFormat:@"reconnect to %@, sleeping %d seconds ...",
583 self->address, timeout];
585 [self logWithFormat:@"reconnect ..."];
588 - (NGHashMap *)processCommand:(id)_command logText:(id)_txt {
589 NGHashMap *map = nil;
590 BOOL repeatCommand = NO;
595 if (ProfileImapEnabled) {
596 gettimeofday(&tv, NULL);
597 ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
598 fprintf(stderr, "{");
600 do { /* TODO: shouldn't that be a while loop? */
603 [self waitPriorReconnectWithRepetitionCount:repeatCnt];
613 if ((ex = [self sendCommand:_command logText:_txt]) != nil) {
614 repeatCommand = [self handleProcessException:ex
615 repetitionCount:repeatCnt];
618 map = [self->parser parseSieveResponse];
621 repeatCommand = [self handleProcessException:localException
622 repetitionCount:repeatCnt];
626 while (repeatCommand);
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);
638 - (NGHashMap *)processCommand:(id)_command {
639 return [self processCommand:_command logText:_command];
642 - (NSException *)sendCommand:(id)_command logText:(id)_txt {
643 NSString *command = nil;
645 if ((command = _command) == nil) /* missing command */
646 return nil; // TODO: return exception?
651 if ([_txt isKindOfClass:[NSData class]]) {
652 fprintf(stderr, "C: ");
653 fwrite([_txt bytes], [_txt length], 1, stderr);
657 fprintf(stderr, "C: %s\n", [_txt cString]);
662 if (![_command isKindOfClass:[NSData class]])
663 _command = [command dataUsingEncoding:NSUTF8StringEncoding];
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];
675 - (NSException *)sendCommand:(id)_command {
676 return [self sendCommand:_command logText:_command];
679 - (NSException *)sendCommand:(id)_command logText:(id)_txt attempts:(int)_c {
684 for (tryAgain = YES, repeatCnt = 0, ex = nil; tryAgain; repeatCnt++) {
686 if (repeatCnt > 1) /* one repeat goes without delay */
687 [self waitPriorReconnectWithRepetitionCount:repeatCnt];
693 ex = [self sendCommand:_command logText:_txt];
695 ex = [localException retain];
698 if (ex == nil) /* everything is fine */
701 if (repeatCnt > _c) /* reached max attempts */
704 /* try again for certain exceptions */
705 tryAgain = [self handleProcessException:ex repetitionCount:repeatCnt];
716 if (![self->io readBytes:&c count:1]) {
717 [self setLastException:[self->io lastException]];
723 - (NSString *)readLiteral {
725 Assumes 1st char is consumed, returns a retained string.
727 Parses: "{" number [ "+" ] "}" CRLF *OCTET
729 unsigned char countBuf[16];
732 unsigned char *octets;
736 for (i = 0; i < 14; i++) {
739 if ((c = [self readByte]) == -1)
747 byteCount = i > 0 ? atoi(countBuf) : 0;
753 if (i == '\r' && i != -1)
764 octets = malloc(byteCount + 4);
765 if (![self->io safeReadBytes:octets count:byteCount]) {
766 [self setLastException:[self->io lastException]];
769 octets[byteCount] = '\0';
771 return [[NSString alloc] initWithUTF8String:octets];
774 - (NSString *)readQuoted {
776 assumes 1st char is consumed, returns a retained string
778 Note: quoted strings are limited to 1KB!
780 unsigned char buf[1032];
789 while ((c != -1) && (c != '"'));
795 return [[NSString alloc] initWithUTF8String:buf];
798 - (NSString *)readStringToCRLF {
799 unsigned char buf[1032];
805 if (c == '\n' || c == '\r')
811 while ((c != -1) && (c != '\r') && (c != '\n') && (i < 1024));
819 if ((c = [self readByte]) != '\n') {
822 [self logWithFormat:@"WARNING(%s): expected LF after CR, got: '%c'",
823 __PRETTY_FUNCTION__, c];
828 return [[NSString alloc] initWithUTF8String:buf];
831 - (NSString *)readString {
832 /* Note: returns a retained string */
835 if ((c1 = [self readByte]) == -1)
839 return [self readQuoted];
841 return [self readLiteral];
843 // Note: this does not return the first char!
844 return [self readStringToCRLF];
847 - (NSString *)readSieveName {
848 return [self readString];
853 - (NSString *)description {
856 ms = [NSMutableString stringWithCapacity:128];
857 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
859 if (self->socket != nil)
860 [ms appendFormat:@" socket=%@", [self socket]];
862 [ms appendFormat:@" address=%@", self->address];
864 [ms appendString:@">"];
868 @end /* NGSieveClient */