diff --git a/autotests/loginjobtest.cpp b/autotests/loginjobtest.cpp index 8121885..52478ef 100644 --- a/autotests/loginjobtest.cpp +++ b/autotests/loginjobtest.cpp @@ -1,326 +1,326 @@ /* Copyright (C) 2009 Andras Mantia Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "kimap2test/fakeserver.h" #include "kimap2/session.h" #include "kimap2/loginjob.h" #include class LoginJobTest: public QObject { Q_OBJECT private Q_SLOTS: void shouldHandleLogin_data() { QTest::addColumn("user"); QTest::addColumn("password"); QTest::addColumn< QList >("scenario"); QList scenario; scenario << FakeServer::greeting() << "C: A000001 CAPABILITY" << "S: A000001 OK" << "C: A000002 LOGIN \"user\" \"password\"" << "S: A000002 OK User logged in"; QTest::newRow("success") << "user" << "password" << scenario; scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 CAPABILITY" << "S: A000001 OK" << "C: A000002 LOGIN \"user_bad\" \"password\"" << "S: A000002 NO Login failed: authentication failure"; QTest::newRow("wrong login") << "user_bad" << "password" << scenario; scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 CAPABILITY" << "S: A000001 OK" << "C: A000002 LOGIN \"user\" \"aa\\\"bb\\\\cc[dd ee\"" << "S: A000002 OK User logged in"; QTest::newRow("special chars") << "user" << "aa\"bb\\cc[dd ee" << scenario; scenario.clear(); scenario << FakeServer::preauth(); QTest::newRow("already authenticated") << "user" << "password" << scenario; } void shouldHandleLogin() { QFETCH(QString, user); QFETCH(QString, password); QFETCH(QList, scenario); FakeServer fakeServer; fakeServer.setScenario(scenario); fakeServer.startAndWait(); KIMAP2::Session *session = new KIMAP2::Session("127.0.0.1", 5989); KIMAP2::LoginJob *login = new KIMAP2::LoginJob(session); login->setUserName(user); login->setPassword(password); bool result = login->exec(); QEXPECT_FAIL("wrong login", "Login with bad user name", Continue); QEXPECT_FAIL("already authenticated", "Trying to log on an already authenticated session", Continue); QVERIFY(result); fakeServer.quit(); delete session; } void shouldHandleProxyLogin_data() { QTest::addColumn("user"); QTest::addColumn("proxy"); QTest::addColumn("password"); QTest::addColumn< QList >("scenario"); QList scenario; scenario << FakeServer::greeting() << "C: A000001 CAPABILITY" << "S: A000001 OK" << "C: A000002 AUTHENTICATE PLAIN" << "S: A000002 OK (success)" << "C: A000003 LOGIN \"proxy\" \"user\" \"password\"" << "S: A000003 OK User logged in"; QTest::newRow("success") << "user" << "proxy" << "password" << scenario; } void shouldHandleProxyLogin() { QFETCH(QString, user); QFETCH(QString, proxy); QFETCH(QString, password); QFETCH(QList, scenario); FakeServer fakeServer; fakeServer.setScenario(scenario); fakeServer.startAndWait(); KIMAP2::Session *session = new KIMAP2::Session("127.0.0.1", 5989); KIMAP2::LoginJob *login = new KIMAP2::LoginJob(session); login->setAuthenticationMode(KIMAP2::LoginJob::Plain); login->setUserName(user); login->setAuthorizationName(proxy); login->setPassword(password); bool result = login->exec(); QVERIFY(result); fakeServer.quit(); delete session; } void shouldSaveServerGreeting_data() { QTest::addColumn("greeting"); QTest::addColumn< QList >("scenario"); QList scenario; scenario << FakeServer::greeting() << "C: A000001 CAPABILITY" << "S: A000001 OK" << "C: A000002 LOGIN \"user\" \"password\"" << "S: A000002 OK Welcome John Smith"; QTest::newRow("greeting") << "Welcome John Smith" << scenario; scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 CAPABILITY" << "S: A000001 OK" << "C: A000002 LOGIN \"user\" \"password\"" << "S: A000002 OK Welcome John Smith (last login: Feb 21, 2010)"; QTest::newRow("greeting with parenthesis") << "Welcome John Smith (last login: Feb 21, 2010)" << scenario; scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 CAPABILITY" << "S: A000001 OK" << "C: A000002 LOGIN \"user\" \"password\"" << "S: A000002 OK"; QTest::newRow("no greeting") << "" << scenario; scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 CAPABILITY" << "S: A000001 OK" << "C: A000002 LOGIN \"user\" \"password\"" << "S: A000002 NO Login failed: authentication failure"; QTest::newRow("login failed") << "" << scenario; } void shouldSaveServerGreeting() { QFETCH(QString, greeting); QFETCH(QList, scenario); FakeServer fakeServer; fakeServer.setScenario(scenario); fakeServer.startAndWait(); KIMAP2::Session *session = new KIMAP2::Session("127.0.0.1", 5989); KIMAP2::LoginJob *login = new KIMAP2::LoginJob(session); login->setUserName("user"); login->setPassword("password"); login->exec(); QCOMPARE(login->serverGreeting(), greeting); fakeServer.quit(); delete session; } void shouldUseSsl_data() { QTest::addColumn< QList >("scenario"); QTest::addColumn< int >("serverEncryption"); QTest::addColumn< int >("clientEncryption"); QTest::addColumn< bool >("startTls"); { QList scenario; scenario << FakeServer::greeting() << "C: A000001 STARTTLS" << "S: A000001 OK" << "C: A000002 CAPABILITY" << "S: A000002 OK" << "C: A000003 LOGIN \"user\" \"password\"" << "S: A000003 OK"; QTest::newRow("starttls tlsv1.0") << scenario << static_cast(QSsl::TlsV1_0) << static_cast(QSsl::TlsV1_0) << true; QTest::newRow("starttls tlsv1.1") << scenario << static_cast(QSsl::TlsV1_1) << static_cast(QSsl::TlsV1_1) << true; QTest::newRow("starttls tlsv1.2") << scenario << static_cast(QSsl::TlsV1_2) << static_cast(QSsl::TlsV1_2) << true; } { QList scenario; scenario << FakeServer::greeting() << "C: A000001 CAPABILITY" << "S: A000001 OK" << "C: A000002 LOGIN \"user\" \"password\"" << "S: A000002 OK"; QTest::newRow("sslv3") << scenario << static_cast(QSsl::SslV3) << static_cast(QSsl::SslV3) << false; //sslv2 is not supported anymore on my system // QTest::newRow("sslv2") << scenario << static_cast(QSsl::SslV2) << static_cast(KIMAP2::LoginJob::SslV2); //AnyProtocol doesn't mean the server can force a specific version (e.g. openssl always starts with a sslv2 hello) QTest::newRow("any protocol with anyssl version") << scenario << static_cast(QSsl::AnyProtocol) << static_cast(QSsl::AnyProtocol) << false; QTest::newRow("tlsv1.0") << scenario << static_cast(QSsl::TlsV1_0) << static_cast(QSsl::TlsV1_0) << false; } } void shouldUseSsl() { QFETCH(QList, scenario); QFETCH(int, serverEncryption); QFETCH(int, clientEncryption); QFETCH(bool, startTls); FakeServer fakeServer; fakeServer.setEncrypted(static_cast(serverEncryption), startTls); fakeServer.setScenario(scenario); fakeServer.startAndWait(); KIMAP2::Session *session = new KIMAP2::Session("127.0.0.1", 5989); QObject::connect(session, &KIMAP2::Session::sslErrors, [session](const QList &errors) { qWarning() << "Got ssl error: " << errors; session->ignoreErrors(errors); }); KIMAP2::LoginJob *login = new KIMAP2::LoginJob(session); login->setUserName("user"); login->setPassword("password"); login->setEncryptionMode(static_cast(clientEncryption), startTls); QVERIFY(login->exec()); fakeServer.quit(); delete session; } void shouldFailOnWrongSslSettings_data() { QTest::addColumn< QList >("scenario"); QTest::addColumn< int >("serverEncryption"); QTest::addColumn< int >("clientEncryption"); QTest::addColumn< int >("expectedErrorCode"); { QList scenario; scenario << FakeServer::greeting(); //For some reason only connecting to tlsv1 results in an ssl handshake error, with the wrong version only the server detects the error and disconnects // QTest::newRow( "ssl v3 v2" ) << scenario << static_cast(QSsl::SslV3) << static_cast(KIMAP2::LoginJob::SslV2) << static_cast(KJob::UserDefinedError); - QTest::newRow("ssl tlsv1 v3") << scenario << static_cast(QSsl::SslV3) << static_cast(QSsl::TlsV1_0) << static_cast(KIMAP2::SSLHandshakeFailed); + QTest::newRow("ssl tlsv1 v3") << scenario << static_cast(QSsl::SslV3) << static_cast(QSsl::TlsV1_0) << static_cast(KIMAP2::SslHandshakeFailed); } } void shouldFailOnWrongSslSettings() { QFETCH(QList, scenario); QFETCH(int, serverEncryption); QFETCH(int, clientEncryption); QFETCH(int, expectedErrorCode); FakeServer fakeServer; fakeServer.setScenario(scenario); fakeServer.startAndWait(); KIMAP2::Session *session = new KIMAP2::Session("127.0.0.1", 5989); QObject::connect(session, &KIMAP2::Session::sslErrors, [session](const QList &errors) { qWarning() << "Got ssl error: " << errors; session->ignoreErrors(errors); }); KIMAP2::LoginJob *login = new KIMAP2::LoginJob(session); login->setUserName("user"); login->setPassword("password"); login->setEncryptionMode(static_cast(clientEncryption), false); QVERIFY(!login->exec()); QCOMPARE(static_cast(login->error()), expectedErrorCode); fakeServer.quit(); delete session; } }; QTEST_GUILESS_MAIN(LoginJobTest) #include "loginjobtest.moc" diff --git a/src/job.h b/src/job.h index f231aa2..1ede4aa 100644 --- a/src/job.h +++ b/src/job.h @@ -1,83 +1,83 @@ /* Copyright (c) 2009 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. */ #ifndef KIMAP2_JOB_H #define KIMAP2_JOB_H #include "kimap2_export.h" #include #include namespace KIMAP2 { class Session; class SessionPrivate; class JobPrivate; struct Message; enum ErrorCodes { ConnectionLost = KJob::UserDefinedError + 1, CommandFailed, CouldNotConnect, - SSLHandshakeFailed, + SslHandshakeFailed, HostNotFound, LoginFailed, LastError }; class KIMAP2_EXPORT Job : public KJob { Q_OBJECT Q_DECLARE_PRIVATE(Job) friend class SessionPrivate; public: virtual ~Job(); Session *session() const; void start() Q_DECL_OVERRIDE; private: virtual void doStart() = 0; virtual void handleResponse(const Message &response); virtual void connectionLost(); void setSocketError(QAbstractSocket::SocketError); void setErrorMessage(const QString &message); protected: enum HandlerResponse { Handled = 0, NotHandled }; HandlerResponse handleErrorReplies(const Message &response); explicit Job(Session *session); explicit Job(JobPrivate &dd); JobPrivate *const d_ptr; }; } #endif diff --git a/src/loginjob.cpp b/src/loginjob.cpp index f6f29ca..b055275 100644 --- a/src/loginjob.cpp +++ b/src/loginjob.cpp @@ -1,650 +1,650 @@ /* Copyright (c) 2009 Kevin Ottens Copyright (c) 2009 Andras Mantia Copyright (c) 2017 Christian Mollekopf 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 "kimap_debug.h" #include "job_p.h" #include "message_p.h" #include "session_p.h" #include "rfccodecs.h" #include "common.h" extern "C" { #include } static const sasl_callback_t callbacks[] = { { SASL_CB_ECHOPROMPT, Q_NULLPTR, nullptr }, { SASL_CB_NOECHOPROMPT, Q_NULLPTR, nullptr }, { SASL_CB_GETREALM, Q_NULLPTR, nullptr }, { SASL_CB_USER, Q_NULLPTR, nullptr }, { SASL_CB_AUTHNAME, Q_NULLPTR, nullptr }, { SASL_CB_PASS, Q_NULLPTR, nullptr }, { SASL_CB_CANON_USER, Q_NULLPTR, nullptr }, { SASL_CB_LIST_END, Q_NULLPTR, nullptr } }; namespace KIMAP2 { 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(QSsl::UnknownProtocol), startTls(false), authState(Login), plainLoginDisabled(false) { conn = Q_NULLPTR; client_interact = Q_NULLPTR; } ~LoginJobPrivate() { } bool sasl_interact(); bool startAuthentication(); void sendPlainLogin(); bool answerChallenge(const QByteArray &data); void sslResponse(bool response); void saveServerGreeting(const Message &response); void login(); void retrieveCapabilities(); LoginJob *q; QString userName; QString authorizationName; QString password; QString serverGreeting; QSsl::SslProtocol encryptionMode; bool startTls; QString authMode; AuthState authState; QStringList capabilities; bool plainLoginDisabled; sasl_conn_t *conn; sasl_interact_t *client_interact; }; } using namespace KIMAP2; bool LoginJobPrivate::sasl_interact() { qCDebug(KIMAP2_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(KIMAP2_LOG) << "SASL_INTERACT id:" << interact->id; switch (interact->id) { case SASL_CB_AUTHNAME: if (!authorizationName.isEmpty()) { qCDebug(KIMAP2_LOG) << "SASL_CB_[AUTHNAME]: '" << authorizationName << "'"; interact->result = strdup(authorizationName.toUtf8()); interact->len = strlen((const char *) interact->result); break; } case SASL_CB_USER: qCDebug(KIMAP2_LOG) << "SASL_CB_[USER|AUTHNAME]: '" << userName << "'"; interact->result = strdup(userName.toUtf8()); interact->len = strlen((const char *) interact->result); break; case SASL_CB_PASS: qCDebug(KIMAP2_LOG) << "SASL_CB_PASS: [hidden]"; interact->result = strdup(password.toUtf8()); interact->len = strlen((const char *) interact->result); break; default: interact->result = Q_NULLPTR; interact->len = 0; break; } interact++; } //FIXME This should return false at least in some cases return true; } LoginJob::LoginJob(Session *session) : Job(*new LoginJobPrivate(this, session, QString::fromUtf8("Login"))) { qCDebug(KIMAP2_LOG) << this; } LoginJob::~LoginJob() { qCDebug(KIMAP2_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; } /* * The IMAP authentication procedure is unfortunately ridiculously complicated due to the many different options: * * An IMAP Session always has the following structure: * * Connection is established. * * Server sends greeting. * * Client authenticates somehow. * * ..... * * If the we have a plain connection it's simple: * * Wait for the greeting * * Login using the chosen authentication mechanism * * If we're using TLS (without STARTTLS, so directly): * * Immediately initiate TLS handshake. * * Wait for the greeting * * Get CAPABILITIES to figure out which AUTH mechs are supported * * Login using the chosen authentication mechanism * * If we're using TLS with STARTTLS: * * Wait for the greeting (on the unencrypted connection) * * Send STARTTLS and wait for OK * * Initiate TLS handshake * * Get CAPABILITIES to figure out which AUTH mechs are supported * * Login using the chosen authentication mechanism */ void LoginJob::doStart() { Q_D(LoginJob); qCDebug(KIMAP2_LOG) << "doStart" << this; connect(d->sessionInternal(), SIGNAL(encryptionNegotiationResult(bool)), this, SLOT(sslResponse(bool))); if (session()->state() == Session::Disconnected) { auto guard = new QObject(this); QObject::connect(session(), &Session::stateChanged, guard, [d, guard](KIMAP2::Session::State newState, KIMAP2::Session::State) { qCDebug(KIMAP2_LOG) << "Session state changed" << newState; d->login(); delete guard; }); if (!d->startTls && d->encryptionMode != QSsl::UnknownProtocol) { //We have to encrypt for the greeting d->sessionInternal()->startSsl(d->encryptionMode); } //We wait for the server greeting return; } else { qCInfo(KIMAP2_LOG) << "Session is ready, carring on"; //The session is ready, we can carry on. d->login(); } } void LoginJobPrivate::login() { // Don't authenticate on a session in the authenticated state if (q->session()->isConnected()) { q->setError(LoginJob::UserDefinedError); q->setErrorText(QString::fromUtf8("IMAP session in the wrong state for authentication")); q->emitResult(); return; } if (startTls) { //With STARTTLS we have to try to upgrade our connection before the login qCInfo(KIMAP2_LOG) << "Starting with tls"; authState = LoginJobPrivate::StartTls; sendCommand("STARTTLS", {}); return; } else { //If this is unecrypted we can retrieve capabilties. Otherwise we wait for the sslResponse. if (encryptionMode == QSsl::UnknownProtocol) { retrieveCapabilities(); } else { qCInfo(KIMAP2_LOG) << "Waiting for encryption before retrieveing capabilities."; } } } void LoginJobPrivate::sslResponse(bool response) { qCDebug(KIMAP2_LOG) << "Got an ssl response " << response; if (response) { retrieveCapabilities(); } else { q->setError(LoginFailed); q->setErrorText(QString::fromUtf8("Login failed, TLS negotiation failed.")); encryptionMode = QSsl::UnknownProtocol; q->emitResult(); } } void LoginJobPrivate::retrieveCapabilities() { qCDebug(KIMAP2_LOG) << "Retrieving capabilities."; authState = LoginJobPrivate::Capability; sendCommand("CAPABILITY", {}); } void LoginJob::handleResponse(const Message &response) { Q_D(LoginJob); if (response.content.isEmpty()) { return; } //set the actual command name for standard responses QString commandName = QStringLiteral("Login"); if (d->authState == LoginJobPrivate::Capability) { commandName = QStringLiteral("Capability"); } else if (d->authState == LoginJobPrivate::StartTls) { commandName = QStringLiteral("StartTls"); } enum ResponseCode { OK, ERR, UNTAGGED, CONTINUATION, MALFORMED }; QByteArray tag = response.content.first().toString(); ResponseCode code = OK; qCDebug(KIMAP2_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(LoginFailed); setErrorText(QString("%1 failed, server replied: %2").arg(commandName).arg(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; } qCInfo(KIMAP2_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: //Start encryption and wait for sslResponse d->sessionInternal()->startSsl(d->encryptionMode); break; case LoginJobPrivate::Capability: //cleartext login, if enabled if (d->authMode.isEmpty()) { if (d->plainLoginDisabled) { setError(LoginFailed); setErrorText(QString("Login failed, plain login is disabled by the server.")); emitResult(); } else { d->sendPlainLogin(); } } else { bool authModeSupported = false; //PLAIN is always supported as defined in the standard. We should also get an AUTH= capability, but in case a server doesn't properly announce it we'll just accept it anyways. if (d->authMode == "PLAIN") { authModeSupported = true; } //find the selected SASL authentication method Q_FOREACH (const QString &capability, d->capabilities) { if (capability.startsWith(QLatin1String("AUTH="))) { if (capability.mid(5) == d->authMode) { authModeSupported = true; break; } } } if (!authModeSupported) { setError(LoginFailed); setErrorText(QString("Login failed, authentication mode %1 is not supported by the server.").arg(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 case LoginJobPrivate::Login: d->saveServerGreeting(response); emitResult(); //got an OK, command done break; } } if (code == MALFORMED) { setErrorText(QString("%1 failed, malformed reply from the server.").arg(commandName)); emitResult(); } } bool LoginJobPrivate::startAuthentication() { //SASL authentication if (!initSASL()) { q->setError(LoginFailed); q->setErrorText(QString("Login failed, client cannot initialize the SASL library.")); return false; } authState = LoginJobPrivate::Authenticate; const char *out = Q_NULLPTR; uint outlen = 0; const char *mechusing = Q_NULLPTR; int result = sasl_client_new("imap", m_session->hostName().toLatin1(), Q_NULLPTR, nullptr, callbacks, 0, &conn); if (result != SASL_OK) { qCWarning(KIMAP2_LOG) << "sasl_client_new failed with:" << result; q->setError(LoginFailed); q->setErrorText(QString::fromUtf8(sasl_errdetail(conn))); return false; } do { result = sasl_client_start(conn, authMode.toLatin1(), &client_interact, capabilities.contains(QStringLiteral("SASL-IR")) ? &out : Q_NULLPTR, &outlen, &mechusing); if (result == SASL_INTERACT) { if (!sasl_interact()) { sasl_dispose(&conn); q->setError(LoginFailed); //TODO: check up the actual error q->setErrorText(QString("sasl_interact failed")); return false; } } } while (result == SASL_INTERACT); if (result != SASL_CONTINUE && result != SASL_OK) { qCWarning(KIMAP2_LOG) << "sasl_client_start failed with:" << result; q->setError(LoginFailed); q->setErrorText(QString::fromUtf8(sasl_errdetail(conn))); sasl_dispose(&conn); return false; } QByteArray tmp = QByteArray::fromRawData(out, outlen); QByteArray challenge = tmp.toBase64(); if (challenge.isEmpty()) { sendCommand("AUTHENTICATE", authMode.toLatin1()); } else { sendCommand("AUTHENTICATE", authMode.toLatin1() + ' ' + challenge); } return true; } void LoginJobPrivate::sendPlainLogin() { authState = LoginJobPrivate::Login; qCDebug(KIMAP2_LOG) << "sending LOGIN"; sendCommand("LOGIN", '"' + quoteIMAP(userName).toUtf8() + '"' + ' ' + '"' + quoteIMAP(password).toUtf8() + '"'); } bool LoginJobPrivate::answerChallenge(const QByteArray &data) { QByteArray challenge = data; int result = -1; const char *out = Q_NULLPTR; uint outlen = 0; do { result = sasl_client_step(conn, challenge.isEmpty() ? Q_NULLPTR : challenge.data(), challenge.size(), &client_interact, &out, &outlen); if (result == SASL_INTERACT) { if (!sasl_interact()) { q->setError(LoginFailed); //TODO: check up the actual error q->setErrorText(QString("sasl_interact failed")); sasl_dispose(&conn); return false; } } } while (result == SASL_INTERACT); if (result != SASL_CONTINUE && result != SASL_OK) { qCWarning(KIMAP2_LOG) << "sasl_client_step failed with:" << result; q->setError(LoginFailed); //TODO: check up the actual error q->setErrorText(QString::fromUtf8(sasl_errdetail(conn))); sasl_dispose(&conn); return false; } QByteArray tmp = QByteArray::fromRawData(out, outlen); challenge = tmp.toBase64(); sessionInternal()->sendData(challenge); return true; } void LoginJob::setEncryptionMode(QSsl::SslProtocol mode, bool startTls) { Q_D(LoginJob); d->encryptionMode = mode; d->startTls = startTls; } QSsl::SslProtocol 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); qCWarning(KIMAP2_LOG) << "Connection to server lost " << d->m_socketError; if (d->m_socketError == QSslSocket::SslHandshakeFailedError) { - setError(SSLHandshakeFailed); + setError(SslHandshakeFailed); setErrorText(QString::fromUtf8("SSL handshake failed.")); emitResult(); } else if (d->m_socketError == QSslSocket::HostNotFoundError) { setError(HostNotFound); setErrorText(QString::fromUtf8("Host not found.")); emitResult(); } else { setError(CouldNotConnect); setErrorText(QString::fromUtf8("Connection to server lost.")); emitResult(); } } void LoginJobPrivate::saveServerGreeting(const Message &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() == Message::Part::List) { serverGreeting += QLatin1Char('('); foreach (const QByteArray &item, response.content.at(i).toList()) { 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"