/* Copyright (C) 2000-2005 SKYRIX Software AG This file is part of SOPE. SOPE 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. SOPE 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 SOPE; see the file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include #include "NGSieveClient.h" #include "NGImap4Support.h" #include "NGImap4ResponseParser.h" #include "NSString+Imap4.h" #include "imCommon.h" #include @interface NGSieveClient(Private) - (NGHashMap *)processCommand:(NSString *)_command; - (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt; - (void)sendCommand:(NSString *)_command; - (void)sendCommand:(NSString *)_command logText:(NSString *)_txt; - (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map; - (NSMutableDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map; - (NSDictionary *)login; @end /*" ** An implementation of an Imap4 client ** ** A folder name always looks like an absolute filename (/inbox/doof) ** "*/ @implementation NGSieveClient static int defaultSievePort = 143; static NSNumber *YesNumber = nil; static NSNumber *NoNumber = nil; static BOOL ProfileImapEnabled = NO; static BOOL LOG_PASSWORD = NO; static BOOL debugImap4 = NO; + (void)initialize { static BOOL didInit = NO; NSUserDefaults *ud; if (didInit) return; didInit = YES; ud = [NSUserDefaults standardUserDefaults]; LOG_PASSWORD = [ud boolForKey:@"SieveLogPassword"]; ProfileImapEnabled = [ud boolForKey:@"ProfileImapEnabled"]; debugImap4 = [ud boolForKey:@"ImapDebugEnabled"]; YesNumber = [[NSNumber numberWithBool:YES] retain]; NoNumber = [[NSNumber numberWithBool:NO] retain]; } + (id)clientWithAddress:(id)_address { return [[(NGSieveClient *)[self alloc] initWithAddress:_address] autorelease]; } + (id)clientWithHost:(id)_host { return [[[self alloc] initWithHost:_host] autorelease]; } - (id)initWithHost:(id)_host { NGInternetSocketAddress *a; a = [NGInternetSocketAddress addressWithPort:defaultSievePort onHost:_host]; return [self initWithAddress:a]; } /**" ** designated initializer "**/ - (id)initWithAddress:(id)_address { if ((self = [super init])) { self->address = [_address retain]; self->debug = debugImap4; } return self; } - (void)dealloc { [self->text release]; [self->address release]; [self->socket release]; [self->parser release]; [self->login release]; [self->password release]; [super dealloc]; } /* equality */ - (BOOL)isEqual:(id)_obj { if (_obj == self) return YES; if ([_obj isKindOfClass:[NGSieveClient class]]) return [self isEqualToSieveClient:_obj]; return NO; } - (BOOL)isEqualToSieveClient:(NGSieveClient *)_obj { if (_obj == self) return YES; if (_obj == nil) return NO; return [[_obj address] isEqual:self->address]; } /* accessors */ - (id)socket { return self->socket; } - (id)address { return self->address; } /* connection */ /*" ** Opens a connection to given Host. "*/ - (NSDictionary *)openConnection { struct timeval tv; double ti = 0.0; if (ProfileImapEnabled) { 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->socket = [[NGActiveSocket socketConnectedToAddress:self->address] retain]; self->text = [(NGCTextStream *)[NGCTextStream alloc] initWithSource:self->socket]; self->parser = [[NGImap4ResponseParser alloc] initWithStream:self->socket]; /* receive greeting from server without tag-id */ if (ProfileImapEnabled) { gettimeofday(&tv, NULL); ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti; fprintf(stderr, "[%s] : time needed: %4.4fs\n", __PRETTY_FUNCTION__, ti < 0.0 ? -1.0 : ti); } return [self normalizeOpenConnectionResponse: [self->parser parseSieveResponse]]; } /*" ** Check whether stream is already open (could be closed because server-timeout) "*/ - (NSNumber *)isConnected { 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 { if ((_login == nil) || (_passwd == nil)) return nil; [self->login release]; self->login = nil; [self->password release]; self->password = nil; self->login = [_login copy]; self->password = [_passwd copy]; return [self login]; } - (void)reconnect { [self closeConnection]; [self openConnection]; [self login]; } - (NSDictionary *)login { NGHashMap *map = nil; NSData *auth; char *buf; int bufLen, logLen; logLen = [self->login cStringLength]; bufLen = (logLen*2) + [self->password cStringLength] +2; buf = calloc(sizeof(char), bufLen); sprintf(buf, "%s %s %s", [self->login cString], [self->login cString], [self->password cString]); buf[logLen] = '\0'; buf[logLen*2+1] = '\0'; auth = [NSData dataWithBytesNoCopy:buf length:bufLen]; if ([auth respondsToSelector:@selector(dataByEncodingBase64WithLineLength:)]) auth = [auth dataByEncodingBase64WithLineLength:4096 /* 'unlimited' */]; else auth = [auth dataByEncodingBase64]; /* old API */ if (LOG_PASSWORD == 1) { map = [self processCommand:[NSString stringWithFormat: @"AUTHENTICATE \"PLAIN\" {%d+}\r\n%s", [auth length], [auth bytes]]]; } else { map = [self processCommand:[NSString stringWithFormat: @"AUTHENTICATE \"PLAIN\" {%d+}\r\n%s", [auth length], [auth bytes]] logText:@"AUTHENTICATE \"PLAIN\" {%d+}\r\nLOGIN:PASSWORD\r\n"]; } return [self normalizeResponse:map]; } /* ** logout from the connected host and close the connection */ - (NSDictionary *)logout { NGHashMap *map = [self processCommand:@"logout"]; [self closeConnection]; return [self normalizeResponse:map]; } - (NSDictionary *)getScript:(NSString *)_scriptName { [self notImplemented:_cmd]; return nil; } - (BOOL)isValidScriptName:(NSString *)_name { return ([_name length] == 0) ? NO : YES; } - (NSDictionary *)putScript:(NSString *)_name script:(NSString *)_script { NGHashMap *map; NSString *s; if (![self isValidScriptName:_name]) { [self logWithFormat:@"%s: missing script-name", __PRETTY_FUNCTION__]; return nil; } if ([_script length] == 0) { [self logWithFormat:@"%s: missing script", __PRETTY_FUNCTION__]; return nil; } s = [NSString stringWithFormat: @"PUTSCRIPT \"%@\" {%d+}\r\n%@\r\n", _name, [_script length], _script]; map = [self processCommand:s]; return [self normalizeResponse:map]; } - (NSDictionary *)setActiveScript:(NSString *)_name { NGHashMap *map; if (![self isValidScriptName:_name]) { NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__); return nil; } map = [self processCommand: [NSString stringWithFormat:@"SETACTIVE \"%@\"\r\n", _name]]; return [self normalizeResponse:map]; } - (NSDictionary *)deleteScript:(NSString *)_name { NGHashMap *map; NSString *s; if (![self isValidScriptName:_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; } /* ** 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 { id keys[3], values[3]; NSParameterAssert(_map != nil); keys[0] = @"RawResponse"; values[0] = _map; keys[1] = @"result"; values[1] = [[_map objectForKey:@"ok"] boolValue] ? YesNumber : NoNumber; return [NSMutableDictionary dictionaryWithObjects:values forKeys:keys count:2]; } /* ** filter for open connection */ - (NSDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map { NSMutableDictionary *result; NSString *tmp; result = [self normalizeResponse:_map]; if (![[[_map objectEnumeratorForKey:@"ok"] nextObject] boolValue]) return result; if ((tmp = [_map objectForKey:@"implementation"])) [result setObject:tmp forKey:@"server"]; if ((tmp = [_map objectForKey:@"sieve"])) [result setObject:tmp forKey:@"capabilities"]; return result; } /* ** filter for list ** list : NSDictionary (folder name as key and flags as value) */ - (NSString *)description { return [NSString stringWithFormat:@"", (unsigned)self, [self socket]]; } /* Private Methods */ - (BOOL)handleProcessException:(NSException *)_exception repetitionCount:(int)_cnt { if (_cnt > 3) { [_exception raise]; return NO; } if ([_exception isKindOfClass:[NGIOException class]]) { [self logWithFormat: @"WARNING: got exception try to restore connection: %@", _exception]; return YES; } if ([_exception isKindOfClass:[NGImap4ParserException class]]) { [self logWithFormat: @"WARNING: Got Parser-Exception try to restore connection: %@", _exception]; return YES; } [_exception raise]; return NO; } - (void)waitPriorReconnectWithRepetitionCount:(int)_cnt { unsigned timeout; timeout = _cnt * 4; [self logWithFormat:@"reconnect to %@, sleeping %d seconds ...", self->address, timeout]; sleep(timeout); [self logWithFormat:@"reconnect ..."]; } - (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_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); fprintf(stderr, "{"); } do { /* TODO: shouldn't that be a while loop? */ if (repeatCommand) { if (repeatCnt > 1) [self waitPriorReconnectWithRepetitionCount:repeatCnt]; repeatCnt++; [self reconnect]; repeatCommand = NO; } NS_DURING { [self sendCommand:_command logText:_txt]; map = [self->parser parseSieveResponse]; } NS_HANDLER { repeatCommand = [self handleProcessException:localException repetitionCount:repeatCnt]; } NS_ENDHANDLER; } while (repeatCommand); if (ProfileImapEnabled) { gettimeofday(&tv, NULL); ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti; fprintf(stderr, "}[%s] : time needed: %4.4fs\n", __PRETTY_FUNCTION__, ti < 0.0 ? -1.0 : ti); } return map; } - (NGHashMap *)processCommand:(NSString *)_command { return [self processCommand:_command logText:_command]; } - (void)sendCommand:(NSString *)_command logText:(NSString *)_txt { NSString *command = nil; command = _command; if (self->debug) fprintf(stderr, "C: %s\n", [_txt cString]); [self->text writeString:command]; [self->text writeString:@"\r\n"]; [self->text flush]; } - (void)sendCommand:(NSString *)_command { [self sendCommand:_command logText:_command]; } @end /* NGSieveClient */