/*
 * Copyright (c) 2000, 2001 Alex Zepeda <zipzippy@sonic.net>
 * Copyright (c) 2001 Michael H�ckel <Michael@Haeckel.Net>
 * Copyright (c) 2002 Aaron J. Seigo <aseigo@olympusproject.org>
 * Copyright (c) 2003 Marc Mutz <mutz@kde.org>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 */

#include <config.h>

#ifdef HAVE_SYS_TYPES_H
# include <sys/types.h>
#endif
#include <stdio.h>

#ifdef HAVE_LIBSASL2
extern "C" {
#include <sasl/sasl.h>
}
#endif

#include "smtp.h"
#include "request.h"
#include "response.h"
#include "transactionstate.h"
#include "command.h"
using KioSMTP::Capabilities;
using KioSMTP::Command;
using KioSMTP::EHLOCommand;
using KioSMTP::AuthCommand;
using KioSMTP::MailFromCommand;
using KioSMTP::RcptToCommand;
using KioSMTP::DataCommand;
using KioSMTP::TransferCommand;
using KioSMTP::Request;
using KioSMTP::Response;
using KioSMTP::TransactionState;

#include <tdeemailsettings.h>
#include <ksock.h>
#include <kdebug.h>
#include <kinstance.h>
#include <tdeio/connection.h>
#include <tdeio/slaveinterface.h>
#include <tdelocale.h>

#include <tqstring.h>
#include <tqstringlist.h>
#include <tqcstring.h>

#include <memory>

#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

#ifdef HAVE_SYS_SOCKET_H
# include <sys/socket.h>
#endif
#include <netdb.h>

#ifndef NI_NAMEREQD
// FIXME for KDE 3.3: fake defintion
// API design flaw in KExtendedSocket::resolve
# define NI_NAMEREQD	0
#endif


extern "C" {
  TDE_EXPORT int kdemain(int argc, char **argv);
} 

int kdemain(int argc, char **argv)
{
  TDEInstance instance("tdeio_smtp");

  if (argc != 4) {
    fprintf(stderr,
            "Usage: tdeio_smtp protocol domain-socket1 domain-socket2\n");
    exit(-1);
  }

#ifdef HAVE_LIBSASL2
  if ( sasl_client_init( NULL ) != SASL_OK ) {
    fprintf(stderr, "SASL library initialization failed!\n");
    exit(-1);
  }
#endif
  SMTPProtocol slave( argv[2], argv[3], tqstricmp( argv[1], "smtps" ) == 0 );
  slave.dispatchLoop();
#ifdef HAVE_LIBSASL2
  sasl_done();
#endif
  return 0;
}

SMTPProtocol::SMTPProtocol(const TQCString & pool, const TQCString & app,
                           bool useSSL)
:  TCPSlaveBase(useSSL ? 465 : 25, 
                useSSL ? "smtps" : "smtp", 
                pool, app, useSSL),
   m_iOldPort(0),
   m_opened(false)
{
  //kdDebug(7112) << "SMTPProtocol::SMTPProtocol" << endl;
  mPendingCommandQueue.setAutoDelete( true );
  mSentCommandQueue.setAutoDelete( true );
}

unsigned int SMTPProtocol::sendBufferSize() const {
  // ### how much is eaten by SSL/TLS overhead?
  const int fd = fileno( fp );
  int value = -1;
  kde_socklen_t len = sizeof(value);
  if ( fd < 0 || ::getsockopt( fd, SOL_SOCKET, SO_SNDBUF, (char*)&value, &len ) )
    value = 1024; // let's be conservative
  kdDebug(7112) << "send buffer size seems to be " << value << " octets." << endl;
  return value > 0 ? value : 1024 ;
}

SMTPProtocol::~SMTPProtocol() {
  //kdDebug(7112) << "SMTPProtocol::~SMTPProtocol" << endl;
  smtp_close();
}

void SMTPProtocol::openConnection() {
  if ( smtp_open() )
    connected();
  else
    closeConnection();
}

void SMTPProtocol::closeConnection() {
  smtp_close();
}

