/*
  Copyright (c) 2010 Volker Krause <vkrause@kde.org>

  This library is free software; you can redistribute it and/or modify it
  under the terms of the GNU Library General Public License as published by
  the Free Software Foundation; either version 2 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 Library General Public
  License for more details.

  You should have received a copy of the GNU Library General Public License
  along with this library; see the file COPYING.LIB.  If not, write to the
  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  02110-1301, USA.
*/

#include "smtpsession.h"

#include "common.h"
#include "smtp/smtpsessioninterface.h"
#include "smtp/request.h"
#include "smtp/response.h"
#include "smtp/command.h"
#include "smtp/transactionstate.h"

#include <ktcpsocket.h>
#include <KMessageBox>
#include <KIO/PasswordDialog>
#include <kio/authinfo.h>
#include <kio/global.h>
#include <kio/sslui.h>
#include <KLocalizedString>
#include <KDebug>

#include <QtCore/QQueue>

using namespace MailTransport;
using namespace KioSMTP;

class MailTransport::SmtpSessionPrivate : public KioSMTP::SMTPSessionInterface
{
  public:
    explicit SmtpSessionPrivate( SmtpSession *session ) :
      useTLS( true ),
      socket( 0 ),
      currentCommand( 0 ),
      currentTransactionState( 0 ),
      state( Initial ),
      q( session )
     {}

    void dataReq() { /* noop */ };
    int readData( QByteArray &ba )
    {
      if ( data->atEnd() ) {
        ba.clear();
        return 0;
      } else {
        Q_ASSERT( data->isOpen() );
        ba = data->read( 32 * 1024 );
        return ba.size();
      }
    }

    void error( int id, const QString &msg )
    {
      kDebug() << id << msg;
      // clear state so further replies don't end up in failed commands etc.
      currentCommand = 0;
      currentTransactionState = 0;

      if ( errorMessage.isEmpty() ) {
        errorMessage = KIO::buildErrorString( id, msg );
      }
      q->disconnectFromHost();
    }

    void informationMessageBox( const QString &msg, const QString &caption )
    {
      KMessageBox::information( 0, msg, caption );
    }

    bool openPasswordDialog( KIO::AuthInfo &authInfo ) {
      return KIO::PasswordDialog::getNameAndPassword(
        authInfo.username,
        authInfo.password,
        &( authInfo.keepPassword ),
        authInfo.prompt,
        authInfo.readOnly,
        authInfo.caption,
        authInfo.comment,
        authInfo.commentLabel ) == KIO::PasswordDialog::Accepted;
    }

    bool startSsl()
    {
      kDebug();
      Q_ASSERT( socket );
      socket->setAdvertisedSslVersion( KTcpSocket::TlsV1 );
      socket->ignoreSslErrors();
      socket->startClientEncryption();
      const bool encrypted = socket->waitForEncrypted( 60 * 1000 );

      const KSslCipher cipher = socket->sessionCipher();
      if ( !encrypted ||
           socket->sslErrors().count() > 0 ||
           socket->encryptionMode() != KTcpSocket::SslClientMode ||
           cipher.isNull() ||
           cipher.usedBits() == 0 ) {
        kDebug() << "Initial SSL handshake failed. cipher.isNull() is" << cipher.isNull()
                 << ", cipher.usedBits() is" << cipher.usedBits()
                 << ", the socket says:" <<  socket->errorString()
                 << "and the list of SSL errors contains"
                 << socket->sslErrors().count() << "items.";

        if ( KIO::SslUi::askIgnoreSslErrors( socket ) ) {
          return true;
        } else {
          return false;
        }
      } else {
        kDebug() << "TLS negotiation done.";
        return true;
      }
    }

    bool lf2crlfAndDotStuffingRequested() const { return true; }
    QString requestedSaslMethod() const { return saslMethod; }
    TLSRequestState tlsRequested() const { return useTLS ? ForceTLS : ForceNoTLS; }

    void socketConnected()
    {
      kDebug();
      if ( destination.protocol() == QLatin1String( "smtps" ) ) {
        if ( !startSsl() ) {
          error( KIO::ERR_SLAVE_DEFINED, i18n( "SSL negotiation failed." ) );
        }
      }
    }

    void socketDisconnected()
    {
      kDebug();
      emit q->result( q );
      q->deleteLater();
    }

    void socketError( KTcpSocket::Error err )
    {
      kDebug() << err;
      error( KIO::ERR_CONNECTION_BROKEN, socket->errorString() );

      if ( socket->state() != KTcpSocket::ConnectedState ) {
        // we have been disconnected by the error condition already, so just signal error result
        emit q->result( q );
        q->deleteLater();
      }
    }

