@interface NGSieveClient(Private)
-- (NGHashMap *)processCommand:(NSString *)_command;
-- (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt;
+- (NGHashMap *)processCommand:(id)_command;
+- (NGHashMap *)processCommand:(id)_command logText:(id)_txt;
-- (void)sendCommand:(NSString *)_command;
-- (void)sendCommand:(NSString *)_command logText:(NSString *)_txt;
+- (NSException *)sendCommand:(id)_command;
+- (NSException *)sendCommand:(id)_command logText:(id)_txt;
+- (NSException *)sendCommand:(id)_command logText:(id)_txt attempts:(int)_c;
- (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map;
- (NSMutableDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map;
- (NSDictionary *)login;
+/* parsing */
+
+- (NSString *)readStringToCRLF;
+- (NSString *)readString;
+
@end
/*
NoNumber = [[NSNumber numberWithBool:NO] retain];
}
++ (id)clientWithURL:(id)_url {
+ return [[[self alloc] initWithURL:_url] autorelease];
+}
+
+ (id)clientWithAddress:(id<NGSocketAddress>)_address {
- return
- [[(NGSieveClient *)[self alloc] initWithAddress:_address] autorelease];
+ NGSieveClient *client;
+
+ client = [self alloc];
+ return [[client initWithAddress:_address] autorelease];
}
+ (id)clientWithHost:(id)_host {
return [[[self alloc] initWithHost:_host] autorelease];
}
+- (id)initWithNSURL:(NSURL *)_url {
+ NGInternetSocketAddress *a;
+ int port;
+
+ if ((port = [[_url port] intValue]) == 0)
+ port = defaultSievePort;
+
+ a = [NGInternetSocketAddress addressWithPort:port
+ onHost:[_url host]];
+ if ((self = [self initWithAddress:a])) {
+ self->login = [[_url user] copy];
+ self->password = [[_url password] copy];
+ }
+ return self;
+}
+- (id)initWithURL:(id)_url {
+ if (_url == nil) {
+ [self release];
+ return nil;
+ }
+
+ if (![_url isKindOfClass:[NSURL class]])
+ _url = [NSURL URLWithString:[_url stringValue]];
+
+ return [self initWithNSURL:_url];
+}
+
- (id)initWithHost:(id)_host {
NGInternetSocketAddress *a;
- (id)initWithAddress:(id<NGSocketAddress>)_address { // di
if ((self = [super init])) {
self->address = [_address retain];
- self->debug = debugImap4;
+ self->debug = debugImap4;
}
return self;
}
- (void)dealloc {
- [self->text release];
+ [self->lastException release];
[self->address release];
+ [self->io release];
[self->socket release];
[self->parser release];
[self->login release];
return self->address;
}
+/* exceptions */
+
+- (void)setLastException:(NSException *)_ex {
+ ASSIGN(self->lastException, _ex);
+}
+- (NSException *)lastException {
+ return self->lastException;
+}
+- (void)resetLastException {
+ [self->lastException release];
+ self->lastException = nil;
+}
+
/* connection */
-/* Opens a connection to given Host. */
+- (void)resetStreams {
+ [self->socket release]; self->socket = nil;
+ [self->io release]; self->io = nil;
+ [self->parser release]; self->parser = nil;
+}
- (NSDictionary *)openConnection {
struct timeval tv;
gettimeofday(&tv, NULL);
ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
}
-
- [self->socket release]; self->socket = nil;
- [self->parser release]; self->parser = nil;
- [self->text release]; self->text = nil;
+
+ [self resetStreams];
self->socket =
[[NGActiveSocket socketConnectedToAddress:self->address] retain];
- self->text =
- [(NGCTextStream *)[NGCTextStream alloc] initWithSource:self->socket];
+ if (self->socket == nil) {
+ [self logWithFormat:@"ERROR: could not connect: %@", self->address];
+ return nil;
+ }
+
+ self->io = [[NGBufferedStream alloc] initWithSource:self->socket];
self->parser = [[NGImap4ResponseParser alloc] initWithStream:self->socket];
/* receive greeting from server without tag-id */
[self->parser parseSieveResponse]];
}
-/*"
-** Check whether stream is already open (could be closed because server-timeout)
-"*/
-
- (NSNumber *)isConnected {
+ // TODO: why does this return a number?!
+ /*
+ Check whether stream is already open (could be closed because
+ server-timeout
+ */
return [NSNumber numberWithBool:
(self->socket == nil)
? NO : [(NGActiveSocket *)self->socket isAlive]];
}
-/*"
-** Close a consisting connection.
-"*/
-
- (void)closeConnection {
[self->socket close];
[self->socket release]; self->socket = nil;
- [self->text release]; self->text = nil;
[self->parser release]; self->parser = nil;
}
-/* login with plaintext password authenticating */
-
- (NSDictionary *)login:(NSString *)_login password:(NSString *)_passwd {
+ /* login with plaintext password authenticating */
+
if ((_login == nil) || (_passwd == nil))
return nil;
NSData *auth;
char *buf;
int bufLen, logLen;
-
-
+
+ if (![self->socket isConnected]) {
+ id con;
+
+ if ((con = [self openConnection]) == nil)
+ return nil;
+ if (![[con objectForKey:@"result"] boolValue])
+ return con;
+ }
+
logLen = [self->login cStringLength];
bufLen = (logLen * 2) + [self->password cStringLength] +2;
- buf = calloc(sizeof(char), bufLen);
-
+ buf = calloc(bufLen + 2, sizeof(char));
+
sprintf(buf, "%s %s %s",
[self->login cString], [self->login cString],
[self->password cString]);
if (LOG_PASSWORD == 1) {
NSString *s;
-
+
s = [NSString stringWithFormat:@"AUTHENTICATE \"PLAIN\" {%d+}\r\n%s",
[auth length], [auth bytes]];
map = [self processCommand:s];
map = [self processCommand:s
logText:@"AUTHENTICATE \"PLAIN\" {%d+}\r\nLOGIN:PASSWORD\r\n"];
}
- return [self normalizeResponse:map];
+ return map ? [self normalizeResponse:map] : nil;
}
/* logout from the connected host and close the connection */
return [self normalizeResponse:map];
}
-- (NSDictionary *)getScript:(NSString *)_scriptName {
- // TODO: implement
- [self notImplemented:_cmd];
- return nil;
+- (NSString *)getScript:(NSString *)_scriptName {
+ NSException *ex;
+ NSString *script, *s;
+
+ s = [@"GETSCRIPT \"" stringByAppendingString:_scriptName];
+ s = [s stringByAppendingString:@"\""];
+ ex = [self sendCommand:s logText:s attempts:3];
+ if (ex != nil) {
+ [self logWithFormat:@"ERROR: could not get script: %@", ex];
+ [self setLastException:ex];
+ return nil;
+ }
+
+ /* read script string */
+
+ if ((script = [[self readString] autorelease]) == nil)
+ return nil;
+
+ /* read response code */
+
+ if ((s = [self readStringToCRLF]) == nil) {
+ [self logWithFormat:@"ERROR: could not parse status line."];
+ return nil;
+ }
+ if ([s length] == 0) { // remainder of previous string
+ [s release];
+ if ((s = [self readStringToCRLF]) == nil) {
+ [self logWithFormat:@"ERROR: could not parse status line."];
+ return nil;
+ }
+ }
+
+ if (![s hasPrefix:@"OK"]) {
+ [self logWithFormat:@"ERROR: status line reports: '%@'", s];
+ [s release];
+ return nil;
+ }
+ [s release];
+
+ return script;
}
- (BOOL)isValidScriptName:(NSString *)_name {
NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__);
return nil;
}
-
+
s = [NSString stringWithFormat:@"DELETESCRIPT \"%@\"\r\n", _name];
map = [self processCommand:s];
return [self normalizeResponse:map];
}
-- (NSDictionary *)listScript:(NSString *)_script {
- [self notImplemented:_cmd];
- return nil;
+
+- (NSDictionary *)listScripts {
+ NSMutableDictionary *md;
+ NSException *ex;
+ NSString *line;
+
+ ex = [self sendCommand:@"LISTSCRIPTS" logText:@"LISTSCRIPTS" attempts:3];
+ if (ex != nil) {
+ [self logWithFormat:@"ERROR: could not list scripts: %@", ex];
+ [self setLastException:ex];
+ return nil;
+ }
+
+ /* read response */
+
+ md = [NSMutableDictionary dictionaryWithCapacity:16];
+ while ((line = [self readStringToCRLF]) != nil) {
+ if ([line hasPrefix:@"OK"])
+ break;
+
+ if ([line hasPrefix:@"NO"]) {
+ md = nil;
+ break;
+ }
+
+ if ([line hasPrefix:@"{"]) {
+ [self logWithFormat:@"unsupported list response line: '%@'", line];
+ }
+ else if ([line hasPrefix:@"\""]) {
+ NSString *s;
+ NSRange r;
+ BOOL isActive;
+
+ s = [line substringFromIndex:1];
+ r = [s rangeOfString:@"\""];
+
+ if (r.length == 0) {
+ [self logWithFormat:@"missing closing quote in line: '%@'", line];
+ [line release]; line = nil;
+ continue;
+ }
+
+ s = [s substringToIndex:r.location];
+ isActive = [line rangeOfString:@"ACTIVE"].length == 0 ? NO : YES;
+
+ [md setObject:isActive ? @"ACTIVE" : @"" forKey:s];
+ }
+ else {
+ [self logWithFormat:@"unexpected list response line (%d): '%@'",
+ [line length], line];
+ }
+
+ [line release]; line = nil;
+ }
+
+ [line release]; line = nil;
+
+ return md;
}
/*
-** Filter for all responses
-** result : NSNumber (response result)
-** exists : NSNumber (number of exists mails in selectet folder
-** recent : NSNumber (number of recent mails in selectet folder
-** expunge : NSArray (message sequence number of expunged mails
- in selectet folder)
+ Filter for all responses
+ result : NSNumber (response result)
+ exists : NSNumber (number of exists mails in selectet folder
+ recent : NSNumber (number of recent mails in selectet folder
+ expunge : NSArray (message sequence number of expunged mails
+ in selectet folder)
*/
- (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map {
forKeys:keys count:2];
}
-/*
-** filter for open connection
-*/
- (NSDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map {
+ /* filter for open connection */
NSMutableDictionary *result;
NSString *tmp;
-
+
result = [self normalizeResponse:_map];
if (![[[_map objectEnumeratorForKey:@"ok"] nextObject] boolValue])
return result;
}
-
-
-/*
-** filter for list
-** list : NSDictionary (folder name as key and flags as value)
-*/
-
-- (NSString *)description {
- return [NSString stringWithFormat:@"<Imap4Client[0x%08X]: socket=%@>",
- (unsigned)self, [self socket]];
-}
-
/* Private Methods */
- (BOOL)handleProcessException:(NSException *)_exception
[self logWithFormat:@"reconnect ..."];
}
-- (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt {
+- (NGHashMap *)processCommand:(id)_command logText:(id)_txt {
NGHashMap *map = nil;
BOOL repeatCommand = NO;
int repeatCnt = 0;
struct timeval tv;
double ti = 0.0;
-
+
if (ProfileImapEnabled) {
gettimeofday(&tv, NULL);
ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
}
NS_DURING {
- [self sendCommand:_command logText:_txt];
- map = [self->parser parseSieveResponse];
+ NSException *ex;
+
+ if ((ex = [self sendCommand:_command logText:_txt]) != nil) {
+ repeatCommand = [self handleProcessException:ex
+ repetitionCount:repeatCnt];
+ }
+ else
+ map = [self->parser parseSieveResponse];
}
NS_HANDLER {
repeatCommand = [self handleProcessException:localException
return map;
}
-- (NGHashMap *)processCommand:(NSString *)_command {
+- (NGHashMap *)processCommand:(id)_command {
return [self processCommand:_command logText:_command];
}
-- (void)sendCommand:(NSString *)_command logText:(NSString *)_txt {
- // TODO: should accept 'NSData' commands
+- (NSException *)sendCommand:(id)_command logText:(id)_txt {
NSString *command = nil;
- command = _command;
+ if ((command = _command) == nil) /* missing command */
+ return nil; // TODO: return exception?
+
+ /* log */
+
+ if (self->debug) {
+ if ([_txt isKindOfClass:[NSData class]]) {
+ fprintf(stderr, "C: ");
+ fwrite([_txt bytes], [_txt length], 1, stderr);
+ fputc('\n', stderr);
+ }
+ else
+ fprintf(stderr, "C: %s\n", [_txt cString]);
+ }
+
+ /* write */
+
+ if (![_command isKindOfClass:[NSData class]])
+ _command = [command dataUsingEncoding:NSUTF8StringEncoding];
+
+ if (![self->io safeWriteData:_command])
+ return [self->io lastException];
+ if (![self->io writeBytes:"\r\n" count:2])
+ return [self->io lastException];
+ if (![self->io flush])
+ return [self->io lastException];
+
+ return nil;
+}
+
+- (NSException *)sendCommand:(id)_command {
+ return [self sendCommand:_command logText:_command];
+}
+
+- (NSException *)sendCommand:(id)_command logText:(id)_txt attempts:(int)_c {
+ NSException *ex;
+ BOOL tryAgain;
+ int repeatCnt;
+
+ for (tryAgain = YES, repeatCnt = 0, ex = nil; tryAgain; repeatCnt++) {
+ if (repeatCnt > 0) {
+ if (repeatCnt > 1) /* one repeat goes without delay */
+ [self waitPriorReconnectWithRepetitionCount:repeatCnt];
+ [self reconnect];
+ tryAgain = NO;
+ }
+
+ NS_DURING
+ ex = [self sendCommand:_command logText:_txt];
+ NS_HANDLER
+ ex = [localException retain];
+ NS_ENDHANDLER;
+
+ if (ex == nil) /* everything is fine */
+ break;
+
+ if (repeatCnt > _c) /* reached max attempts */
+ break;
+
+ /* try again for certain exceptions */
+ tryAgain = [self handleProcessException:ex repetitionCount:repeatCnt];
+ }
+
+ return ex;
+}
+
+/* low level */
+
+- (int)readByte {
+ unsigned char c;
+
+ if (![self->io readBytes:&c count:1]) {
+ [self setLastException:[self->io lastException]];
+ return -1;
+ }
+ return c;
+}
+
+- (NSString *)readLiteral {
+ /*
+ Assumes 1st char is consumed, returns a retained string.
+
+ Parses: "{" number [ "+" ] "}" CRLF *OCTET
+ */
+ unsigned char countBuf[16];
+ int i;
+ unsigned byteCount;
+ unsigned char *octets;
+
+ /* read count */
- if (self->debug)
- fprintf(stderr, "C: %s\n", [_txt cString]);
+ for (i = 0; i < 14; i++) {
+ int c;
+
+ if ((c = [self readByte]) == -1)
+ return nil;
+ if (c == '}')
+ break;
+
+ countBuf[i] = c;
+ }
+ countBuf[i] = '\0';
+ byteCount = i > 0 ? atoi(countBuf) : 0;
+
+ /* read CRLF */
+
+ i = [self readByte];
+ if (i != '\n') {
+ if (i == '\r' && i != -1)
+ i = [self readByte];
+ if (i == -1)
+ return nil;
+ }
+
+ /* read octet */
+
+ if (byteCount == 0)
+ return @"";
+
+ octets = malloc(byteCount + 4);
+ if (![self->io safeReadBytes:octets count:byteCount]) {
+ [self setLastException:[self->io lastException]];
+ return nil;
+ }
+ octets[byteCount] = '\0';
- [self->text writeString:command];
- [self->text writeString:@"\r\n"];
- [self->text flush];
+ return [[NSString alloc] initWithUTF8String:octets];
}
-- (void)sendCommand:(NSString *)_command {
- [self sendCommand:_command logText:_command];
+- (NSString *)readQuoted {
+ /*
+ assumes 1st char is consumed, returns a retained string
+
+ Note: quoted strings are limited to 1KB!
+ */
+ unsigned char buf[1032];
+ int i, c;
+
+ i = 0;
+ do {
+ c = [self readByte];
+ buf[i] = c;
+ i++;
+ }
+ while ((c != -1) && (c != '"'));
+ buf[i] = '\0';
+
+ if (c == -1)
+ return nil;
+
+ return [[NSString alloc] initWithUTF8String:buf];
+}
+
+- (NSString *)readStringToCRLF {
+ unsigned char buf[1032];
+ int i, c;
+
+ i = 0;
+ do {
+ c = [self readByte];
+ if (c == '\n' || c == '\r')
+ break;
+
+ buf[i] = c;
+ i++;
+ }
+ while ((c != -1) && (c != '\r') && (c != '\n') && (i < 1024));
+ buf[i] = '\0';
+
+ if (c == -1)
+ return nil;
+
+ /* consume CRLF */
+ if (c == '\r') {
+ if ((c = [self readByte]) != '\n') {
+ if (c == -1)
+ return nil;
+ [self logWithFormat:@"WARNING(%s): expected LF after CR, got: '%c'",
+ __PRETTY_FUNCTION__, c];
+ return nil;
+ }
+ }
+
+ return [[NSString alloc] initWithUTF8String:buf];
+}
+
+- (NSString *)readString {
+ /* Note: returns a retained string */
+ int c1;
+
+ if ((c1 = [self readByte]) == -1)
+ return nil;
+
+ if (c1 == '"')
+ return [self readQuoted];
+ if (c1 == '{')
+ return [self readLiteral];
+
+ return [self readStringToCRLF];
+}
+
+- (NSString *)readSieveName {
+ return [self readString];
+}
+
+/* description */
+
+- (NSString *)description {
+ NSMutableString *ms;
+
+ ms = [NSMutableString stringWithCapacity:128];
+ [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
+
+ if (self->socket != nil)
+ [ms appendFormat:@" socket=%@", [self socket]];
+ else
+ [ms appendFormat:@" address=%@", self->address];
+
+ [ms appendString:@">"];
+ return ms;
}
@end /* NGSieveClient */
--- /dev/null
+/*
+ Copyright (C) 2004 Helge Hess
+
+ This file is part of OpenGroupware.org.
+
+ OGo is free software; you can redistribute it and/or modify it under
+ the terms of the GNU Lesser General Public License as published by the
+ Free Software Foundation; either version 2, or (at your option) any
+ later version.
+
+ OGo is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
+ License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with OGo; see the file COPYING. If not, write to the
+ Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
+ 02111-1307, USA.
+*/
+
+#include <NGImap4/NGSieveClient.h>
+#include "common.h"
+
+static NSURL *getDefaultsURL(void) {
+ NSUserDefaults *ud;
+ NSString *pwd, *user, *host;
+ NSString *url;
+
+ ud = [NSUserDefaults standardUserDefaults];
+
+ if ((url = [ud stringForKey:@"url"]) != nil)
+ return [NSURL URLWithString:url];
+
+ user = [ud stringForKey:@"user"];
+ pwd = [ud stringForKey:@"password"];
+ host = [ud stringForKey:@"host"];
+
+ url = [@"http://" stringByAppendingString:user];
+ url = [url stringByAppendingString:@":"];
+ url = [url stringByAppendingString:pwd];
+ url = [url stringByAppendingString:@"@"];
+ url = [url stringByAppendingString:host];
+ url = [url stringByAppendingString:@":2000/"];
+ return [NSURL URLWithString:url];
+}
+
+static int test(NSArray *args) {
+ NSUserDefaults *ud;
+ NGSieveClient *client;
+ NSURL *url;
+ id res;
+
+ ud = [NSUserDefaults standardUserDefaults];
+
+ url = getDefaultsURL();
+ NSLog(@"check URL: %@", url);
+
+ client = [[NGSieveClient alloc] initWithURL:url];
+ NSLog(@" client: %@", client);
+
+ if ((res = [client login:[url user] password:[url password]]) == nil) {
+ NSLog(@"could not login %@: %@", [url user], client);
+ return 1;
+ }
+
+ NSLog(@" login %@: %@", [url user], res);
+ NSLog(@" client: %@", client);
+
+ res = [client listScripts];
+ NSLog(@" list: %@", res);
+
+ res = [client getScript:@"ogo"];
+ NSLog(@" get 'ogo': %@", res);
+
+ return 0;
+}
+
+int main(int argc, char **argv, char **env) {
+ NSAutoreleasePool *pool;
+ int res;
+
+ pool = [NSAutoreleasePool new];
+#if LIB_FOUNDATION_LIBRARY
+ [NSProcessInfo initializeWithArguments:argv count:argc environment:env];
+#endif
+
+ res = test([[NSProcessInfo processInfo] argumentsWithoutDefaults]);
+
+ [pool release];
+ exit(0);
+ /* static linking */
+ [NGExtensions class];
+ return 0;
+}