/*
**  SMTP.m
**
**  Copyright (c) 2001, 2002, 2003
**
**  Author: Ludovic Marcotte <ludovic@Sophos.ca>
**
**  This library 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.1 of the License, or (at your option) any later version.
**  
**  This library 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 this library; if not, write to the Free Software
**  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/

#include <Pantomime/SMTP.h>

#include <Pantomime/Connection.h>
#include <Pantomime/Constants.h>
#include <Pantomime/InternetAddress.h>
#include <Pantomime/MD5.h>
#include <Pantomime/Message.h>
#include <Pantomime/MimeUtility.h>
#include <Pantomime/NSData+Extensions.h>
#include <Pantomime/TCPConnection.h>

#include <Foundation/NSBundle.h>
#include <Foundation/NSDebug.h>
#include <Foundation/NSEnumerator.h>
#include <Foundation/NSPathUtilities.h>

#define CR '\r'
#undef LF
#define LF '\n'

@implementation SMTP

//
//
//
- (id) initWithName: (NSString *) theName
               port: (int) thePort
{
  self = [super init];

  [self _preInit];
  [self setName: theName];
  [self setPort: thePort];

  tcpConnection = [[TCPConnection alloc] initWithName: [self name]
					 port: thePort];

  if ( !tcpConnection )
    {
      AUTORELEASE(self);
      return nil;
    }
  
  if ( ![self _postInit] )
    {
      AUTORELEASE(self);
      return nil;
    }

  return self;
}


//
//
//
- (id) initWithName: (NSString *) theName
{
  return [self initWithName: theName
	       port: 25];
}


//
//
//
- (id) initSSLWithName: (NSString *) theName
		  port: (int) thePort
{
  NSMutableArray *allPaths;
  NSBundle *aBundle;
  NSString *aPath;
  int i;

  self = [super init];

  [self _preInit];
  [self setName: theName];
  [self setPort: thePort];

  // We load our TCPSSLConnection bundle.
  allPaths = [NSMutableArray array];
  [allPaths addObjectsFromArray: NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,
								     NSLocalDomainMask|NSNetworkDomainMask|NSSystemDomainMask|NSUserDomainMask,
								     YES)];
#ifdef MACOSX
  [allPaths insertObject: [[NSBundle mainBundle] builtInPlugInsPath]  atIndex: 0];
#endif

  aBundle = nil;
  
  for (i = 0; i < [allPaths count]; i++)
    {
      aPath = [NSString stringWithFormat: @"%@/Pantomime/TCPSSLConnection.bundle",
			[allPaths objectAtIndex: i]];
      
      aBundle = [NSBundle bundleWithPath: aPath];
      
      if ( aBundle ) break;
    }
  
  if ( !aBundle )
    {
      NSDebugLog(@"SMTP: Failed to load the TCPSSLConnection bundle");
      AUTORELEASE(self);
      return nil;
    }
  
  tcpConnection = [[[aBundle principalClass] alloc] initWithName: theName
						    port: thePort];

  if ( !tcpConnection )
    {
      AUTORELEASE(self);
      return nil;
    }
  
  if ( ![self _postInit] )
    {
      AUTORELEASE(self);
      return nil;
    }

  return self; 
}

//
//
//
- (void) dealloc
{
  RELEASE(supportedMechanisms);
  RELEASE(responsesFromServer);
  RELEASE(name);
  
  TEST_RELEASE(username);

  TEST_RELEASE((id<NSObject>)tcpConnection);
  
  [super dealloc];
}


//
// This method is used to authenticate ourself to the SMTP server.
//
- (BOOL) authenticate: (NSString *) theUsername
	     password: (NSString *) thePassword
	    mechanism: (NSString *) theMechanism
{
  // We first retain the username for future use
  username = RETAIN(theUsername);

  // If the mechanism is nil, we go from the 'best' one to the 'worst' one
  if ( !theMechanism )
    {
      NSDebugLog(@"SMTP authentication mechanism is nil - FIXME!");
    }
  else if ( [theMechanism caseInsensitiveCompare: @"PLAIN"] == NSOrderedSame )
    {
      return [self _plainAuthentication: theUsername  password: thePassword];
    }
  else if ( [theMechanism caseInsensitiveCompare: @"LOGIN"] == NSOrderedSame )
    {
      return [self _loginAuthentication: theUsername  password: thePassword];
    }
  else if ( [theMechanism caseInsensitiveCompare: @"CRAM-MD5"] == NSOrderedSame )
    {
      return [self _cramMD5Authentication: theUsername  password: thePassword];
    }
  
  NSDebugLog(@"Unsupported SMTP authentication method.");
  
  return NO;
}		  


//
//
//
- (NSString *) name
{
  return name;
}


//
//
//
- (void) setName: (NSString *) theName
{
  if ( theName )
    {
      RETAIN(theName);
      RELEASE(name);
      name = theName;
    }
  else
    {
      DESTROY(theName);
    }
}


- (int) port
{
  return port;
}

- (void) setPort: (int) thePort
{
  port = thePort;
}

//
//
//
- (id<Connection>) tcpConnection
{
  return tcpConnection;
}


//
//
//
- (NSString *) username
{
  return username;
}

//
//
//
- (BOOL) sendMessage: (Message *) theMessage
{
  return [self _sendMessage: theMessage
	       withRawSource: [theMessage dataValue]];
}


//
// The message received has \n for each lines, we must put \r\n 
// at the end of each line instead.
//
- (BOOL) sendMessageFromRawSource: (NSData *) theData
{
  Message *aMessage;
  BOOL aBOOL;

  aMessage = [[Message alloc] initWithData: theData];

  aBOOL = [self _sendMessage: aMessage 
		withRawSource: theData];

  DESTROY(aMessage);
  
  return aBOOL;
}


//
//
//
- (void) close
{
  [[self tcpConnection] writeLine: @"QUIT"];
  [self _parseServerOutput];

  if ( [self lastResponseCode] != 221 )
    {
      NSDebugLog(@"SMTP: An error occured while ending the connection with the SMTP server.");
      [[self tcpConnection] close];
    }
}


//
//
//
- (NSArray *) supportedMechanisms
{
  return [NSArray arrayWithArray: supportedMechanisms];
}


//
// This method sends a RSET SMTP command.
//
- (void) reset
{ 
  [[self tcpConnection] writeLine: @"RSET"];
  [self _parseServerOutput];

  if ( [self lastResponseCode] != 250 )
    {
      NSDebugLog(@"SMTP: RSET failed.");
    }
}


//
// This method returns the last response (in text) obtained from the SMTP
// server. If the last command issued a multiline response, it'll return the
// last text response and none of the previous ones.
//
- (NSString *) lastResponse
{
  if ( [responsesFromServer count] > 0 )
    {
      return [[responsesFromServer lastObject] text];
    }

  return nil;
}


//
// Same as -lastResponse except it does return only the response code.
//
- (int) lastResponseCode
{
  if ( [responsesFromServer count] > 0 )
    {
      return [[responsesFromServer lastObject] code];
    }

  return 0;
}

@end


//
// Private methods
//
@implementation SMTP (Private)


//
// This method receives a string like: AUTH PLAIN LOGIN
//                                     AUTH=PLAIN LOGIN X-NETSCAPE-HAS-BUGS
// It decode the string to build a list of supported authentication mechanisms.
//
- (void) _decodeSupportedAuthenticationMechanismFromResponse: (SMTPResponse *) theResponse
{
  NSRange aRange;
  
  //
  // Even if we already have decoded some SMTP authentication mechanisms, we must decode
  // again the mechanism received since some servers send:
  //
  // AUTH LOGIN
  // AUTH=PLAIN CRAM-MD5 DIGEST-MD5
  //
  // and we could ignore the relevant authentication mechanisms.
  //
  aRange = [[theResponse text] rangeOfString: @"AUTH"
			       options: NSCaseInsensitiveSearch];
  
  if ( aRange.length )
    {
      NSEnumerator *theEnumerator;
      NSString *aString;

      // We trim the AUTH (and the ' ' or '=' following it)
      aString = [[theResponse text] substringFromIndex: (aRange.location+aRange.length) + 1]; 
      
      // We trim our \r\n
      aString = [aString substringToIndex: ([aString length] - 2)];
      
      theEnumerator = [[aString componentsSeparatedByString: @" "] objectEnumerator];

      while ( (aString = [theEnumerator nextObject]) )
	{
	  if ( ![supportedMechanisms containsObject: aString] )
	    {
	      [supportedMechanisms addObject: aString];
	    }
	}
    }
}

//
// rfc1870: Message Size Declaration
// This method receives a string like: SIZE size-param
//                                     size-param ::= [1*DIGIT]
// It decode the maximum size (of one message) allowed by the server
//
- (void) _decodeMaxSizeAllowedFromResponse: (SMTPResponse *) theResponse
{
  NSRange aRange;
  
  // If the server only provided us the SIZE\r\n, w/o any value.
  if ( [[theResponse text] length] == 6 )
    {
      maxSizeAllowedByServer = 0;
      return;
    }

  aRange = [[theResponse text] rangeOfString: @"SIZE"
			       options: NSCaseInsensitiveSearch];
  
  if ( aRange.length )
    {
      NSString *aString;
      
      // We trim the SIZE (and the ' ' or '=' following it)
      aString = [[theResponse text] substringFromIndex: (aRange.location+aRange.length) + 1]; 
      
      // We trim our \r\n
      aString = [aString substringToIndex: ([aString length] - 2)];
      
      maxSizeAllowedByServer = [aString intValue];
    }
}


//
//
//
- (void) _parseServerOutput
{
  SMTPResponse *aResponse;
  NSString *aString;
  BOOL isMultiline;
  
  
  // We remove all previously read responses
  [responsesFromServer removeAllObjects];
  isMultiline = YES;
  
  while ( isMultiline )
    {
      // From RFC2821:
      //
      // Formally, a reply is defined to be the sequence: a
      // three-digit code, <SP>, one line of text, and <CRLF>, or a multiline (...)
      // 
      // The format for multiline replies requires that every line, except the
      // last, begin with the reply code, followed immediately by a hyphen,
      // "-" (also known as minus), followed by text.  The last line will
      // begin with the reply code, followed immediately by <SP>, optionally
      // some text, and <CRLF>.  As noted above, servers SHOULD send the <SP>
      // if subsequent text is not sent, but clients MUST be prepared for it
      // to be omitted.
      //
      // So, we have:
      //
      // Reply-line = Reply-code [ SP text ] CRLF
      // 
      // For example:
      //
      // 123-First line
      // 123-Second line
      // 123-234 text beginning with numbers
      // 123 The last line
      //
      aString = [[self tcpConnection] readStringToEndOfLine];

      if ( !aString )
	{
	  NSDebugLog(@"SMTP: Error on reading the code.");
	  return;
	}
      
      // We verify if we got a multiline response
      if ( [aString length] > 3 && [aString characterAtIndex: 3] == '-' )
	{
	  isMultiline = YES;
	}
      else
	{
	  isMultiline = NO;
	}

      // We decode our response. If we got only the SMTP code (the server ommited to send the text)...
      if ( [aString length] < 5 )
	{
	  aResponse = [[SMTPResponse alloc] initResponseWithCode: [[aString substringToIndex: 3] intValue]  text: nil];
	}
      else
	{
	  aResponse = [[SMTPResponse alloc] initResponseWithCode: [[aString substringToIndex: 3] intValue]
					    text: [aString substringFromIndex: 4]];
	}

      [responsesFromServer addObject: aResponse];
      RELEASE(aResponse);
    }
}


//
//
//
- (BOOL) _sendMessage: (Message *) theMessage
	withRawSource: (NSData *) theRawSource
{
  BOOL isBouncedMessage;
  NSString *from;
  NSRange aRange;
  
  if ( !theMessage )
    {
      return NO;
    }
  
  // We first verify if it's a bounced message
  if ( [theMessage resentFrom ])
    {
      isBouncedMessage = YES;
      from = [[theMessage resentFrom] address];
    }
  else
    {
      isBouncedMessage = NO;
      from = [[theMessage from] address];
    }
  
  // We first replace all occurences of \n by \r\n
  theRawSource = [[NSMutableData dataWithData: theRawSource] replaceLFWithCRLF];
  
  //
  // According to RFC 2821 section 4.5.2, we must check for the character
  // sequence "<CRLF>.<CRLF>"; any occurrence have its period duplicated
  // to avoid data transparency. 
  //
  aRange = [theRawSource rangeOfCString: "\r\n."];
  
  if ( aRange.location != NSNotFound )
    {
      NSMutableData *aRawSource;
      NSRange aSubRange;
      
      aRawSource = [NSMutableData dataWithCapacity: [theRawSource length] + 1];
      aSubRange = NSMakeRange(0,0);
      
      do
	{
	  aSubRange = NSMakeRange(aSubRange.location, aRange.location - aSubRange.location);

	  [aRawSource appendData: [theRawSource subdataWithRange: aSubRange]];
	  [aRawSource appendBytes: "\r\n.."
		      length: 4];

	  aSubRange = NSMakeRange(aRange.location + 3, [theRawSource length] - aRange.location - 3);
	  aRange = [theRawSource rangeOfCString: "\r\n."
				 options: 0 
				 range: aSubRange];
	} 
      while ( aRange.location != NSNotFound );
      
      [aRawSource appendData: [theRawSource subdataWithRange: aSubRange]];
      theRawSource = aRawSource;
    }

  if ( maxSizeAllowedByServer != 0 )
    {
      // We declare the message size
      [[self tcpConnection] writeLine: [NSString stringWithFormat: @"MAIL FROM:<%@> SIZE=%d", 
						 from, 
						 [theRawSource length]]];
    }
  else
    {
      [[self tcpConnection] writeLine: [NSString stringWithFormat: @"MAIL FROM:<%@>", from]];
    }
  
  // We read the server's response after our MAIL FROM: ...
  [self _parseServerOutput];

  // We verify the response from our server from the MAIL FROM command
  if ( [self lastResponseCode] != 250 )
    {
      return NO;
    }
  
  if ( ![self _writeRecipients: [theMessage recipients] 
	      usingBouncingMode: isBouncedMessage] )
    {
      return NO;
    }
  
  return [self _writeMessageFromRawSource: theRawSource];
}


//
//
//
- (BOOL) _writeRecipients: (NSArray *) recipients
	usingBouncingMode: (BOOL) aBOOL;
{
  NSEnumerator *recipientsEnumerator;
  InternetAddress *theAddress;
  NSString *aString;
  
  recipientsEnumerator = [recipients objectEnumerator];
      
  // We verify if we have at least one recipient
  if ( !recipients || [recipients count] == 0 )
    {
      NSDebugLog(@"SMTP: No recipients were found, aborting.");
      return NO;
    }
  
  while ( (theAddress = [recipientsEnumerator nextObject]) )
    {
      // If it's a bounced message...
      if ( aBOOL )
	{
	  // We only get the bounced recipients
	  if ( [theAddress type] > 3 )
	    {
	      aString = [NSString stringWithFormat: @"RCPT TO:<%@>", [theAddress address]];
	    }
	  else
	    {
	      aString = nil;
	    }
	}
      else
	{
	  // Otherwise, we get the real recipients
	  if ( [theAddress type] < 4 )
	    {
	      aString = [NSString stringWithFormat: @"RCPT TO:<%@>", [theAddress address]];
	    }
	  else
	    {
	      aString = nil;
	    }
	}
      
      // If we have a recipient to write, let's write the string to the socket!
      if ( aString )
	{
	  [[self tcpConnection] writeLine: aString];
	  [self _parseServerOutput];

	  // We verify if the server accepted this recipient.
	  if ( [self lastResponseCode] != 250 )
	    {
	      return NO;
	    }
	}
    } // while (...)

  return YES;
}


//
//
//
- (BOOL) _writeMessageFromRawSource: (NSData *) theRawSource
{    
  [[self tcpConnection] writeLine: @"DATA"];
  [self _parseServerOutput];

  if ( [self lastResponseCode] != 354 )
    {
      NSDebugLog(@"SMTP: an error occured while writing the DATA command, we abort.");
      return NO;
    }

  [[self tcpConnection] writeData: theRawSource];
  [[self tcpConnection] writeString: @"\r\n.\r\n"];
  
  [self _parseServerOutput];

  if ( [self lastResponseCode] != 250 )
    {
      return NO;
    }

  return YES;
}


//
// PLAIN authentication mechanism (RFC2595)
//
- (BOOL) _plainAuthentication: (NSString *) theUsername
		     password: (NSString *) thePassword
{  
  [[self tcpConnection] writeLine: @"AUTH PLAIN"];
  [self _parseServerOutput];
  
  if ( [self lastResponseCode] == 334 )
    {
      NSMutableData *aMutableData;
      NSString *aString;
      
      int len_username, len_password;
      
      len_username = [theUsername length];
  
      if ( !thePassword )
	{
	  len_password = 0;
	}
      else
	{
	  len_password = [thePassword length];
	}
      
      // We create our phrase
      aMutableData = [NSMutableData dataWithLength: (len_username + len_password + 2)];
      
      [aMutableData replaceBytesInRange: NSMakeRange(1,len_username)
		    withBytes: [[theUsername dataUsingEncoding: NSASCIIStringEncoding] bytes]];
      
      
      [aMutableData replaceBytesInRange: NSMakeRange(2 + len_username, len_password)
		    withBytes: [[thePassword dataUsingEncoding: NSASCIIStringEncoding] bytes]];
      
      aString = [[NSString alloc] initWithData: [MimeUtility encodeBase64: aMutableData
							     lineLength: 0]
				  encoding: NSASCIIStringEncoding];
      
      [[self tcpConnection] writeLine: aString];
      RELEASE(aString);
      
      [self _parseServerOutput];

      if ( [self lastResponseCode] == 235 )
	{
	  NSDebugLog(@"PLAIN Authentication successful");
	  return YES;
	}
    }

  return NO;
}


//
// LOGIN authentication mechanism (undocumented but easy to figure out)
//
- (BOOL) _loginAuthentication: (NSString *) theUsername
		     password: (NSString *) thePassword
{
  NSString *aString;
  
  [[self tcpConnection] writeLine: @"AUTH LOGIN"];

  aString = [[self tcpConnection] readStringToEndOfLine];
 
  if ( [aString hasPrefix: @"334"] )
    {
      NSString *un, *pw;

      un = [[NSString alloc] initWithData: [MimeUtility encodeBase64: [theUsername dataUsingEncoding: NSASCIIStringEncoding]
							lineLength: 0]
			     encoding: NSASCIIStringEncoding];
      
      [[self tcpConnection] writeLine: un];
      RELEASE(un);
      
      aString = [[self tcpConnection] readStringToEndOfLine];

      if ( [aString hasPrefix: @"334"] )
	{
	  pw = [[NSString alloc] initWithData: [MimeUtility encodeBase64: [thePassword dataUsingEncoding: NSASCIIStringEncoding]
							    lineLength: 0]
				 encoding: NSASCIIStringEncoding];
	  
	  [[self tcpConnection] writeLine: pw];
	  RELEASE(pw);
	  
	  [self _parseServerOutput];
	  
	  if ( [self lastResponseCode] == 235 )
	    {
	      NSDebugLog(@"LOGIN Authentication successful");
	      return YES;
	    }
	}
    }
  
  return NO;
}


//
// CRAM-MD5 authentication mechanism (2195)
//
- (BOOL) _cramMD5Authentication: (NSString *) theUsername
		       password: (NSString *) thePassword
{
  NSString *aString;
  
  [[self tcpConnection] writeLine: @"AUTH CRAM-MD5"];
  
  aString = [[self tcpConnection] readStringToEndOfLine];
  
  if ( [aString hasPrefix: @"334"] )
    {
      MD5 *aMD5;
      
      // We trim the "334 " and we keep the challenge phrase
      aString = [aString substringFromIndex: 4];
      
      // We trim our \r\n
      aString = [aString substringToIndex: ([aString length] - 2)];
      
      aString = [[NSString alloc] initWithData: [MimeUtility decodeBase64: [aString dataUsingEncoding: NSASCIIStringEncoding]]
    				  encoding: NSASCIIStringEncoding];;
      
      
      aMD5 = [[MD5 alloc] initWithString: aString
			  encoding: NSASCIIStringEncoding];
      [aMD5 computeDigest];
      RELEASE(aString);
      
      aString = [NSString stringWithFormat: @"%@ %@", theUsername, [aMD5 hmacAsStringUsingPassword: thePassword]];
      aString = [[NSString alloc] initWithData: [MimeUtility encodeBase64: [aString dataUsingEncoding: NSASCIIStringEncoding]
    							     lineLength: 0]
    				  encoding: NSASCIIStringEncoding];
      RELEASE(aMD5);
      
      [[self tcpConnection] writeLine: aString];
      RELEASE(aString);
      
      [self _parseServerOutput];
      
      if ( [self lastResponseCode] )
	{
	  NSDebugLog(@"CRAM-MD5 Authentication successful");
	  return YES;
	}
    }

  return NO;
}


//
//
//
- (void) _preInit
{
  supportedMechanisms = [[NSMutableArray alloc] init];
  responsesFromServer = [[NSMutableArray alloc] init];
  username = nil;
  maxSizeAllowedByServer = 0;
}


//
// This method returns no in case of a failure
//
- (BOOL) _postInit
{
  unsigned int i;

  // We read the server's greeting message (code 220)
  [self _parseServerOutput];

  if ( [self lastResponseCode] != 220 )
    {
      return NO;
    }
  
  [[self tcpConnection] writeLine: @"EHLO localhost.localdomain"];
  [self _parseServerOutput];

  // If the server doesn't support the extented SMTP service.
  if ( [self lastResponseCode] != 250 )
    {
      NSDebugLog(@"SMTP: The server doesn't support the extended SMTP service.");
      [[self tcpConnection] writeLine: @"HELO localhost.localdomain"];
      [self _parseServerOutput];
      
      if ( [self lastResponseCode] != 250 )
	{
	  return NO;
	}
    }

  // We analyze the lines
  for ( i = 0; i < [responsesFromServer count]; i++ )
    {
      // Decode authentication mechanism
      [self _decodeSupportedAuthenticationMechanismFromResponse: [responsesFromServer objectAtIndex: i]];
      
      // RFC1870: Message Size Declaration
      [self _decodeMaxSizeAllowedFromResponse: [responsesFromServer objectAtIndex: i]];
    }

  return YES;
}

@end


//
// SMTPResponse implementation
//
@implementation SMTPResponse

- (id) initResponseWithCode: (int) theCode
		       text: (NSString *) theText
{
  self = [super init];

  code = theCode;

  if ( theText )
    {
      ASSIGN(text, theText);
    }
  else
    {
      text = nil;
    }

  return self;
}


//
//
//
- (int) code
{
  return code;
}


//
//
//
- (NSString *) text
{
  return text;
}


//
//
//
- (void) dealloc
{
  TEST_RELEASE(text);
  
  [super dealloc];
}

@end
