/*
    Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
    Author: Volker Krause <volker.krause@kdab.com>

    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.
*/

//krazy:excludeall=null since used by SASL (C library)

#include "session.h"
#include "response.h"
#include "sievejob_p.h"

#include <kdebug.h>
#include <ktcpsocket.h>
#include <kio/sslui.h>
#include <kio/authinfo.h>
#include <KLocalizedString>
#include <KPasswordDialog>
#include <KMessageBox>

static sasl_callback_t callbacks[] = {
    { SASL_CB_ECHOPROMPT, NULL, NULL },
    { SASL_CB_NOECHOPROMPT, NULL, NULL },
    { SASL_CB_GETREALM, NULL, NULL },
    { SASL_CB_USER, NULL, NULL },
    { SASL_CB_AUTHNAME, NULL, NULL },
    { SASL_CB_PASS, NULL, NULL },
    { SASL_CB_CANON_USER, NULL, NULL },
    { SASL_CB_LIST_END, NULL, NULL }
};

using namespace KManageSieve;

Session::Session( QObject *parent ) :
  QObject( parent ),
  m_socket( new KTcpSocket( this ) ),
  m_sasl_conn( 0 ),
  m_sasl_client_interact( 0 ),
  m_currentJob( 0 ),
  m_state( None ),
  m_pendingQuantity( -1 ),
  m_supportsStartTls( false )
{
  kDebug();
  connect( m_socket, SIGNAL(readyRead()), SLOT(dataReceived()) );
  connect( m_socket, SIGNAL(error(KTcpSocket::Error)), SLOT(socketError()) );
  connect( m_socket, SIGNAL(disconnected()), SLOT(socketError()) );

  static bool saslInitialized = false;
  if ( !saslInitialized ) {
    initSASL();
    saslInitialized = true;
  }
}

Session::~Session()
{
  kDebug();
  disconnectFromHost( false );
  delete m_socket;
}

void Session::connectToHost( const KUrl &url )
{
  kDebug() << url;
  if ( m_socket->state() == KTcpSocket::ConnectedState || m_socket->state() == KTcpSocket::ConnectingState )
    return;

  m_url = url;
  m_socket->connectToHost( url.host(), url.port() ? url.port() : 4190 );
  m_state = PreTlsCapabilities;
}

void Session::disconnectFromHost( bool sendLogout )
{
  if ( sendLogout )
    sendData( "LOGOUT" );
  m_socket->disconnectFromHost();
  if ( m_currentJob )
    killJob( m_currentJob );
  foreach ( SieveJob* job, m_jobs )
    killJob( job );
  deleteLater();
}

void Session::dataReceived()
{
  if ( m_pendingQuantity > 0 ) {
    const QByteArray buffer = m_socket->read( qMin( m_pendingQuantity, m_socket->bytesAvailable() ) );
    m_data += buffer;
    m_pendingQuantity -= buffer.size();
    if ( m_pendingQuantity <= 0 ) {
      kDebug() << "S: " << m_data.trimmed();
      processResponse( m_lastResponse, m_data );
    }
  }

  while ( m_socket->canReadLine() ) {
    QByteArray line = m_socket->readLine();
    if ( line.endsWith( "\r\n" ) ) { //krazy:exclude=strings
      line.chop( 2 );
    }
    if ( line.isEmpty() ) {
      continue; // ignore CRLF after data blocks
    }
    kDebug() << "S: " << line;
    Response r;
    if ( !r.parseResponse( line ) ) {
      kDebug() << "protocol violation!";
      disconnectFromHost( false );
    }
    kDebug() << r.type() << r.key() << r.value() << r.extra() << r.quantity();

    m_lastResponse = r;
    if ( r.type() == Response::Quantity ) {
      m_data.clear();
      m_pendingQuantity = r.quantity();
      dataReceived(); // in case the data block is already completely in the buffer
      return;
    } else if ( r.operationResult() == Response::Bye ) {
      disconnectFromHost( false );
      return;
    }
    processResponse( r, QByteArray() );
  }
}

void Session::feedBack(const QByteArray& data)
{
  Response response;
  if ( !response.parseResponse( data ) ) {
    m_errorMsg = KIO::buildErrorString( KIO::ERR_UNKNOWN, i18n( "Syntax error." ) );
    disconnectFromHost();
    return;
  }
  m_lastResponse = response;

  if ( response.type() == Response::Quantity ) {
    m_data.clear();
    m_pendingQuantity = response.quantity();
    dataReceived();
    return;
  } else {
    processResponse( response, QByteArray() );
  }
}

