/* Copyright (c) 1010 Sven Weidauer * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated * documentation files (the "Software"), to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and * to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */#import "SieveClient.h"#import "NSData+Base64Encoding.h"#import "NSScanner+QuotedString.h"#import "SieveOperation.h"#import "SieveClientInternals.h"// TODO: Verschiedene Empfangs-Routinen aufspalten und vereinheltichenNSString*constkSieveURLScheme=@"sieve";NSString*constkSieveErrorDomain=@"SieveErrorDomain";NSString*constkSieveErrorResponseDictKey=@"SieveErrorResponseDictKey";staticconstuint32_tkSieveProtocolType=FOUR_CHAR_CODE('SieV');@interfaceSieveClient()@property(readwrite,copy)NSString*host;@property(readwrite,retain)AsyncSocket*socket;@property(readwrite,retain)SaslConn*sasl;@property(readwrite,copy)NSString*availableMechanisms;@property(readwrite,assign)SieveClientStatusstatus;@property(readwrite,retain)NSMutableArray*operations;@property(readwrite,retain)SieveOperation*currentOperation;-(void)processResponseLine:(NSData*)readDatalist:(NSMutableArray*)receivedResponsesblock:(void(^)(NSDictionary*))completionBlock;-(void)receiveResponse:(void(^)(NSDictionary*))completionBlock;-(void)authWithUser:(NSString*)userNameandPassword:(NSString*)password;@end@implementationSieveClient@synthesizeavailableMechanisms;@synthesizehost,socket;@synthesizestatus;@synthesizesasl;@synthesizestartTLSIfPossible;@synthesizedelegate;@synthesizeuser;@synthesizeoperations;@synthesizecurrentOperation;-(NSMutableArray*)operations;{if(nil==operations){operations=[[NSMutableArrayalloc]init];}returnoperations;}-init;{if(nil==[superinit])returnnil;startTLSIfPossible=YES;returnself;}-(void)dealloc;{[selfsetHost:nil];[socketsetDelegate:nil];[selfsetSocket:nil];[selfsetSasl:nil];[superdealloc];}#pragma mark --(void)receiveCaps:(NSDictionary*)response;{if([[responseobjectForKey:@"responseCode"]isEqualToString:@"OK"]){boolreadyForAuth=YES;for(NSString*linein[responseobjectForKey:@"response"]){NSScanner*scanner=[NSScannerscannerWithString:line];[scannersetCaseSensitive:NO];NSString*item=nil;if([scannerscanQuotedString:&item]){item=[itemlowercaseString];NSString*rest=@"";[scannerscanQuotedString:&rest];if([itemisEqualToString:@"sasl"]){[selfsetAvailableMechanisms:rest];}elseif([itemisEqualToString:@"starttls"]){if(startTLSIfPossible&&!TLSActive){[selfstartTLS];readyForAuth=NO;break;// muss nicht weiter bearbeitet werden, nach TLS handshake kommt sowieso neue caps}}elseif([itemisEqualToString:@"sieve"]){// TODO: in array splitten und speichern als verfügbare erweiterungen speichern}elseif([itemisEqualToString:@"notify"]){// TODO: in array splitten und speichern als verfügbare notify-methoden speichern}}}if(readyForAuth){[selfsetStatus:SieveClientConnected];if([delegaterespondsToSelector:@selector(sieveClientEstablishedConnection:)]){[delegatesieveClientEstablishedConnection:self];}[selfauth];}}else{// TODO: notify delegate}}-(void)connectToURL:(NSURL*)url;{NSAssert([[urlscheme]isEqualToString:kSieveURLScheme],@"Accepting only sieve URLs");unsignedport=kSieveDefaultPort;NSNumber*urlPort=[urlport];if(nil!=urlPort){port=[urlPortunsignedIntValue];}[selfsetUser:[urluser]];[selfconnectToHost:[urlhost]port:port];}-(void)connectToHost:(NSString*)newHostport:(unsigned)port;{NSAssert(status==SieveClientDisconnected,@"Cannot connect if already connected");[[selfmutableArrayValueForKey:@"log"]removeAllObjects];[selfsetHost:newHost];[selfsetSocket:[[[AsyncSocketalloc]initWithDelegate:self]autorelease]];[socketconnectToHost:hostonPort:porterror:NULL];[selfreceiveResponse:^(NSDictionary*response){[selfreceiveCaps:response];}];}-(void)disconnect;{NSAssert(status!=SieveClientDisconnected,@"Already disconnected");[selfsend:@"LOGOUT"];[socketdisconnect];}-(void)startTLS;{[selfsend:@"STARTTLS"completion:^(NSDictionary*info){if([[infoobjectForKey:@"responseCode"]isEqualToString:@"OK"]){[socketstartTLS:nil];TLSActive=true;[selfreceiveResponse:^(NSDictionary*response){[selfreceiveCaps:response];}];}}];}-(void)logLine:(NSString*)linefrom:(NSString*)source;{[[selfmutableArrayValueForKey:@"log"]addObject:[NSDictionarydictionaryWithObjectsAndKeys:line,@"line",source,@"who",nil]];}-(void)sendString:(NSString*)line;{NSData*lineData=[[linestringByAppendingString:@"\r\n"]dataUsingEncoding:NSUTF8StringEncoding];[socketwriteData:lineDatawithTimeout:-1tag:0];}-(void)sendData:(NSData*)data{[socketwriteData:datawithTimeout:-1tag:0];}-(void)send:(NSString*)linecompletion:(void(^)(NSDictionary*))completionBlock;{[selflogLine:linefrom:@"Client"];[selfsendString:line];[selfreceiveResponse:completionBlock];}-(void)send:(NSString*)line;{[selfsend:linecompletion:nil];}-(void)receiveNextLine:(NSMutableArray*)responsesblock:(void(^)(NSDictionary*))completionBlock;{[socketreadDataToData:[AsyncSocketCRLFData]withTimeout:-1tag:0block:^(NSData*received){[selfprocessResponseLine:receivedlist:responsesblock:completionBlock];}];}-(void)readStringFromScanner:(NSScanner*)scannercompletion:(void(^)(NSString*))block;{if([scannerscanString:@"{"intoString:NULL]){intstringLength=0;[scannerscanInt:&stringLength];[socketreadDataToLength:stringLength+2withTimeout:-1tag:0block:^(NSData*readData){NSString*str=[[[NSStringalloc]initWithData:readDataencoding:NSUTF8StringEncoding]autorelease];block(str);}];}else{NSString*result=[[scannerstring]substringFromIndex:[scannerscanLocation]];block(result);}}-(void)receiveString:(void(^)(NSString*))completionBlock;{[socketreadDataToData:[AsyncSocketCRLFData]withTimeout:-1tag:0block:^(NSData*received){NSString*receivedLine=[[[NSStringalloc]initWithData:receivedencoding:NSUTF8StringEncoding]autorelease];NSScanner*scanner=[NSScannerscannerWithString:receivedLine];[scannersetCaseSensitive:NO];[selfreadStringFromScanner:scannercompletion:completionBlock];}];}-(void)processResponseLine:(NSData*)readDatalist:(NSMutableArray*)receivedResponsesblock:(void(^)(NSDictionary*))completionBlock;{NSString*receivedLine=[[[NSStringalloc]initWithData:readDataencoding:NSUTF8StringEncoding]autorelease];NSScanner*scanner=[NSScannerscannerWithString:receivedLine];[scannersetCaseSensitive:NO];NSString*responseString=nil;if([scannerscanString:@"OK"intoString:&responseString]||[scannerscanString:@"NO"intoString:&responseString]||[scannerscanString:@"BYE"intoString:&responseString]){[selflogLine:receivedLinefrom:@"Server"];NSString*extraInfo=nil;if([scannerscanString:@"("intoString:NULL]){[scannerscanUpToString:@")"intoString:&extraInfo];[scannerscanString:@")"intoString:NULL];}[selfreadStringFromScanner:scannercompletion:^(NSString*userInfo){if(completionBlock!=nil){NSMutableDictionary*completeResponse=[NSMutableDictionarydictionary];[completeResponsesetObject:responseStringforKey:@"responseCode"];if(extraInfo)[completeResponsesetObject:extraInfoforKey:@"extraInfo"];if(userInfo)[completeResponsesetObject:userInfoforKey:@"userInfo"];if([receivedResponsescount]>0){[completeResponsesetObject:receivedResponsesforKey:@"response"];}completionBlock(completeResponse);}}];}else{[selfreadStringFromScanner:scannercompletion:^(NSString*line){[receivedResponsesaddObject:line];[selfreceiveNextLine:receivedResponsesblock:(void(^)(NSDictionary*))completionBlock];}];}}-(void)receiveResponse:(void(^)(NSDictionary*))completionBlock;{NSMutableArray*receivedResponses=[NSMutableArrayarray];[selfreceiveNextLine:receivedResponsesblock:completionBlock];}-(void)authStep:(NSString*)receivedString;{NSScanner*scanner=[NSScannerscannerWithString:receivedString];if([scannerscanString:@"OK "intoString:NULL]){[selflogLine:receivedStringfrom:@"Server"];NSData*lastData=nil;if([scannerscanString:@"(SASL"intoString:NULL]){NSString*rest=@"";if([scannerscanQuotedString:&rest])lastData=[restbase64DecodedData];}if(lastData||(status!=SieveClientAuthenticated)){SaslConnStatusrc=[saslfinishWithServerData:lastData];if(rc==SaslConnSuccess){[selfsetStatus:SieveClientAuthenticated];}}if(status==SieveClientAuthenticated){NSAssert(![saslneedsToEncodeData],@"Sieve security layers are not supported.");[selfsetSasl:nil];if([delegaterespondsToSelector:@selector(sieveClientSucceededAuth:)]){[delegatesieveClientSucceededAuth:self];}[selfstartNextOperation];}}elseif([scannerscanString:@"NO "intoString:NULL]){[selflogLine:receivedStringfrom:@"Server"];[selfsetStatus:SieveClientConnected];[delegatesieveClient:selfneedsCredentials:nil];}elseif([scannerscanString:@"BYE "intoString:NULL]){[selflogLine:receivedStringfrom:@"Server"];[socketdisconnect];// TODO: Notify delegate that connection was dropped}else{NSString*rest=@"";if([scannerscanQuotedString:&rest])receivedString=rest;NSData*data=[receivedStringbase64DecodedData];NSData*outData=nil;SaslConnStatusrc=[saslcontinueWithServerData:dataclientOut:&outData];if(rc==SaslConnFailed){// Abort the auth by sending "*"[selfsendString:@"\"*\""];[selfreceiveResponse:nil];[selfdisconnect];// TODO: Notify delegate that connection was dropped}else{if(rc==SaslConnSuccess)[selfsetStatus:SieveClientAuthenticated];NSString*authCommand=@"\"\"";if(nil!=outData)authCommand=[NSStringstringWithFormat:@"\"%@\"",[outDatabase64EncodedString]];[selfsendString:authCommand];[selfreceiveString:^(NSString*receivedString){[selfauthStep:receivedString];}];}}}-(void)continueAuthWithCredentials:(NSURLCredential*)creds;{if([credspersistence]==NSURLCredentialPersistencePermanent){NSData*serverData=[hostdataUsingEncoding:NSUTF8StringEncoding];NSData*userData=[[credsuser]dataUsingEncoding:NSUTF8StringEncoding];NSData*passwordData=[[credspassword]dataUsingEncoding:NSUTF8StringEncoding];SecKeychainAddInternetPassword(NULL,[serverDatalength],[serverDatabytes],0,NULL,[userDatalength],[userDatabytes],0,NULL,[socketconnectedPort],kSieveProtocolType,kSecAuthenticationTypeDefault,[passwordDatalength],[passwordDatabytes],NULL);}[selfauthWithUser:[credsuser]andPassword:[credspassword]];}-(void)cancelAuth;{// TODO: Disconnect socket}-(void)authWithUser:(NSString*)userNameandPassword:(NSString*)password;{NSAssert(status==SieveClientConnected,@"Wrong status");[selfsetStatus:SieveClientAuthenticating];[selfsetSasl:[[[SaslConnalloc]initWithService:kSieveURLSchemeserver:hostsocket:socketflags:SaslConnSuccessData]autorelease]];[selfsetUser:userName];[saslsetAuthName:userName];[saslsetPassword:password];NSData*outData=nil;SaslConnStatusrc=[saslstartWithMechanisms:availableMechanismsclientOut:&outData];if(rc!=SaslConnFailed){if(SaslConnSuccess==rc)[selfsetStatus:SieveClientAuthenticated];NSString*authCommand=[NSStringstringWithFormat:@"AUTHENTICATE \"%@\"",[saslmechanism]];[selflogLine:authCommandfrom:@"Client"];if(outData){NSString*encodedData=[outDatabase64EncodedString];authCommand=[authCommandstringByAppendingFormat:@" \"%@\"",encodedData];}[selfsendString:authCommand];[selfreceiveString:^(NSString*receivedString){[selfauthStep:receivedString];}];}else{[selfdisconnect];// TODO: Notify delegate that connection was dropped}}-(void)auth;{NSString*userName=[selfuser];NSString*password=nil;if(!triedKeychain){NSData*serverName=[hostdataUsingEncoding:NSUTF8StringEncoding];UInt32passwordLength=0;void*passwordData=NULL;SecKeychainItemRefitem=NULL;constchar*userNameString=NULL;unsigneduserNameLength=0;if(nil!=userName){NSData*userNameData=[userNamedataUsingEncoding:NSUTF8StringEncoding];userNameString=[userNameDatabytes];userNameLength=[userNameDatalength];}OSStatusresult=SecKeychainFindInternetPassword(NULL,[serverNamelength],[serverNamebytes],0,NULL,userNameLength,userNameString,0,NULL,[socketconnectedPort],kSieveProtocolType,kSecAuthenticationTypeDefault,&passwordLength,&passwordData,&item);if(errSecSuccess==result){NSData*data=[NSDatadataWithBytesNoCopy:passwordDatalength:passwordLengthfreeWhenDone:NO];password=[[[NSStringalloc]initWithData:dataencoding:NSUTF8StringEncoding]autorelease];if(nil==userName){SecKeychainAttributenameAttribute={kSecAccountItemAttr,0,NULL};SecKeychainAttributeListattrList={1,&nameAttribute};result=SecKeychainItemCopyContent(item,NULL,&attrList,NULL,NULL);if(result==errSecSuccess){data=[NSDatadataWithBytesNoCopy:nameAttribute.datalength:nameAttribute.lengthfreeWhenDone:NO];userName=[[[NSStringalloc]initWithData:dataencoding:NSUTF8StringEncoding]autorelease];SecKeychainItemFreeContent(&attrList,NULL);}}SecKeychainItemFreeContent(NULL,passwordData);}triedKeychain=YES;}if(nil!=password&&nil!=userName){[selfauthWithUser:userNameandPassword:password];}else{NSURLCredential*partialCred=[NSURLCredentialcredentialWithUser:userNamepassword:passwordpersistence:NSURLCredentialPersistenceNone];[delegatesieveClient:selfneedsCredentials:partialCred];}}-(void)startNextOperation;{if([operationscount]>0){[selfsetCurrentOperation:[operationsobjectAtIndex:0]];[operationsremoveObjectAtIndex:0];[currentOperationstart];}else{[selfsetCurrentOperation:nil];}}-(void)addOperation:(SieveOperation*)operation;{[[selfoperations]addObject:operation];if(nil==currentOperation&&status==SieveClientAuthenticated)[selfstartNextOperation];}-(void)putScript:(NSString*)scriptwithName:(NSString*)namedelegate:(id)newDelegateuserInfo:(void*)userInfo;{[selfaddOperation:[[[SievePutScriptOperationalloc]initWithScript:scriptname:nameforClient:selfdelegate:newDelegateuserInfo:userInfo]autorelease]];}-(void)putScript:(NSString*)scriptwithName:(NSString*)name;{[selfputScript:scriptwithName:namedelegate:delegateuserInfo:NULL];}-(void)setActiveScript:(NSString*)scriptNamedelegate:(id)newDelegateuserInfo:(void*)userInfo;{[selfaddOperation:[[[SieveSetActiveOperationalloc]initWithScript:scriptNameforClient:selfdelegate:newDelegateuserInfo:userInfo]autorelease]];}-(void)setActiveScript:(NSString*)scriptName;{[selfsetActiveScript:scriptNamedelegate:delegateuserInfo:NULL];}-(void)renameScript:(NSString*)oldNameto:(NSString*)newNamedelegate:(id)newDelegateuserInfo:(void*)userInfo;{[selfaddOperation:[[[SieveRenameScriptOperationalloc]initWithOldName:oldNamenewName:newNameforClient:selfdelegate:newDelegateuserInfo:userInfo]autorelease]];}-(void)renameScript:(NSString*)oldNameto:(NSString*)newName;{[selfrenameScript:oldNameto:newNamedelegate:delegateuserInfo:NULL];}-(void)deleteScript:(NSString*)scriptNamedelegate:(id)newDelegateuserInfo:(void*)userInfo;{[selfaddOperation:[[[SieveDeleteScriptOperationalloc]initWithScript:scriptNameforClient:selfdelegate:newDelegateuserInfo:userInfo]autorelease]];}-(void)deleteScript:(NSString*)scriptName;{[selfdeleteScript:scriptNamedelegate:delegateuserInfo:NULL];}-(void)listScriptsWithDelegate:(id)newDelegateuserInfo:(void*)userInfo;{[selfaddOperation:[[[SieveListScriptsOperationalloc]initForClient:selfdelegate:newDelegateuserInfo:userInfo]autorelease]];}-(void)listScripts;{[selflistScriptsWithDelegate:delegateuserInfo:NULL];}-(void)getScript:(NSString*)scriptNamedelegate:(id)newDelegateuserInfo:(void*)userInfo;{[selfaddOperation:[[[SieveGetScriptOperationalloc]initWithScript:scriptNameforClient:selfdelegate:newDelegateuserInfo:userInfo]autorelease]];}-(void)getScript:(NSString*)scriptName;{[selfgetScript:scriptNamedelegate:delegateuserInfo:NULL];}#pragma mark -#pragma mark Logging-(NSArray*)log{if(!log){log=[[NSMutableArrayalloc]init];}return[[logretain]autorelease];}-(unsigned)countOfLog{if(!log){log=[[NSMutableArrayalloc]init];}return[logcount];}-(id)objectInLogAtIndex:(unsigned)theIndex{if(!log){log=[[NSMutableArrayalloc]init];}return[logobjectAtIndex:theIndex];}-(void)getLog:(id*)objsPtrrange:(NSRange)range{if(!log){log=[[NSMutableArrayalloc]init];}[loggetObjects:objsPtrrange:range];}-(void)insertObject:(id)objinLogAtIndex:(unsigned)theIndex{if(!log){log=[[NSMutableArrayalloc]init];}[loginsertObject:objatIndex:theIndex];}-(void)removeObjectFromLogAtIndex:(unsigned)theIndex{if(!log){log=[[NSMutableArrayalloc]init];}[logremoveObjectAtIndex:theIndex];}-(void)replaceObjectInLogAtIndex:(unsigned)theIndexwithObject:(id)obj{if(!log){log=[[NSMutableArrayalloc]init];}[logreplaceObjectAtIndex:theIndexwithObject:obj];}@end