diff --git a/src/loginjob.cpp b/src/loginjob.cpp index c021c25..ebcc809 100644 --- a/src/loginjob.cpp +++ b/src/loginjob.cpp @@ -1,638 +1,612 @@ /* Copyright (c) 2009 Kevin Ottens Copyright (c) 2009 Andras Mantia 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 "loginjob.h" #include #include "kimap_debug.h" #include #include "job_p.h" #include "response_p.h" #include "session_p.h" #include "rfccodecs.h" #include "common.h" extern "C" { #include } static const sasl_callback_t callbacks[] = { { SASL_CB_ECHOPROMPT, nullptr, nullptr }, { SASL_CB_NOECHOPROMPT, nullptr, nullptr }, { SASL_CB_GETREALM, nullptr, nullptr }, { SASL_CB_USER, nullptr, nullptr }, { SASL_CB_AUTHNAME, nullptr, nullptr }, { SASL_CB_PASS, nullptr, nullptr }, { SASL_CB_CANON_USER, nullptr, nullptr }, { SASL_CB_LIST_END, nullptr, nullptr } }; namespace KIMAP { class LoginJobPrivate : public JobPrivate { public: enum AuthState { StartTls = 0, Capability, Login, Authenticate }; LoginJobPrivate(LoginJob *job, Session *session, const QString &name) : JobPrivate(session, name), q(job), encryptionMode(LoginJob::Unencrypted), authState(Login), plainLoginDisabled(false) { conn = nullptr; client_interact = nullptr; } ~LoginJobPrivate() { } bool sasl_interact(); bool startAuthentication(); bool answerChallenge(const QByteArray &data); void sslResponse(bool response); void saveServerGreeting(const Response &response); LoginJob *q; QString userName; QString authorizationName; QString password; QString serverGreeting; LoginJob::EncryptionMode encryptionMode; QString authMode; AuthState authState; QStringList capabilities; bool plainLoginDisabled; sasl_conn_t *conn; sasl_interact_t *client_interact; }; } using namespace KIMAP; bool LoginJobPrivate::sasl_interact() { qCDebug(KIMAP_LOG) << "sasl_interact"; sasl_interact_t *interact = client_interact; //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) { //TODO: dialog for use name?? break; } } interact = client_interact; while (interact->id != SASL_CB_LIST_END) { qCDebug(KIMAP_LOG) << "SASL_INTERACT id:" << interact->id; switch (interact->id) { case SASL_CB_AUTHNAME: if (!authorizationName.isEmpty()) { qCDebug(KIMAP_LOG) << "SASL_CB_[AUTHNAME]: '" << authorizationName << "'"; interact->result = strdup(authorizationName.toUtf8().constData()); interact->len = strlen((const char *) interact->result); break; } Q_FALLTHROUGH(); case SASL_CB_USER: qCDebug(KIMAP_LOG) << "SASL_CB_[USER|AUTHNAME]: '" << userName << "'"; interact->result = strdup(userName.toUtf8().constData()); interact->len = strlen((const char *) interact->result); break; case SASL_CB_PASS: qCDebug(KIMAP_LOG) << "SASL_CB_PASS: [hidden]"; interact->result = strdup(password.toUtf8().constData()); interact->len = strlen((const char *) interact->result); break; default: interact->result = nullptr; interact->len = 0; break; } interact++; } return true; } LoginJob::LoginJob(Session *session) : Job(*new LoginJobPrivate(this, session, i18n("Login"))) { Q_D(LoginJob); connect(d->sessionInternal(), SIGNAL(encryptionNegotiationResult(bool)), this, SLOT(sslResponse(bool))); qCDebug(KIMAP_LOG) << this; } LoginJob::~LoginJob() { qCDebug(KIMAP_LOG) << this; } QString LoginJob::userName() const { Q_D(const LoginJob); return d->userName; } void LoginJob::setUserName(const QString &userName) { Q_D(LoginJob); d->userName = userName; } QString LoginJob::authorizationName() const { Q_D(const LoginJob); return d->authorizationName; } void LoginJob::setAuthorizationName(const QString &authorizationName) { Q_D(LoginJob); d->authorizationName = authorizationName; } QString LoginJob::password() const { Q_D(const LoginJob); return d->password; } void LoginJob::setPassword(const QString &password) { Q_D(LoginJob); d->password = password; } void LoginJob::doStart() { Q_D(LoginJob); qCDebug(KIMAP_LOG) << this; // Don't authenticate on a session in the authenticated state if (session()->state() == Session::Authenticated || session()->state() == Session::Selected) { setError(UserDefinedError); setErrorText(i18n("IMAP session in the wrong state for authentication")); emitResult(); return; } // Trigger encryption negotiation only if needed EncryptionMode encryptionMode = d->encryptionMode; - switch (d->sessionInternal()->negotiatedEncryption()) { - case KTcpSocket::UnknownSslVersion: - break; // Do nothing the encryption mode still needs to be negotiated - - // For the other cases, pretend we're going unencrypted as that's the - // encryption mode already set on the session - // (so for instance we won't issue another STARTTLS for nothing if that's - // not needed) - case KTcpSocket::SslV2: - if (encryptionMode == SslV2) { - encryptionMode = Unencrypted; - } - break; - case KTcpSocket::SslV3: - if (encryptionMode == SslV3) { - encryptionMode = Unencrypted; - } - break; - case KTcpSocket::TlsV1: - if (encryptionMode == TlsV1) { - encryptionMode = Unencrypted; - } - break; - case KTcpSocket::AnySslVersion: - if (encryptionMode == AnySslVersion) { - encryptionMode = Unencrypted; - } - break; + const auto negotiatedEncryption = d->sessionInternal()->negotiatedEncryption(); + if (negotiatedEncryption != KTcpSocket::UnknownSslVersion) { + // If the socket is already encrypted, pretend we did not want any + // encryption + encryptionMode = Unencrypted; } - if (encryptionMode == SslV2 || - encryptionMode == SslV3 || - encryptionMode == SslV3_1 || - encryptionMode == AnySslVersion) { - KTcpSocket::SslVersion version = KTcpSocket::SslV2; - if (encryptionMode == SslV3) { - version = KTcpSocket::SslV3; - } - if (encryptionMode == SslV3_1) { - version = KTcpSocket::SslV3_1; - } - if (encryptionMode == AnySslVersion) { - version = KTcpSocket::AnySslVersion; + if (encryptionMode == SSLorTLS) { + d->sessionInternal()->startSsl(KTcpSocket::SecureProtocols); + } else if (encryptionMode == STARTTLS) { + if (d->capabilities.contains(QLatin1String("STARTTLS"))) { + d->authState = LoginJobPrivate::StartTls; + d->tags << d->sessionInternal()->sendCommand("STARTTLS"); + } else { + qCWarning(KIMAP_LOG) << "STARTTLS not supported by server!"; + setError(UserDefinedError); + emitResult(); + return; } - d->sessionInternal()->startSsl(version); - - } else if (encryptionMode == TlsV1) { - d->authState = LoginJobPrivate::StartTls; - d->tags << d->sessionInternal()->sendCommand("STARTTLS"); - - } else if (encryptionMode == Unencrypted) { + } else { if (d->authMode.isEmpty()) { d->authState = LoginJobPrivate::Login; qCDebug(KIMAP_LOG) << "sending LOGIN"; d->tags << d->sessionInternal()->sendCommand("LOGIN", '"' + quoteIMAP(d->userName).toUtf8() + '"' + ' ' + '"' + quoteIMAP(d->password).toUtf8() + '"'); } else { if (!d->startAuthentication()) { emitResult(); } } } } void LoginJob::handleResponse(const Response &response) { Q_D(LoginJob); if (response.content.isEmpty()) { return; } //set the actual command name for standard responses QString commandName = i18n("Login"); if (d->authState == LoginJobPrivate::Capability) { commandName = i18n("Capability"); } else if (d->authState == LoginJobPrivate::StartTls) { commandName = i18n("StartTls"); } enum ResponseCode { OK, ERR, UNTAGGED, CONTINUATION, MALFORMED }; QByteArray tag = response.content.first().toString(); ResponseCode code = OK; qCDebug(KIMAP_LOG) << commandName << tag; if (tag == "+") { code = CONTINUATION; } else if (tag == "*") { if (response.content.size() < 2) { code = MALFORMED; // Received empty untagged response } else { code = UNTAGGED; } } else if (d->tags.contains(tag)) { if (response.content.size() < 2) { code = MALFORMED; } else if (response.content[1].toString() == "OK") { code = OK; } else { code = ERR; } } switch (code) { case MALFORMED: // We'll handle it later break; case ERR: //server replied with NO or BAD for SASL authentication if (d->authState == LoginJobPrivate::Authenticate) { sasl_dispose(&d->conn); } setError(UserDefinedError); setErrorText(i18n("%1 failed, server replied: %2", commandName, QLatin1String(response.toString().constData()))); emitResult(); return; case UNTAGGED: // The only untagged response interesting for us here is CAPABILITY if (response.content[1].toString() == "CAPABILITY") { QList::const_iterator p = response.content.begin() + 2; while (p != response.content.end()) { QString capability = QLatin1String(p->toString()); d->capabilities << capability; if (capability == QLatin1String("LOGINDISABLED")) { d->plainLoginDisabled = true; } ++p; } qCDebug(KIMAP_LOG) << "Capabilities updated: " << d->capabilities; } break; case CONTINUATION: if (d->authState != LoginJobPrivate::Authenticate) { // Received unexpected continuation response for something // other than AUTHENTICATE command code = MALFORMED; break; } if (d->authMode == QLatin1String("PLAIN")) { if (response.content.size() > 1 && response.content.at(1).toString() == "OK") { return; } QByteArray challengeResponse; if (!d->authorizationName.isEmpty()) { challengeResponse += d->authorizationName.toUtf8(); } challengeResponse += '\0'; challengeResponse += d->userName.toUtf8(); challengeResponse += '\0'; challengeResponse += d->password.toUtf8(); challengeResponse = challengeResponse.toBase64(); d->sessionInternal()->sendData(challengeResponse); } else if (response.content.size() >= 2) { if (!d->answerChallenge(QByteArray::fromBase64(response.content[1].toString()))) { emitResult(); //error, we're done } } else { // Received empty continuation for authMode other than PLAIN code = MALFORMED; } break; case OK: switch (d->authState) { case LoginJobPrivate::StartTls: d->sessionInternal()->startSsl(KTcpSocket::SecureProtocols); break; case LoginJobPrivate::Capability: //cleartext login, if enabled if (d->authMode.isEmpty()) { if (d->plainLoginDisabled) { setError(UserDefinedError); setErrorText(i18n("Login failed, plain login is disabled by the server.")); emitResult(); } else { d->authState = LoginJobPrivate::Login; d->tags << d->sessionInternal()->sendCommand("LOGIN", '"' + quoteIMAP(d->userName).toUtf8() + '"' + ' ' + '"' + quoteIMAP(d->password).toUtf8() + '"'); } } else { bool authModeSupported = false; //find the selected SASL authentication method for (const QString &capability : qAsConst(d->capabilities)) { if (capability.startsWith(QLatin1String("AUTH="))) { if (capability.midRef(5) == d->authMode) { authModeSupported = true; break; } } } if (!authModeSupported) { setError(UserDefinedError); setErrorText(i18n("Login failed, authentication mode %1 is not supported by the server.", d->authMode)); emitResult(); } else if (!d->startAuthentication()) { emitResult(); //problem, we're done } } break; case LoginJobPrivate::Authenticate: sasl_dispose(&d->conn); //SASL authentication done // Fall through Q_FALLTHROUGH(); case LoginJobPrivate::Login: d->saveServerGreeting(response); emitResult(); //got an OK, command done break; } } if (code == MALFORMED) { setErrorText(i18n("%1 failed, malformed reply from the server.", commandName)); emitResult(); } } bool LoginJobPrivate::startAuthentication() { //SASL authentication if (!initSASL()) { q->setError(LoginJob::UserDefinedError); q->setErrorText(i18n("Login failed, client cannot initialize the SASL library.")); return false; } authState = LoginJobPrivate::Authenticate; const char *out = nullptr; uint outlen = 0; const char *mechusing = nullptr; int result = sasl_client_new("imap", m_session->hostName().toLatin1().constData(), nullptr, nullptr, callbacks, 0, &conn); if (result != SASL_OK) { const QString saslError = QString::fromUtf8(sasl_errdetail(conn)); qCWarning(KIMAP_LOG) << "sasl_client_new failed with:" << result << saslError; q->setError(LoginJob::UserDefinedError); q->setErrorText(saslError); return false; } do { qCDebug(KIMAP_LOG) << "Trying authmod" << authMode.toLatin1(); result = sasl_client_start(conn, authMode.toLatin1().constData(), &client_interact, capabilities.contains(QLatin1String("SASL-IR")) ? &out : nullptr, &outlen, &mechusing); if (result == SASL_INTERACT) { if (!sasl_interact()) { sasl_dispose(&conn); q->setError(LoginJob::UserDefinedError); //TODO: check up the actual error return false; } } } while (result == SASL_INTERACT); if (result != SASL_CONTINUE && result != SASL_OK) { const QString saslError = QString::fromUtf8(sasl_errdetail(conn)); qCWarning(KIMAP_LOG) << "sasl_client_start failed with:" << result << saslError; q->setError(LoginJob::UserDefinedError); q->setErrorText(saslError); sasl_dispose(&conn); return false; } QByteArray tmp = QByteArray::fromRawData(out, outlen); QByteArray challenge = tmp.toBase64(); if (challenge.isEmpty()) { tags << sessionInternal()->sendCommand("AUTHENTICATE", authMode.toLatin1()); } else { tags << sessionInternal()->sendCommand("AUTHENTICATE", authMode.toLatin1() + ' ' + challenge); } return true; } bool LoginJobPrivate::answerChallenge(const QByteArray &data) { QByteArray challenge = data; int result = -1; const char *out = nullptr; uint outlen = 0; do { result = sasl_client_step(conn, challenge.isEmpty() ? nullptr : challenge.data(), challenge.size(), &client_interact, &out, &outlen); if (result == SASL_INTERACT) { if (!sasl_interact()) { q->setError(LoginJob::UserDefinedError); //TODO: check up the actual error sasl_dispose(&conn); return false; } } } while (result == SASL_INTERACT); if (result != SASL_CONTINUE && result != SASL_OK) { const QString saslError = QString::fromUtf8(sasl_errdetail(conn)); qCWarning(KIMAP_LOG) << "sasl_client_step failed with:" << result << saslError; q->setError(LoginJob::UserDefinedError); //TODO: check up the actual error q->setErrorText(saslError); sasl_dispose(&conn); return false; } QByteArray tmp = QByteArray::fromRawData(out, outlen); challenge = tmp.toBase64(); sessionInternal()->sendData(challenge); return true; } void LoginJobPrivate::sslResponse(bool response) { if (response) { authState = LoginJobPrivate::Capability; tags << sessionInternal()->sendCommand("CAPABILITY"); } else { q->setError(LoginJob::UserDefinedError); q->setErrorText(i18n("Login failed, TLS negotiation failed.")); encryptionMode = LoginJob::Unencrypted; q->emitResult(); } } void LoginJob::setEncryptionMode(EncryptionMode mode) { Q_D(LoginJob); + if (mode == SslV2 || mode == SslV3 || mode == SslV3_1 || mode == AnySslVersion) { + mode = SSLorTLS; + } else if (mode == TlsV1) { + mode = STARTTLS; + } d->encryptionMode = mode; } LoginJob::EncryptionMode LoginJob::encryptionMode() { Q_D(LoginJob); return d->encryptionMode; } void LoginJob::setAuthenticationMode(AuthenticationMode mode) { Q_D(LoginJob); switch (mode) { case ClearText: d->authMode = QLatin1String(""); break; case Login: d->authMode = QStringLiteral("LOGIN"); break; case Plain: d->authMode = QStringLiteral("PLAIN"); break; case CramMD5: d->authMode = QStringLiteral("CRAM-MD5"); break; case DigestMD5: d->authMode = QStringLiteral("DIGEST-MD5"); break; case GSSAPI: d->authMode = QStringLiteral("GSSAPI"); break; case Anonymous: d->authMode = QStringLiteral("ANONYMOUS"); break; case XOAuth2: d->authMode = QStringLiteral("XOAUTH2"); break; default: d->authMode = QStringLiteral(""); } } void LoginJob::connectionLost() { Q_D(LoginJob); //don't emit the result if the connection was lost before getting the tls result, as it can mean //the TLS handshake failed and the socket was reconnected in normal mode if (d->authState != LoginJobPrivate::StartTls) { qCWarning(KIMAP_LOG) << "Connection to server lost " << d->m_socketError; if (d->m_socketError == KTcpSocket::SslHandshakeFailedError) { setError(KJob::UserDefinedError); setErrorText(i18n("SSL handshake failed.")); emitResult(); } else { setError(ERR_COULD_NOT_CONNECT); setErrorText(i18n("Connection to server lost.")); emitResult(); } } } void LoginJobPrivate::saveServerGreeting(const Response &response) { // Concatenate the parts of the server response into a string, while dropping the first two parts // (the response tag and the "OK" code), and being careful not to add useless extra whitespace. for (int i = 2; i < response.content.size(); i++) { if (response.content.at(i).type() == Response::Part::List) { serverGreeting += QLatin1Char('('); const QList itemLst = response.content.at(i).toList(); for (const QByteArray &item : itemLst) { serverGreeting += QLatin1String(item) + QLatin1Char(' '); } serverGreeting.chop(1); serverGreeting += QStringLiteral(") "); } else { serverGreeting += QLatin1String(response.content.at(i).toString()) + QLatin1Char(' '); } } serverGreeting.chop(1); } QString LoginJob::serverGreeting() const { Q_D(const LoginJob); return d->serverGreeting; } #include "moc_loginjob.cpp" diff --git a/src/loginjob.h b/src/loginjob.h index 8665ae3..d37e79c 100644 --- a/src/loginjob.h +++ b/src/loginjob.h @@ -1,134 +1,139 @@ /* Copyright (c) 2009 Kevin Ottens Copyright (c) 2009 Andras Mantia 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. */ #ifndef KIMAP_LOGINJOB_H #define KIMAP_LOGINJOB_H #include "kimap_export.h" #include "job.h" namespace KIMAP { class Session; struct Response; class LoginJobPrivate; class KIMAP_EXPORT LoginJob : public Job { Q_OBJECT Q_DECLARE_PRIVATE(LoginJob) friend class SessionPrivate; public: enum EncryptionMode { Unencrypted = 0, - TlsV1, - SslV2, - SslV3, - SslV3_1, - AnySslVersion + TlsV1, ///< @deprecated Use EncryptionMode::STARTTLS instead + SslV2, ///< @deprecated Use EncryptionMode::SSLorTLS instead + SslV3, ///< @deprecated Use EncryptionMode::SSLorTLS instead + SslV3_1, ///< @deprecated Use EncryptionMode::SSLorTLS instead + AnySslVersion, ///< @deprecated Use EncryptionMode::SSLorTLS instead + SSLorTLS, /*!< Use SSL/TLS encryption, KIMAP will automatically negoatiate + the best supported encryption protocol. */ + STARTTLS /*!< Use STARTTLS to upgrade an initially plaintext connection to + encrypted connection. KIMAP will automatically negoatiate + the best supported encryption protocol. */ }; enum AuthenticationMode { ClearText = 0, Login, Plain, CramMD5, DigestMD5, NTLM, GSSAPI, Anonymous, XOAuth2 }; enum ErrorCode { ERR_COULD_NOT_CONNECT = KJob::UserDefinedError + 23 // same as in kio }; explicit LoginJob(Session *session); ~LoginJob() override; QString userName() const; void setUserName(const QString &userName); /** * Get the authorization identity. * @since 4.10 */ QString authorizationName() const; /** * Set the authorization identity. * * If set, proxy-authentication according to RFC4616 will be used. * * Note that this feature only works with the "PLAIN" AuthenticationMode. * * The @param authorizationName will be used together with the password() to get authenticated as userName() by the authorization of the provided credentials. * This allows to login as a user using the admin credentials and the users name. * @since 4.10 */ void setAuthorizationName(const QString &authorizationName); QString password() const; void setPassword(const QString &password); /** * Returns the server greeting, in case of a successful login. * If the login wasn't successful, this method returns an empty string. Use errorString() to * get the error message in this case. * * Note that the content of this response is not defined by the IMAP protocol and is * implementation-dependent. * @since 4.7 */ QString serverGreeting() const; /** * Set the encryption mode for the connection. In case an encryption mode is set, the caller * MUST check the encryptionMode() result after executing the job, to see if the connection is * encrypted or not (e.g handshaking failed). * @param mode the encryption mode, see EncryptionModes */ void setEncryptionMode(EncryptionMode mode); /** Get the encryption mode. @return the currently active encryption mode */ EncryptionMode encryptionMode(); void setAuthenticationMode(AuthenticationMode mode); protected: void doStart() override; void handleResponse(const Response &response) override; void connectionLost() override; private: Q_PRIVATE_SLOT(d_func(), void sslResponse(bool)) }; } #endif diff --git a/src/session.cpp b/src/session.cpp index 9a4b03a..677d0aa 100644 --- a/src/session.cpp +++ b/src/session.cpp @@ -1,538 +1,535 @@ /* 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 "kimap_debug.h" #include "job.h" #include "job_p.h" #include "loginjob.h" #include "response_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 (!qEnvironmentVariableIsEmpty("KIMAP_LOGFILE")) { d->logger = new SessionLogger; } d->isSocketConnected = false; d->state = Disconnected; d->jobRunning = false; d->thread = new SessionThread(hostName, port); connect(d->thread, &SessionThread::encryptionNegotiationResult, d, &SessionPrivate::onEncryptionNegotiationResult); connect(d->thread, &SessionThread::sslError, d, &SessionPrivate::handleSslError); connect(d->thread, &SessionThread::socketDisconnected, d, &SessionPrivate::socketDisconnected); connect(d->thread, &SessionThread::responseReceived, d, &SessionPrivate::responseReceived); connect(d->thread, &SessionThread::socketConnected, d, &SessionPrivate::socketConnected); connect(d->thread, &SessionThread::socketActivity, d, &SessionPrivate::socketActivity); connect(d->thread, &SessionThread::socketError, d, &SessionPrivate::socketError); d->socketTimer.setSingleShot(true); connect(&d->socketTimer, &QTimer::timeout, d, &SessionPrivate::onSocketTimeout); d->startSocketTimer(); } Session::~Session() { //Make sure all jobs know we're done d->socketDisconnected(); delete d->thread; d->thread = nullptr; } void Session::setUiProxy(const 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) { //ignoreSslError is async, so the thread might already be gone when it returns QPointer _t = thread; const bool ignoreSslError = uiProxy && uiProxy->ignoreSslError(errorData); if (_t) { _t->sslErrorHandlerResponse(ignoreSslError); } } SessionPrivate::SessionPrivate(Session *session) : QObject(session), q(session), isSocketConnected(false), state(Session::Disconnected), logger(nullptr), thread(nullptr), jobRunning(false), currentJob(nullptr), 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, &KJob::result, this, &SessionPrivate::jobDone); QObject::connect(job, &QObject::destroyed, this, &SessionPrivate::jobDestroyed); if (state != Session::Disconnected) { startNext(); } } void SessionPrivate::startNext() { #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) QMetaObject::invokeMethod(this, &SessionPrivate::doStartNext); #else QMetaObject::invokeMethod(this, "doStartNext"); #endif } 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 = nullptr; emit q->jobQueueSizeChanged(q->jobQueueSize()); startNext(); } void SessionPrivate::jobDestroyed(QObject *job) { queue.removeAll(static_cast(job)); if (currentJob == job) { currentJob = nullptr; } } void SessionPrivate::responseReceived(const Response &response) { if (logger && isConnected()) { 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") { Response simplified = response; if (simplified.content.size() >= 2) { simplified.content.removeFirst(); // Strip the tag simplified.content.removeFirst(); // Strip the code } qCDebug(KIMAP_LOG) << "Received BYE: " << simplified.toString(); return; } switch (state) { case Session::Disconnected: if (socketTimer.isActive()) { stopSocketTimer(); } if (code == "OK") { setState(Session::NotAuthenticated); Response 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); Response 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 != nullptr) { restartSocketTimer(); currentJob->handleResponse(response); } else { qCWarning(KIMAP_LOG) << "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 && isConnected()) { 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); + willUseSsl = (login->encryptionMode() == KIMAP::LoginJob::SSLorTLS); userName = login->userName(); } } if (state == Session::Disconnected && willUseSsl) { startNext(); } else { startSocketTimer(); } } bool SessionPrivate::isConnected() const { return state == Session::Authenticated || state == Session::Selected; } void SessionPrivate::socketDisconnected() { if (socketTimer.isActive()) { stopSocketTimer(); } if (logger && isConnected()) { logger->disconnectionOccured(); } if (isSocketConnected) { 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->d_ptr->setSocketError(error); } else if (!queue.isEmpty()) { currentJob = queue.takeFirst(); currentJob->d_ptr->setSocketError(error); } if (isSocketConnected) { thread->closeSocket(); } else { emit q->connectionFailed(); 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(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() { qCDebug(KIMAP_LOG) << "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"