/* Copyright (c) 2009 Kevin Ottens Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Kevin Ottens 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 "session.h" #include "session_p.h" #include "sessionuiproxy.h" #include #include #include #include #include "job.h" #include "loginjob.h" #include "message_p.h" #include "sessionlogger_p.h" #include "sessionthread_p.h" #include "rfccodecs.h" Q_DECLARE_METATYPE( KTcpSocket::SslVersion ) Q_DECLARE_METATYPE( QSslSocket::SslMode ) static const int _kimap_sslVersionId = qRegisterMetaType(); using namespace KIMAP; Session::Session( const QString &hostName, quint16 port, QObject *parent) : QObject( parent ), d( new SessionPrivate( this ) ) { if ( !qgetenv( "KIMAP_LOGFILE" ).isEmpty() ) { d->logger = new SessionLogger; } d->isSocketConnected = false; d->state = Disconnected; d->jobRunning = false; d->thread = new SessionThread( hostName, port ); connect( d->thread, SIGNAL(encryptionNegotiationResult(bool,KTcpSocket::SslVersion)), d, SLOT(onEncryptionNegotiationResult(bool,KTcpSocket::SslVersion)) ); connect( d->thread, SIGNAL(sslError(KSslErrorUiData)), d, SLOT(handleSslError(KSslErrorUiData)) ); connect( d->thread, SIGNAL(socketDisconnected()), d, SLOT(socketDisconnected()) ); connect( d->thread, SIGNAL(responseReceived(KIMAP::Message)), d, SLOT(responseReceived(KIMAP::Message)) ); connect( d->thread, SIGNAL(socketConnected()), d, SLOT(socketConnected()) ); connect( d->thread, SIGNAL(socketActivity()), d, SLOT(socketActivity()) ); connect( d->thread, SIGNAL(socketError(KTcpSocket::Error)), d, SLOT(socketError(KTcpSocket::Error)) ); d->socketTimer.setSingleShot( true ); connect( &d->socketTimer, SIGNAL(timeout()), d, SLOT(onSocketTimeout()) ); d->startSocketTimer(); } Session::~Session() { delete d->thread; d->thread = 0; } void Session::setUiProxy(SessionUiProxy::Ptr proxy) { d->uiProxy = proxy; } void Session::setUiProxy(SessionUiProxy *proxy) { setUiProxy( SessionUiProxy::Ptr( proxy ) ); } QString Session::hostName() const { return d->thread->hostName(); } quint16 Session::port() const { return d->thread->port(); } Session::State Session::state() const { return d->state; } QString Session::userName() const { return d->userName; } QByteArray Session::serverGreeting() const { return d->greeting; } int Session::jobQueueSize() const { return d->queue.size() + ( d->jobRunning ? 1 : 0 ); } void KIMAP::Session::close() { d->thread->closeSocket(); } void SessionPrivate::handleSslError(const KSslErrorUiData& errorData) { const bool ignoreSslError = uiProxy && uiProxy->ignoreSslError( errorData ); //ignoreSslError is async, so the thread might already be gone when it returns if ( thread ) { thread->sslErrorHandlerResponse(ignoreSslError); } } SessionPrivate::SessionPrivate( Session *session ) : QObject( session ), q( session ), state( Session::Disconnected ), logger( 0 ), currentJob( 0 ), tagCount( 0 ), sslVersion( KTcpSocket::UnknownSslVersion ), socketTimerInterval( 30000 ) // By default timeouts on 30s { } SessionPrivate::~SessionPrivate() { delete logger; } void SessionPrivate::addJob(Job *job) { queue.append( job ); emit q->jobQueueSizeChanged( q->jobQueueSize() ); QObject::connect( job, SIGNAL(result(KJob*)), this, SLOT(jobDone(KJob*)) ); QObject::connect( job, SIGNAL(destroyed(QObject*)), this, SLOT(jobDestroyed(QObject*)) ); if ( state != Session::Disconnected ) { startNext(); } } void SessionPrivate::startNext() { QMetaObject::invokeMethod( this, "doStartNext" ); } void SessionPrivate::doStartNext() { if ( queue.isEmpty() || jobRunning || !isSocketConnected ) { return; } restartSocketTimer(); jobRunning = true; currentJob = queue.dequeue(); currentJob->doStart(); } void SessionPrivate::jobDone( KJob *job ) { Q_UNUSED( job ); Q_ASSERT( job == currentJob ); stopSocketTimer(); jobRunning = false; currentJob = 0; emit q->jobQueueSizeChanged( q->jobQueueSize() ); startNext(); } void SessionPrivate::jobDestroyed( QObject *job ) { queue.removeAll( static_cast( job ) ); if ( currentJob == job ) { currentJob = 0; } } void SessionPrivate::responseReceived( const Message &response ) { if ( logger && ( state == Session::Authenticated || state == Session::Selected ) ) { logger->dataReceived( response.toString() ); } QByteArray tag; QByteArray code; if ( response.content.size()>=1 ) { tag = response.content[0].toString(); } if ( response.content.size()>=2 ) { code = response.content[1].toString(); } // BYE may arrive as part of a LOGOUT sequence or before the server closes the connection after an error. // In any case we should wait until the server closes the connection, so we don't have to do anything. if ( code == "BYE" ) { Message simplified = response; if ( simplified.content.size() >= 2 ) { simplified.content.removeFirst(); // Strip the tag simplified.content.removeFirst(); // Strip the code } kDebug() << "Received BYE: " << simplified.toString(); return; } switch ( state ) { case Session::Disconnected: if ( socketTimer.isActive() ) { stopSocketTimer(); } if ( code == "OK" ) { setState( Session::NotAuthenticated ); Message simplified = response; simplified.content.removeFirst(); // Strip the tag simplified.content.removeFirst(); // Strip the code greeting = simplified.toString().trimmed(); // Save the server greeting startNext(); } else if ( code == "PREAUTH" ) { setState( Session::Authenticated ); Message simplified = response; simplified.content.removeFirst(); // Strip the tag simplified.content.removeFirst(); // Strip the code greeting = simplified.toString().trimmed(); // Save the server greeting startNext(); } else { thread->closeSocket(); } return; case Session::NotAuthenticated: if ( code == "OK" && tag == authTag ) { setState( Session::Authenticated ); } break; case Session::Authenticated: if ( code == "OK" && tag == selectTag ) { setState( Session::Selected ); currentMailBox = upcomingMailBox; } break; case Session::Selected: if ( ( code == "OK" && tag == closeTag ) || ( code != "OK" && tag == selectTag ) ) { setState( Session::Authenticated ); currentMailBox = QByteArray(); } else if ( code == "OK" && tag == selectTag ) { currentMailBox = upcomingMailBox; } break; } if ( tag == authTag ) { authTag.clear(); } if ( tag == selectTag ) { selectTag.clear(); } if ( tag == closeTag ) { closeTag.clear(); } // If a job is running forward it the response if ( currentJob != 0 ) { restartSocketTimer(); currentJob->handleResponse( response ); } else { qWarning() << "A message was received from the server with no job to handle it:" << response.toString() << '(' + response.toString().toHex() + ')'; } } void SessionPrivate::setState(Session::State s) { if ( s != state ) { Session::State oldState = state; state = s; emit q->stateChanged( state, oldState ); } } QByteArray SessionPrivate::sendCommand( const QByteArray &command, const QByteArray &args ) { QByteArray tag = 'A' + QByteArray::number( ++tagCount ).rightJustified( 6, '0' ); QByteArray payload = tag + ' ' + command; if ( !args.isEmpty() ) { payload += ' ' + args; } sendData( payload ); if ( command == "LOGIN" || command == "AUTHENTICATE" ) { authTag = tag; } else if ( command == "SELECT" || command == "EXAMINE" ) { selectTag = tag; upcomingMailBox = args; upcomingMailBox.remove( 0, 1 ); upcomingMailBox = upcomingMailBox.left( upcomingMailBox.indexOf( '\"') ); upcomingMailBox = KIMAP::decodeImapFolderName( upcomingMailBox ); } else if ( command == "CLOSE" ) { closeTag = tag; } return tag; } void SessionPrivate::sendData( const QByteArray &data ) { restartSocketTimer(); if ( logger && ( state == Session::Authenticated || state == Session::Selected ) ) { logger->dataSent( data ); } thread->sendData( data + "\r\n" ); } void SessionPrivate::socketConnected() { stopSocketTimer(); isSocketConnected = true; bool willUseSsl = false; if ( !queue.isEmpty() ) { KIMAP::LoginJob *login = qobject_cast( queue.first() ); if ( login ) { willUseSsl = ( login->encryptionMode() == KIMAP::LoginJob::SslV2 ) || ( login->encryptionMode() == KIMAP::LoginJob::SslV3 ) || ( login->encryptionMode() == KIMAP::LoginJob::SslV3_1 ) || ( login->encryptionMode() == KIMAP::LoginJob::AnySslVersion ); userName = login->userName(); } } if ( state == Session::Disconnected && willUseSsl ) { startNext(); } else { startSocketTimer(); } } void SessionPrivate::socketDisconnected() { if ( socketTimer.isActive() ) { stopSocketTimer(); } if ( logger && ( state == Session::Authenticated || state == Session::Selected ) ) { logger->disconnectionOccured(); } if ( state != Session::Disconnected ) { setState( Session::Disconnected ); emit q->connectionLost(); } else { emit q->connectionFailed(); } isSocketConnected = false; clearJobQueue(); } void SessionPrivate::socketActivity() { restartSocketTimer(); } void SessionPrivate::socketError(KTcpSocket::Error error) { if ( socketTimer.isActive() ) { stopSocketTimer(); } if ( currentJob ) { currentJob->setSocketError(error); } else if ( !queue.isEmpty() ) { currentJob = queue.takeFirst(); currentJob->setSocketError(error); } if ( isSocketConnected ) { thread->closeSocket(); } else { emit q->connectionFailed(); emit q->connectionLost(); // KDE5: Remove this. We shouldn't emit connectionLost() if we weren't connected in the first place clearJobQueue(); } } void SessionPrivate::clearJobQueue() { if ( currentJob ) { currentJob->connectionLost(); } else if ( !queue.isEmpty() ) { currentJob = queue.takeFirst(); currentJob->connectionLost(); } QQueue queueCopy = queue; // copy because jobDestroyed calls removeAll qDeleteAll(queueCopy); queue.clear(); emit q->jobQueueSizeChanged( 0 ); } void SessionPrivate::startSsl(const KTcpSocket::SslVersion &version) { thread->startSsl( version ); } QString Session::selectedMailBox() const { return QString::fromUtf8( d->currentMailBox ); } void SessionPrivate::onEncryptionNegotiationResult(bool isEncrypted, KTcpSocket::SslVersion version) { if ( isEncrypted ) { sslVersion = version; } else { sslVersion = KTcpSocket::UnknownSslVersion; } emit encryptionNegotiationResult( isEncrypted ); } KTcpSocket::SslVersion SessionPrivate::negotiatedEncryption() const { return sslVersion; } void SessionPrivate::setSocketTimeout( int ms ) { bool timerActive = socketTimer.isActive(); if ( timerActive ) { stopSocketTimer(); } socketTimerInterval = ms; if ( timerActive ) { startSocketTimer(); } } int SessionPrivate::socketTimeout() const { return socketTimerInterval; } void SessionPrivate::startSocketTimer() { if ( socketTimerInterval < 0 ) { return; } Q_ASSERT( !socketTimer.isActive() ); socketTimer.start( socketTimerInterval ); } void SessionPrivate::stopSocketTimer() { if ( socketTimerInterval < 0 ) { return; } socketTimer.stop(); } void SessionPrivate::restartSocketTimer() { if ( socketTimer.isActive() ) { stopSocketTimer(); } startSocketTimer(); } void SessionPrivate::onSocketTimeout() { kDebug() << "Socket timeout!"; thread->closeSocket(); } void Session::setTimeout( int timeout ) { d->setSocketTimeout( timeout * 1000 ); } int Session::timeout() const { return d->socketTimeout() / 1000; } #include "moc_session.cpp" #include "moc_session_p.cpp"