void Session::processResponse(const KManageSieve::Response& response, const QByteArray& data)
{
  switch ( m_state ) {
    // should probably be refactored into a capability job
    case PreTlsCapabilities:
    case PostTlsCapabilities:
      if ( response.type() == Response::Action ) {
        if ( response.operationSuccessful() ) {
          kDebug() << "Sieve server ready & awaiting authentication.";
          if ( m_state == PreTlsCapabilities ) {
            if ( !allowUnencrypted() && !QSslSocket::supportsSsl() ) {
              m_errorMsg = KIO::buildErrorString( KIO::ERR_SLAVE_DEFINED, i18n("Can not use TLS since the underlying Qt library does not support it.") );
              disconnectFromHost();
              return;
            }
            if ( !allowUnencrypted() && QSslSocket::supportsSsl() && !m_supportsStartTls &&
                  KMessageBox::warningContinueCancel( 0,
                            i18n("TLS encryption was requested, but your Sieve server does not advertise TLS in its capabilities.\n"
                                  "You can choose to try to initiate TLS negotiations nonetheless, or cancel the operation."),
                                                      i18n("Server Does Not Advertise TLS"), KGuiItem(i18n("&Start TLS nonetheless")), KStandardGuiItem::cancel(),
                                                      QString::fromLatin1( "ask_starttls_%1" ).arg(m_url.host() )  ) != KMessageBox::Continue )
            {
              m_errorMsg = KIO::buildErrorString( KIO::ERR_USER_CANCELED, i18n("TLS encryption requested, but not supported by server.") );
              disconnectFromHost();
              return;
            }

            if ( m_supportsStartTls && QSslSocket::supportsSsl() ) {
              m_state = StartTls;
              sendData( "STARTTLS" );
            } else {
              m_state = Authenticating;
              startAuthentication();
            }
          } else {
            m_state = Authenticating;
            startAuthentication();
          }
        } else {
          kDebug() << "Unknown action " << response.action() << ".";
        }
      } else if ( response.key() == "IMPLEMENTATION" ) {
        m_implementation = QString::fromLatin1( response.value() );
        kDebug() << "Connected to Sieve server: " << response.value();
      } else if ( response.key() == "SASL") {
        m_saslMethods = QString::fromLatin1( response.value() ).split( ' ', QString::SkipEmptyParts );
        kDebug() << "Server SASL authentication methods: " << m_saslMethods;
      } else if ( response.key() == "SIEVE" ) {
        // Save script capabilities
        m_sieveExtensions = QString::fromLatin1( response.value() ).split( ' ', QString::SkipEmptyParts );
        kDebug() << "Server script capabilities: " << m_sieveExtensions;
      } else if (response.key() == "STARTTLS") {
        kDebug() << "Server supports TLS";
        m_supportsStartTls = true;
      } else {
        kDebug() << "Unrecognised key " << response.key();
      }
      break;
    case StartTls:
      if ( response.operationSuccessful() ) {
        QMetaObject::invokeMethod( this, "startSsl", Qt::QueuedConnection ); // queued to avoid deadlock with waitForEncrypted
        m_state = None;
      } else {
        m_errorMsg = KIO::buildErrorString( KIO::ERR_SLAVE_DEFINED, i18n("The server does not seem to support TLS. Disable TLS if you want to connect without encryption.") );
        disconnectFromHost();
      }
      break;
    case Authenticating:
      if ( response.operationResult() == Response::Other ) {
        if ( !saslClientStep( data ) ) {
          m_errorMsg = KIO::buildErrorString( KIO::ERR_COULD_NOT_AUTHENTICATE, QString::fromUtf8( sasl_errdetail( m_sasl_conn ) ) );
          disconnectFromHost();
          return;
        }
      } else {
        m_state = None;
        sasl_dispose( &m_sasl_conn );
        if ( response.operationSuccessful() ) {
          kDebug() << "Authentication complete.";
          QMetaObject::invokeMethod( this, "executeNextJob", Qt::QueuedConnection );
        } else {
          KIO::buildErrorString( KIO::ERR_COULD_NOT_AUTHENTICATE, i18n("Authentication failed.\nMost likely the password is wrong.\nThe server responded:\n%1", QString::fromLatin1( response.action() ) ) );
          disconnectFromHost();
          return;
        }
      }
      break;
    default:
      if ( m_currentJob ) {
        if ( m_currentJob->d->handleResponse( response, data ) ) {
          m_currentJob = 0;
          QMetaObject::invokeMethod( this, "executeNextJob", Qt::QueuedConnection );
        }
        break;
      } else {
        // we can get here in the kill current job case
        if ( response.operationResult() != Response::Other ) {
          QMetaObject::invokeMethod( this, "executeNextJob", Qt::QueuedConnection );
          return;
        }
      }
      kDebug() << "Unhandled response!";
  }
}