void SMTPProtocol::special( const TQByteArray & aData ) {
  TQDataStream s( aData, IO_ReadOnly );
  int what;
  s >> what;
  if ( what == 'c' ) {
    infoMessage( createSpecialResponse() );
#ifndef NDEBUG
    kdDebug(7112) << "special('c') returns \"" << createSpecialResponse() << "\"" << endl;
#endif
  } else if ( what == 'N' ) {
    if ( !execute( Command::NOOP ) )
      return;
  } else {
    error( TDEIO::ERR_INTERNAL,
	   i18n("The application sent an invalid request.") );
    return;
  }
  finished();
}


// Usage: smtp://smtphost:port/send?to=user@host.com&subject=blah
// If smtphost is the name of a profile, it'll use the information 
// provided by that profile.  If it's not a profile name, it'll use it as
// nature intended.
// One can also specify in the query:
// headers=0 (turns off header generation)
// to=emailaddress
// cc=emailaddress
// bcc=emailaddress
// subject=text
// profile=text (this will override the "host" setting)
// hostname=text (used in the HELO)
// body={7bit,8bit} (default: 7bit; 8bit activates the use of the 8BITMIME SMTP extension)
void SMTPProtocol::put(const KURL & url, int /*permissions */ ,
                       bool /*overwrite */ , bool /*resume */ )
{
  Request request = Request::fromURL( url ); // parse settings from URL's query

  KEMailSettings mset;
  KURL open_url = url;
  if ( !request.hasProfile() ) {
    //kdDebug(7112) << "tdeio_smtp: Profile is null" << endl;
    bool hasProfile = mset.profiles().contains( open_url.host() );
    if ( hasProfile ) {
      mset.setProfile(open_url.host());
      open_url.setHost(mset.getSetting(KEMailSettings::OutServer));
      m_sUser = mset.getSetting(KEMailSettings::OutServerLogin);
      m_sPass = mset.getSetting(KEMailSettings::OutServerPass);

      if (m_sUser.isEmpty())
        m_sUser = TQString::null;
      if (m_sPass.isEmpty())
        m_sPass = TQString::null;
      open_url.setUser(m_sUser);
      open_url.setPass(m_sPass);
      m_sServer = open_url.host();
      m_iPort = open_url.port();
    }
    else {
      mset.setProfile(mset.defaultProfileName());
    }
  } 
  else {
    mset.setProfile( request.profileName() );
  }

  // Check KEMailSettings to see if we've specified an E-Mail address
  // if that worked, check to see if we've specified a real name
  // and then format accordingly (either: emailaddress@host.com or
  // Real Name <emailaddress@host.com>)
  if ( !request.hasFromAddress() ) {
    const TQString from = mset.getSetting( KEMailSettings::EmailAddress );
    if ( !from.isNull() )
      request.setFromAddress( from );
    else if ( request.emitHeaders() ) {
      error(TDEIO::ERR_NO_CONTENT, i18n("The sender address is missing."));
      return;
    }
  }

  if ( !smtp_open( request.heloHostname() ) )
  {
    error(TDEIO::ERR_SERVICE_NOT_AVAILABLE,
          i18n("SMTPProtocol::smtp_open failed (%1)") // ### better error message?
              .arg(open_url.path()));
    return;
  }

  if ( request.is8BitBody()
       && !haveCapability("8BITMIME") && metaData("8bitmime") != "on" ) {
    error( TDEIO::ERR_SERVICE_NOT_AVAILABLE,
	   i18n("Your server does not support sending of 8-bit messages.\n"
		"Please use base64 or quoted-printable encoding.") );
    return;
  }

  queueCommand( new MailFromCommand( this, request.fromAddress().latin1(),
				     request.is8BitBody(), request.size() ) );

  // Loop through our To and CC recipients, and send the proper
  // SMTP commands, for the benefit of the server.
  TQStringList recipients = request.recipients();
  for ( TQStringList::const_iterator it = recipients.begin() ; it != recipients.end() ; ++it )
    queueCommand( new RcptToCommand( this, (*it).latin1() ) );

  queueCommand( Command::DATA );
  queueCommand( new TransferCommand( this, request.headerFields( mset.getSetting( KEMailSettings::RealName ) ) ) );

  TransactionState ts;
  if ( !executeQueuedCommands( &ts ) ) {
    if ( ts.errorCode() )
      error( ts.errorCode(), ts.errorMessage() );
  } else
    finished();
}