    bool sendCommandLine( const QByteArray &cmdline )
    {
      if ( cmdline.length() < 4096 ) {
        kDebug( 7112 ) << "C: >>" << cmdline.trimmed().data() << "<<";
      } else {
        kDebug( 7112 ) << "C: <" << cmdline.length() << " bytes>";
      }
      ssize_t numWritten, cmdline_len = cmdline.length();
      if ( ( numWritten = socket->write( cmdline ) ) != cmdline_len ) {
        kDebug( 7112 ) << "Tried to write " << cmdline_len << " bytes, but only "
                     << numWritten << " were written!" << endl;
        error( KIO::ERR_SLAVE_DEFINED, i18n ( "Writing to socket failed." ) );
        return false;
      }
      return true;
    }

    bool run( int type, TransactionState * ts = 0 )
    {
      return run( Command::createSimpleCommand( type, this ), ts );
    }

    bool run( Command *cmd, TransactionState *ts = 0 )
    {
      Q_ASSERT( cmd );
      Q_ASSERT( !currentCommand );
      Q_ASSERT( !currentTransactionState || currentTransactionState == ts );

      // ### WTF?
      if ( cmd->doNotExecute( ts ) ) {
        return true;
      }

      currentCommand = cmd;
      currentTransactionState = ts;

      while ( !cmd->isComplete() && !cmd->needsResponse() ) {
        const QByteArray cmdLine = cmd->nextCommandLine( ts );
        if ( ts && ts->failedFatally() ) {
          q->disconnectFromHost( false );
          return false;
        }
        if ( cmdLine.isEmpty() ) {
          continue;
        }
        if ( !sendCommandLine( cmdLine ) ) {
          q->disconnectFromHost( false );
          return false;
        }
      }
      return true;
    }

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

    void queueCommand( KioSMTP::Command * command )
    {
      mPendingCommandQueue.enqueue( command );
    }

    bool runQueuedCommands( TransactionState *ts )
    {
      Q_ASSERT( ts );
      Q_ASSERT( !currentTransactionState || ts == currentTransactionState );
      currentTransactionState = ts;
      kDebug( canPipelineCommands(), 7112 ) << "using pipelining";

      while ( !mPendingCommandQueue.isEmpty() ) {
        QByteArray cmdline = collectPipelineCommands( ts );
        if ( ts->failedFatally() ) {
          q->disconnectFromHost( false );
          return false;
        }
        if ( ts->failed() ) {
          break;
        }
        if ( cmdline.isEmpty() ) {
          continue;
        }
        if ( !sendCommandLine( cmdline ) || ts->failedFatally() ) {
          q->disconnectFromHost( false );
          return false;
        }
        if ( !mSentCommandQueue.isEmpty() ) {
          return true; // wait for responses
        }
      }

      if ( ts->failed() ) {
        kDebug() << "transaction state failed: " << ts->errorCode() << ts->errorMessage();
        if ( errorMessage.isEmpty() ) {
          errorMessage = ts->errorMessage();
        }
        state = SmtpSessionPrivate::Reset;
        if ( !run( Command::RSET, currentTransactionState ) ) {
          q->disconnectFromHost( false );
        }
        return false;
      }

      delete currentTransactionState;
      currentTransactionState = 0;
      return true;
    }

    QByteArray collectPipelineCommands( TransactionState *ts )
    {
      Q_ASSERT( ts );
      QByteArray cmdLine;
      unsigned int cmdLine_len = 0;

      while ( !mPendingCommandQueue.isEmpty() ) {

        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 QByteArray currentCmdLine = cmd->nextCommandLine( ts );
          if ( ts->failedFatally() ) {
            return cmdLine;
          }
          const unsigned int currentCmdLine_len = currentCmdLine.length();

          cmdLine_len += currentCmdLine_len;
          cmdLine += currentCmdLine;

          // If we are executing the transfer command, don't collect the whole
          // command line (which may be several MBs) before sending it, but instead
          // send the data each time we have collected 32 KB of the command line.
          //
          // This way, the progress information in clients like KMail works correctly,
          // because otherwise, the TransferCommand would read the whole data from the
          // job at once, then sending it. The progress update on the client however
          // happens when sending data to the job, not when this slave writes the data
          // to the socket. Therefore that progress update is incorrect.
          //
          // 32 KB seems to be a sensible limit. Additionally, a job can only transfer
          // 32 KB at once anyway.
          if ( dynamic_cast<TransferCommand *>( cmd ) != 0 &&
              cmdLine_len >= 32 * 1024 ) {
            return cmdLine;
          }
        }

        mSentCommandQueue.enqueue( mPendingCommandQueue.dequeue() );

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

      return cmdLine;
    }