void Session::socketError()
{
  kDebug() << m_socket->errorString();
  disconnectFromHost( false );
}

void Session::scheduleJob(SieveJob* job)
{
  kDebug() << job;
  m_jobs.enqueue( job );
  QMetaObject::invokeMethod( this, "executeNextJob", Qt::QueuedConnection );
}

void Session::killJob(SieveJob* job)
{
  kDebug() << job;
  if ( m_currentJob == job ) {
    m_currentJob->d->killed();
    m_currentJob = 0;
  } else {
    m_jobs.removeAll( job );
    job->d->killed();
  }
}

void Session::executeNextJob()
{
  if ( m_socket->state() != KTcpSocket::ConnectedState || m_state != None || m_currentJob || m_jobs.isEmpty() )
    return;
  m_currentJob = m_jobs.dequeue();
  m_currentJob->d->run( this );
}

QStringList Session::sieveExtensions() const
{
  return m_sieveExtensions;
}

bool Session::requestCapabilitiesAfterStartTls() const
{
  // Cyrus didn't send CAPABILITIES after STARTTLS until 2.3.11, which is
  // not standard conform, but we need to support that anyway.
  // m_implementation looks like this 'Cyrus timsieved v2.2.12' for Cyrus btw.
  QRegExp regExp( "Cyrus\\stimsieved\\sv(\\d+)\\.(\\d+)\\.(\\d+)([-\\w]*)", Qt::CaseInsensitive );
  if ( regExp.indexIn( m_implementation ) >= 0 ) {
    const int major = regExp.cap( 1 ).toInt();
    const int minor = regExp.cap( 2 ).toInt();
    const int patch = regExp.cap( 3 ).toInt();
    const QString vendor = regExp.cap( 4 );
    if ( major < 2 || (major == 2 && (minor < 3 || (minor == 3 && patch < 11))) || (vendor == "-kolab-nocaps") ) {
      kDebug() << "Enabling compat mode for Cyrus < 2.3.11 or Cyrus marked as \"kolab-nocaps\"";
      return true;
    }
  }
  return false;
}

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

  const KSslCipher cipher = m_socket->sessionCipher();
  if ( !encrypted || m_socket->sslErrors().count() > 0 || m_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:" <<  m_socket->errorString()
              << "and the list of SSL errors contains"
              << m_socket->sslErrors().count() << "items.";

    if ( !KIO::SslUi::askIgnoreSslErrors( m_socket ) ) {
      disconnectFromHost();
      return;
    }
  }
  kDebug() << "TLS negotiation done.";
  if ( requestCapabilitiesAfterStartTls() )
    sendData( "CAPABILITY" );
  m_state = PostTlsCapabilities;
}

void Session::sendData(const QByteArray& data)
{
  kDebug() << "C: " << data;
  m_socket->write( data );
  m_socket->write( "\r\n" );
}

void Session::startAuthentication()
{
  int result;
  m_sasl_conn = NULL;
  m_sasl_client_interact = NULL;
  const char *out = NULL;
  uint outlen;
  const char *mechusing = NULL;

  result = sasl_client_new( "sieve", m_url.host().toLatin1(), 0, 0, callbacks, 0, &m_sasl_conn );
  if ( result != SASL_OK ) {
    m_errorMsg = KIO::buildErrorString( KIO::ERR_COULD_NOT_AUTHENTICATE, QString::fromUtf8( sasl_errdetail( m_sasl_conn ) ) );
    disconnectFromHost();
    return;
  }

  do {
    result = sasl_client_start( m_sasl_conn, requestedSaslMethod().join(" ").toLatin1(), &m_sasl_client_interact, &out, &outlen, &mechusing);
    if ( result == SASL_INTERACT ) {
      if ( !saslInteract( m_sasl_client_interact ) ) {
        m_errorMsg = KIO::buildErrorString( KIO::ERR_COULD_NOT_AUTHENTICATE, QString::fromUtf8( sasl_errdetail( m_sasl_conn ) ) );
        sasl_dispose( &m_sasl_conn );
        disconnectFromHost();
        return;
      }
    }
  } while ( result == SASL_INTERACT );

  if ( result != SASL_CONTINUE && result != SASL_OK ) {
    m_errorMsg = KIO::buildErrorString( KIO::ERR_COULD_NOT_AUTHENTICATE, QString::fromUtf8( sasl_errdetail( m_sasl_conn ) ) );
    sasl_dispose( &m_sasl_conn );
    disconnectFromHost();
    return;
  }

  kDebug() << "Preferred authentication method is " << mechusing << ".";

  QByteArray authCommand = "AUTHENTICATE \"" + QByteArray(mechusing) + QByteArray("\"");
  const QByteArray challenge = QByteArray::fromRawData( out, outlen ).toBase64();
  if ( !challenge.isEmpty() ) {
    authCommand += " \"";
    authCommand += challenge;
    authCommand += '\"';
  }
  sendData( authCommand );
}