void SMTPProtocol::setHost(const TQString & host, int port,
                           const TQString & user, const TQString & pass)
{
  m_sServer = host;
  m_iPort = port;
  m_sUser = user;
  m_sPass = pass;
}

bool SMTPProtocol::sendCommandLine( const TQCString & cmdline ) {
  //kdDebug( cmdline.length() < 4096, 7112) << "C: " << cmdline.data();
  //kdDebug( cmdline.length() >= 4096, 7112) << "C: <" << cmdline.length() << " bytes>" << endl;
  kdDebug( 7112) << "C: <" << cmdline.length() << " bytes>" << endl;
  ssize_t cmdline_len = cmdline.length();
  if ( write( cmdline.data(), cmdline_len ) != cmdline_len ) {
    error( TDEIO::ERR_COULD_NOT_WRITE, m_sServer );
    return false;
  }
  return true;
}

Response SMTPProtocol::getResponse( bool * ok ) {

  if ( ok )
    *ok = false;

  Response response;
  char buf[2048];

  int recv_len = 0;
  do {
    // wait for data...
    if ( !waitForResponse( 600 ) ) {
      error( TDEIO::ERR_SERVER_TIMEOUT, m_sServer );
      return response;
    }

    // ...read data...
    recv_len = readLine( buf, sizeof(buf) - 1 );
    if ( recv_len < 1 && !isConnectionValid() ) {
      error( TDEIO::ERR_CONNECTION_BROKEN, m_sServer );
      return response;
    }

    kdDebug(7112) << "S: " << TQCString( buf, recv_len + 1 ).data();
    // ...and parse lines...
    response.parseLine( buf, recv_len );

    // ...until the response is complete or the parser is so confused
    // that it doesn't think a RSET would help anymore:
  } while ( !response.isComplete() && response.isWellFormed() );

  if ( !response.isValid() ) {
    error( TDEIO::ERR_NO_CONTENT, i18n("Invalid SMTP response (%1) received.").arg(response.code()) );
    return response;
  }

  if ( ok )
    *ok = true;

  return response;
}

bool SMTPProtocol::executeQueuedCommands( TransactionState * ts ) {
  assert( ts );

  kdDebug( canPipelineCommands(), 7112 ) << "using pipelining" << endl;

  while( !mPendingCommandQueue.isEmpty() ) {
    TQCString cmdline = collectPipelineCommands( ts );
    if ( ts->failedFatally() ) {
      smtp_close( false ); // _hard_ shutdown
      return false;
    }
    if ( ts->failed() )
      break;
    if ( cmdline.isEmpty() )
      continue;
    if ( !sendCommandLine( cmdline ) ||
	 !batchProcessResponses( ts ) ||
	 ts->failedFatally() ) {
      smtp_close( false ); // _hard_ shutdown
      return false;
    }
  }

  if ( ts->failed() ) {
    if ( !execute( Command::RSET ) )
      smtp_close( false );
    return false;
  }
  return true;
}