    void receivedNewData()
    {
      kDebug();
      while ( socket->canReadLine() ) {
        const QByteArray buffer = socket->readLine();
        kDebug() << "S: >>" << buffer << "<<";
        currentResponse.parseLine( buffer, buffer.size() );
        // ...until the response is complete or the parser is so confused
        // that it doesn't think a RSET would help anymore:
        if ( currentResponse.isComplete() ) {
          handleResponse( currentResponse );
          currentResponse = Response();
        } else if ( !currentResponse.isWellFormed() ) {
          error( KIO::ERR_NO_CONTENT,
                 i18n( "Invalid SMTP response (%1) received.", currentResponse.code() ) );
        }
      }
    }

    void handleResponse( const KioSMTP::Response &response )
    {
      if ( !mSentCommandQueue.isEmpty() ) {
        Command *cmd = mSentCommandQueue.head();
        Q_ASSERT( cmd->isComplete() );
        cmd->processResponse( response, currentTransactionState );
        if ( currentTransactionState->failedFatally() ) {
          q->disconnectFromHost( false );
        }
        delete mSentCommandQueue.dequeue();

        if ( mSentCommandQueue.isEmpty() ) {
          if ( !mPendingCommandQueue.isEmpty() ) {
            runQueuedCommands( currentTransactionState );
          } else if ( state == Sending ) {
            delete currentTransactionState;
            currentTransactionState = 0;
            q->disconnectFromHost(); // we are done
          }
        }
        return;
      }

      if ( currentCommand ) {
        if ( !currentCommand->processResponse( response, currentTransactionState ) ) {
          q->disconnectFromHost( false );
        }
        while ( !currentCommand->isComplete() && !currentCommand->needsResponse() ) {
          const QByteArray cmdLine = currentCommand->nextCommandLine( currentTransactionState );
          if ( currentTransactionState && currentTransactionState->failedFatally() ) {
            q->disconnectFromHost( false );
          }
          if ( cmdLine.isEmpty() ) {
            continue;
          }
          if ( !sendCommandLine( cmdLine ) ) {
            q->disconnectFromHost( false );
          }
        }
        if ( currentCommand->isComplete() ) {
          Command *cmd = currentCommand;
          currentCommand = 0;
          currentTransactionState = 0;
          handleCommand( cmd );
        }
        return;
      }

      // command-less responses
      switch ( state ) {
      case Initial: // server greeting
      {
        if ( !response.isOk() ) {
          error( KIO::ERR_COULD_NOT_LOGIN,
                 i18n( "The server (%1) did not accept the connection.\n%2",
                       destination.host(), response.errorMessage() ) );
          break;
        }
        state = EHLOPreTls;
        EHLOCommand *ehloCmdPreTLS = new EHLOCommand( this, myHostname );
        run( ehloCmdPreTLS );
        break;
      }
      default: error( KIO::ERR_SLAVE_DEFINED, i18n( "Unhandled response" ) );
      }
    }

    void handleCommand( Command *cmd )
    {
      switch ( state ) {
      case StartTLS:
      {
        // re-issue EHLO to refresh the capability list (could be have
        // been faked before TLS was enabled):
        state = EHLOPostTls;
        EHLOCommand *ehloCmdPostTLS = new EHLOCommand( this, myHostname );
        run( ehloCmdPostTLS );
        break;
      }
      case EHLOPreTls:
      {
        if ( ( haveCapability( "STARTTLS" ) &&
               tlsRequested() != SMTPSessionInterface::ForceNoTLS ) ||
             tlsRequested() == SMTPSessionInterface::ForceTLS )
        {
          state = StartTLS;
          run( Command::STARTTLS );
          break;
        }
      }
      // fall through
      case EHLOPostTls:
      {
        // 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 ( !destination.user().isEmpty() ||
             haveCapability( "AUTH" ) ||
             !requestedSaslMethod().isEmpty() ) {
          authInfo.username = destination.user();
          authInfo.password = destination.password();
          authInfo.prompt = i18n( "Username and password for your SMTP account:" );

          QStringList strList;
          if ( !requestedSaslMethod().isEmpty() ) {
            strList.append( requestedSaslMethod() );
          } else {
            strList = capabilities().saslMethodsQSL();
          }

          state = Authenticated;
          AuthCommand *authCmd =
            new AuthCommand( this, strList.join( QLatin1String( " " ) ).toLatin1(),
                             destination.host(), authInfo );
          run( authCmd );
          break;
        }
      }
      // fall through
      case Authenticated:
      {
        state = Sending;
        queueCommand( new MailFromCommand( this, request.fromAddress().toLatin1(),
                                           request.is8BitBody(), request.size() ) );
        // Loop through our To and CC recipients, and send the proper
        // SMTP commands, for the benefit of the server.
        const QStringList recipients = request.recipients();
        for ( QStringList::const_iterator it = recipients.begin(); it != recipients.end(); ++it ) {
          queueCommand( new RcptToCommand( this, ( *it ).toLatin1() ) );
        }

        queueCommand( Command::DATA );
        queueCommand( new TransferCommand( this, QByteArray() ) );

        TransactionState *ts = new TransactionState;
        if ( !runQueuedCommands( ts ) ) {
          if ( ts->errorCode() ) {
            error( ts->errorCode(), ts->errorMessage() );
          }
        }
        break;
      }
      case Reset:
        q->disconnectFromHost( true );
        break;
      default:
        error( KIO::ERR_SLAVE_DEFINED, i18n( "Unhandled command response." ) );
      }