QStringList Session::requestedSaslMethod() const
{
  const QString m = m_url.queryItem( "x-mech" );
  if ( !m.isEmpty() )
    return QStringList( m );
  return m_saslMethods;
}

bool Session::saslInteract(void* in)
{
  kDebug();
  sasl_interact_t *interact = ( sasl_interact_t * ) in;

  KIO::AuthInfo ai;
  ai.url = m_url;
  ai.username = m_url.userName();
  ai.password = m_url.password();
  ai.keepPassword = true;
  ai.caption = i18n("Sieve Authentication Details");
  ai.comment = i18n("Please enter your authentication details for your sieve account "
                    "(usually the same as your email password):");

  //some mechanisms do not require username && pass, so it doesn't need a popup
  //window for getting this info
  for ( ; interact->id != SASL_CB_LIST_END; interact++ ) {
    if ( interact->id == SASL_CB_AUTHNAME || interact->id == SASL_CB_PASS ) {
      if ( ai.username.isEmpty() || ai.password.isEmpty()) {

        QPointer<KPasswordDialog> dlg =
          new KPasswordDialog(
            0,
            KPasswordDialog::ShowUsernameLine | KPasswordDialog::ShowKeepPassword
            );
        dlg->setUsername( ai.username );
        dlg->setPassword( ai.password );
        dlg->setKeepPassword( ai.keepPassword );
        dlg->setPrompt( ai.prompt );
        dlg->setUsernameReadOnly( ai.readOnly );
        dlg->setCaption( ai.caption );
        dlg->addCommentLine( ai.commentLabel, ai.comment );

        bool gotIt = false;
        if ( dlg->exec() ) {
          m_url.setUserName( dlg->username() );
          m_url.setPassword( dlg->password() );
          gotIt = true;
        }
        delete dlg;
        if ( !gotIt ) {
          return false;
        }
      }
      break;
    }
  }

  interact = ( sasl_interact_t * ) in;
  while( interact->id != SASL_CB_LIST_END ) {
    kDebug() << "SASL_INTERACT id: " << interact->id;
    switch( interact->id ) {
      case SASL_CB_USER:
      case SASL_CB_AUTHNAME:
        kDebug() << "SASL_CB_[AUTHNAME|USER]: '" << m_url.userName() << "'";
        interact->result = strdup( m_url.userName().toUtf8() );
        if (interact->result )
          interact->len = strlen( (const char *) interact->result );
        else
          interact->len = 0;
        break;
      case SASL_CB_PASS:
        kDebug() << "SASL_CB_PASS: [hidden] ";
        interact->result = strdup( m_url.password().toUtf8() );
        if (interact->result )
          interact->len = strlen( (const char *) interact->result );
        else 
          interact->len = 0;
        break;
      default:
        interact->result = NULL; 
        interact->len = 0;
        break;
    }
    interact++;
  }
  return true;
}

bool Session::saslClientStep(const QByteArray& challenge)
{
  int result;
  const char *out = NULL;
  uint outlen;

  const QByteArray challenge_decoded = QByteArray::fromBase64( challenge );
  do {
    result =
      sasl_client_step( m_sasl_conn,
                        challenge_decoded.isEmpty() ? 0 : challenge_decoded.data(),
                        challenge_decoded.size(),
                        &m_sasl_client_interact,
                        &out, &outlen );
    if ( result == SASL_INTERACT ) {
      if ( !saslInteract( m_sasl_client_interact ) ) {
        sasl_dispose( &m_sasl_conn );
        return false;
      }
    }
  } while ( result == SASL_INTERACT );

  kDebug() << "sasl_client_step: " << result;
  if ( result != SASL_CONTINUE && result != SASL_OK ) {
    kDebug() << "sasl_client_step failed with: " << result << QString::fromUtf8( sasl_errdetail( m_sasl_conn ) );
    sasl_dispose( &m_sasl_conn );
    return false;
  }

  sendData('\"' + QByteArray::fromRawData( out, outlen ).toBase64() + '\"');
  return true;
}

QString Session::errorMessage() const
{
  return m_errorMsg;
}

void Session::setErrorMessage(const QString& msg)
{
  m_errorMsg = msg;
}

bool Session::allowUnencrypted() const
{
  return m_url.queryItem("x-allow-unencrypted") == "true";
}

#include "session.moc"