TQCString SMTPProtocol::collectPipelineCommands( TransactionState * ts ) {
  assert( ts );

  TQCString cmdLine;
  unsigned int cmdLine_len = 0;

  while ( mPendingCommandQueue.head() ) {

    Command * cmd = mPendingCommandQueue.head();

    if ( cmd->doNotExecute( ts ) ) {
      delete mPendingCommandQueue.dequeue();
      if ( cmdLine_len )
	break;
      else
	continue;
    }

    if ( cmdLine_len && cmd->mustBeFirstInPipeline() )
      break;

    if ( cmdLine_len && !canPipelineCommands() )
      break;

    while ( !cmd->isComplete() && !cmd->needsResponse() ) {
      const TQCString currentCmdLine = cmd->nextCommandLine( ts );
      if ( ts->failedFatally() )
	return cmdLine;
      const unsigned int currentCmdLine_len = currentCmdLine.length();

      if ( cmdLine_len && cmdLine_len + currentCmdLine_len > sendBufferSize() ) {
	// must all fit into the send buffer, else connection deadlocks,
	// but we need to have at least _one_ command to send
	cmd->ungetCommandLine( currentCmdLine, ts );
	return cmdLine;
      }
      cmdLine_len += currentCmdLine_len;
      cmdLine += currentCmdLine;
    }

    mSentCommandQueue.enqueue( mPendingCommandQueue.dequeue() );

    if ( cmd->mustBeLastInPipeline() )
      break;
  }

  return cmdLine;
}

bool SMTPProtocol::batchProcessResponses( TransactionState * ts ) {
  assert( ts );

  while ( !mSentCommandQueue.isEmpty() ) {

    Command * cmd = mSentCommandQueue.head();
    assert( cmd->isComplete() );

    bool ok = false;
    Response r = getResponse( &ok );
    if ( !ok )
      return false;
    cmd->processResponse( r, ts );
    if ( ts->failedFatally() )
      return false;

    mSentCommandQueue.remove();
  }

  return true;
}

void SMTPProtocol::queueCommand( int type ) {
  queueCommand( Command::createSimpleCommand( type, this ) );
}

bool SMTPProtocol::execute( int type, TransactionState * ts ) {
  std::unique_ptr<Command> cmd( Command::createSimpleCommand( type, this ) );
  kdFatal( !cmd, 7112 ) << "Command::createSimpleCommand( " << type << " ) returned null!" << endl;
  return execute( cmd.get(), ts );
}

// ### fold into pipelining engine? How? (execute() is often called
// ### when command queues are _not_ empty!)
bool SMTPProtocol::execute( Command * cmd, TransactionState * ts ) 
{
  kdFatal( !cmd, 7112 ) << "SMTPProtocol::execute() called with no command to run!" << endl;

  if (!cmd)
    return false;

  if ( cmd->doNotExecute( ts ) )
    return true;

  do {
    while ( !cmd->isComplete() && !cmd->needsResponse() ) {
      const TQCString cmdLine = cmd->nextCommandLine( ts );
      if ( ts && ts->failedFatally() ) {
	smtp_close( false );
	return false;
      }
      if ( cmdLine.isEmpty() )
	continue;
      if ( !sendCommandLine( cmdLine ) ) {
	smtp_close( false );
	return false;
      }
    }

    bool ok = false;
    Response r = getResponse( &ok );
    if ( !ok ) {
      smtp_close( false );
      return false;
    }
    if ( !cmd->processResponse( r, ts ) ) {
      if ( ts && ts->failedFatally() ||
	   cmd->closeConnectionOnError() ||
	   !execute( Command::RSET ) )
	smtp_close( false );
      return false;
    }
  } while ( !cmd->isComplete() );

  return true;
}