      delete cmd;
    }

  public:
    QString saslMethod;
    bool useTLS;

    KUrl destination;
    KTcpSocket *socket;
    QIODevice *data;
    KioSMTP::Response currentResponse;
    KioSMTP::Command * currentCommand;
    KioSMTP::TransactionState *currentTransactionState;
    KIO::AuthInfo authInfo;
    KioSMTP::Request request;
    QString errorMessage;
    QString myHostname;

    enum State {
      Initial,
      EHLOPreTls,
      StartTLS,
      EHLOPostTls,
      Authenticated,
      Sending,
      Reset
    };
    State state;

    typedef QQueue<KioSMTP::Command*> CommandQueue;
    CommandQueue mPendingCommandQueue;
    CommandQueue mSentCommandQueue;

    static bool saslInitialized;

  private:
    SmtpSession *q;
};

bool SmtpSessionPrivate::saslInitialized = false;

SmtpSession::SmtpSession( QObject *parent ) :
  QObject( parent ),
  d( new SmtpSessionPrivate( this ) )
{
  kDebug();
  d->socket = new KTcpSocket( this );
  connect( d->socket, SIGNAL(connected()), SLOT(socketConnected()) );
  connect( d->socket, SIGNAL(disconnected()), SLOT(socketDisconnected()) );
  connect( d->socket, SIGNAL(error(KTcpSocket::Error)), SLOT(socketError(KTcpSocket::Error)) );
  connect( d->socket, SIGNAL(readyRead()), SLOT(receivedNewData()), Qt::QueuedConnection );

  if ( !d->saslInitialized ) {
    if ( !initSASL() ) {
      exit( -1 );
    }
    d->saslInitialized = true;
  }
}

SmtpSession::~SmtpSession()
{
  kDebug();
  delete d;
}

void SmtpSession::setSaslMethod( const QString &method )
{
  d->saslMethod = method;
}

void SmtpSession::setUseTLS( bool useTLS )
{
  d->useTLS = useTLS;
}

void SmtpSession::connectToHost( const KUrl &url )
{
  kDebug() << url;
  d->socket->connectToHost( url.host(), url.port() );
}

void SmtpSession::disconnectFromHost( bool nice )
{
  if ( d->socket->state() == KTcpSocket::ConnectedState ) {
    if ( nice ) {
      d->run( Command::QUIT );
    }

    d->socket->disconnectFromHost();

    d->clearCapabilities();
    qDeleteAll( d->mPendingCommandQueue );
    d->mPendingCommandQueue.clear();
    qDeleteAll( d->mSentCommandQueue );
    d->mSentCommandQueue.clear();
  }
}

void SmtpSession::sendMessage( const KUrl &destination, QIODevice *data )
{
  d->destination = destination;
  if ( d->socket->state() != KTcpSocket::ConnectedState &&
       d->socket->state() != KTcpSocket::ConnectingState ) {
    connectToHost( destination );
  }

  d->data = data;
  d->request = Request::fromURL( destination ); // parse settings from URL's query

  if ( !d->request.heloHostname().isEmpty() ) {
    d->myHostname = d->request.heloHostname();
  } else {
    d->myHostname = QHostInfo::localHostName();
    if ( d->myHostname.isEmpty() ) {
      d->myHostname = QLatin1String( "localhost.invalid" );
    } else if ( !d->myHostname.contains( QLatin1Char( '.' ) ) ) {
      d->myHostname += QLatin1String( ".localnet" );
    }
  }
}

QString SmtpSession::errorMessage() const
{
  return d->errorMessage;
}

#include "moc_smtpsession.cpp"