bool SMTPProtocol::smtp_open(const TQString& fakeHostname)
{
  if (m_opened && 
      m_iOldPort == port(m_iPort) &&
      m_sOldServer == m_sServer && 
      m_sOldUser == m_sUser &&
      (fakeHostname.isNull() || m_hostname == fakeHostname)) 
    return true;

  smtp_close();
  if (!connectToHost(m_sServer, m_iPort))
    return false;             // connectToHost has already send an error message.
  m_opened = true;

  bool ok = false;
  Response greeting = getResponse( &ok );
  if ( !ok || !greeting.isOk() )
  {
    if ( ok )
      error( TDEIO::ERR_COULD_NOT_LOGIN, 
             i18n("The server did not accept the connection.\n"
		  "%1").arg( greeting.errorMessage() ) );
    smtp_close();
    return false;
  }

  if (!fakeHostname.isNull())
  {
    m_hostname = fakeHostname;
  }
  else
  { 
    TQString tmpPort;
    TDESocketAddress* addr = KExtendedSocket::localAddress(m_iSock);
    // perform name lookup. NI_NAMEREQD means: don't return a numeric
    // value (we need to know when we get have the IP address, so we
    // can enclose it in sqaure brackets (domain-literal). Failure to
    // do so is normally harmless with IPv4, but fails for IPv6:
    if (KExtendedSocket::resolve(addr, m_hostname, tmpPort, NI_NAMEREQD) != 0)
      // FQDN resolution failed
      // use the IP address as domain-literal
      m_hostname = '[' + addr->nodeName() + ']';
    delete addr;

    if(m_hostname.isEmpty())
    {
      m_hostname = "localhost.invalid";
    }
  }

  EHLOCommand ehloCmdPreTLS( this, m_hostname );
  if ( !execute( &ehloCmdPreTLS ) ) {
    smtp_close();
    return false;
  }

  if ( ( haveCapability("STARTTLS") && canUseTLS() && metaData("tls") != "off" )
       || metaData("tls") == "on" ) {
    // For now we're gonna force it on.

    if ( execute( Command::STARTTLS ) ) {

      // re-issue EHLO to refresh the capability list (could be have
      // been faked before TLS was enabled):
      EHLOCommand ehloCmdPostTLS( this, m_hostname );
      if ( !execute( &ehloCmdPostTLS ) ) {
        smtp_close();
        return false;
      }
    }
  }
  // Now we try and login
  if (!authenticate()) {
    smtp_close();
    return false;
  }

  m_iOldPort = m_iPort;
  m_sOldServer = m_sServer;
  m_sOldUser = m_sUser;
  m_sOldPass = m_sPass;

  return true;
}

bool SMTPProtocol::authenticate()
{
  // return with success if the server doesn't support SMTP-AUTH or an user 
  // name is not specified and metadata doesn't tell us to force it.
  if ( (m_sUser.isEmpty() || !haveCapability( "AUTH" )) && 
    metaData( "sasl" ).isEmpty() ) return true;

  TDEIO::AuthInfo authInfo;
  authInfo.username = m_sUser;
  authInfo.password = m_sPass;
  authInfo.prompt = i18n("Username and password for your SMTP account:");
  
  TQStringList strList;

  if (!metaData("sasl").isEmpty())
    strList.append(metaData("sasl").latin1());
  else
    strList = mCapabilities.saslMethodsQSL();

  AuthCommand authCmd( this, strList.join(" ").latin1(), m_sServer, authInfo );
  bool ret = execute( &authCmd );
  m_sUser = authInfo.username;
  m_sPass = authInfo.password;
  return ret;
}

void SMTPProtocol::parseFeatures( const Response & ehloResponse ) {
  mCapabilities = Capabilities::fromResponse( ehloResponse );

  TQString category = usingTLS() ? "TLS" : usingSSL() ? "SSL" : "PLAIN" ;
  setMetaData( category + " AUTH METHODS", mCapabilities.authMethodMetaData() );
  setMetaData( category + " CAPABILITIES", mCapabilities.asMetaDataString() );
#ifndef NDEBUG
  kdDebug(7112) << "parseFeatures() " << category << " AUTH METHODS:"
		<< '\n' + mCapabilities.authMethodMetaData() << endl
		<< "parseFeatures() " << category << " CAPABILITIES:"
		<< '\n' + mCapabilities.asMetaDataString() << endl;
#endif
}

void SMTPProtocol::smtp_close( bool nice ) {
  if (!m_opened)                  // We're already closed
    return;

  if ( nice )
    execute( Command::QUIT );
  kdDebug( 7112 ) << "closing connection" << endl;
  closeDescriptor();
  m_sOldServer = TQString::null;
  m_sOldUser = TQString::null;
  m_sOldPass = TQString::null;
  
  mCapabilities.clear();
  mPendingCommandQueue.clear();
  mSentCommandQueue.clear();

  m_opened = false;
}

void SMTPProtocol::stat(const KURL & url)
{
  TQString path = url.path();
  error(TDEIO::ERR_DOES_NOT_EXIST, url.path());
}