diff --git a/autotests/fakeserver.cpp b/autotests/fakeserver.cpp index dbba154..f25a56f 100644 --- a/autotests/fakeserver.cpp +++ b/autotests/fakeserver.cpp @@ -1,237 +1,231 @@ /* Copyright 2010 BetterInbox Author: Christophe Laveault Gregory Schlomoff This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "fakeserver.h" #include #include #include FakeServer::FakeServer(QObject *parent) : QThread(parent) { moveToThread(this); } QByteArray FakeServer::greeting() { return "S: 220 localhost ESMTP xx777xx"; } QList FakeServer::greetingAndEhlo(bool multiline) { return QList() << greeting() << "C: EHLO 127.0.0.1" << QByteArray("S: 250") + (multiline ? '-' : ' ') + "Localhost ready to roll"; } QList FakeServer::bye() { return { "C: QUIT", "S: 221 So long, and thanks for all the fish", "X: " }; } FakeServer::~FakeServer() { quit(); wait(); } void FakeServer::startAndWait() { start(); // this will block until the event queue starts -#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) - QMetaObject::invokeMethod(this, &FakeServer::started, Qt::BlockingQueuedConnection); -#else QMetaObject::invokeMethod(this, "started", Qt::BlockingQueuedConnection); -#endif - - } void FakeServer::dataAvailable() { QMutexLocker locker(&m_mutex); QTcpSocket *socket = qobject_cast(sender()); Q_ASSERT(socket != nullptr); int scenarioNumber = m_clientSockets.indexOf(socket); QVERIFY(!m_scenarios[scenarioNumber].isEmpty()); readClientPart(scenarioNumber); writeServerPart(scenarioNumber); } void FakeServer::newConnection() { QMutexLocker locker(&m_mutex); m_clientSockets << m_tcpServer->nextPendingConnection(); connect(m_clientSockets.last(), SIGNAL(readyRead()), this, SLOT(dataAvailable())); //m_clientParsers << new KIMAP::ImapStreamParser( m_clientSockets.last(), true ); QVERIFY(m_clientSockets.size() <= m_scenarios.size()); writeServerPart(m_clientSockets.size() - 1); } void FakeServer::run() { m_tcpServer = new QTcpServer(); if (!m_tcpServer->listen(QHostAddress(QHostAddress::LocalHost), 5989)) { qFatal("Unable to start the server"); return; } connect(m_tcpServer, SIGNAL(newConnection()), this, SLOT(newConnection())); exec(); qDeleteAll(m_clientSockets); delete m_tcpServer; } void FakeServer::started() { // do nothing: this is a dummy slot used by startAndWait() } void FakeServer::setScenario(const QList &scenario) { QMutexLocker locker(&m_mutex); m_scenarios.clear(); m_scenarios << scenario; } void FakeServer::addScenario(const QList &scenario) { QMutexLocker locker(&m_mutex); m_scenarios << scenario; } void FakeServer::addScenarioFromFile(const QString &fileName) { QFile file(fileName); file.open(QFile::ReadOnly); QList scenario; // When loading from files we never have the authentication phase // force jumping directly to authenticated state. //scenario << preauth(); while (!file.atEnd()) { scenario << file.readLine().trimmed(); } file.close(); addScenario(scenario); } bool FakeServer::isScenarioDone(int scenarioNumber) const { QMutexLocker locker(&m_mutex); if (scenarioNumber < m_scenarios.size()) { return m_scenarios[scenarioNumber].isEmpty(); } else { return true; // Non existent hence empty, right? } } bool FakeServer::isAllScenarioDone() const { QMutexLocker locker(&m_mutex); foreach (const QList &scenario, m_scenarios) { if (!scenario.isEmpty()) { qDebug() << scenario; return false; } } return true; } void FakeServer::writeServerPart(int scenarioNumber) { QList scenario = m_scenarios[scenarioNumber]; QTcpSocket *clientSocket = m_clientSockets[scenarioNumber]; while (!scenario.isEmpty() && (scenario.first().startsWith("S: ") || scenario.first().startsWith("W: "))) { QByteArray rule = scenario.takeFirst(); if (rule.startsWith("S: ")) { QByteArray payload = rule.mid(3); clientSocket->write(payload + "\r\n"); } else { int timeout = rule.mid(3).toInt(); QTest::qWait(timeout); } } if (!scenario.isEmpty() && scenario.first().startsWith('X')) { scenario.takeFirst(); clientSocket->close(); } if (!scenario.isEmpty()) { QVERIFY(scenario.first().startsWith("C: ")); } m_scenarios[scenarioNumber] = scenario; } void FakeServer::readClientPart(int scenarioNumber) { QList scenario = m_scenarios[scenarioNumber]; QTcpSocket *clientSocket = m_clientSockets[scenarioNumber]; while (!scenario.isEmpty() && scenario.first().startsWith("C: ")) { QByteArray line = clientSocket->readLine(); QByteArray received = "C: " + line.trimmed(); QByteArray expected = scenario.takeFirst(); if (expected == "C: SKIP" && !scenario.isEmpty()) { expected = scenario.takeFirst(); while (received != expected) { received = "C: " + clientSocket->readLine().trimmed(); } } QCOMPARE(QString::fromUtf8(received), QString::fromUtf8(expected)); QCOMPARE(received, expected); } if (!scenario.isEmpty()) { QVERIFY(scenario.first().startsWith("S: ")); } m_scenarios[scenarioNumber] = scenario; } diff --git a/src/session.cpp b/src/session.cpp index 2129f72..0be26a9 100644 --- a/src/session.cpp +++ b/src/session.cpp @@ -1,523 +1,519 @@ /* Copyright 2010 BetterInbox Author: Christophe Laveault Gregory Schlomoff This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "session.h" #include "session_p.h" #include "sessionthread_p.h" #include "job.h" #include "serverresponse_p.h" #include "loginjob.h" #include "sendjob.h" #include "ksmtp_debug.h" #include #include #include #include #include using namespace KSmtp; Q_DECLARE_METATYPE(KTcpSocket::SslVersion) Q_DECLARE_METATYPE(KSslErrorUiData) SessionPrivate::SessionPrivate(Session *session) : QObject(session), q(session), m_state(Session::Disconnected), m_thread(nullptr), m_socketTimerInterval(10000), m_startLoop(nullptr), m_sslVersion(KTcpSocket::UnknownSslVersion), m_jobRunning(false), m_currentJob(nullptr), m_ehloRejected(false), m_size(0), m_allowsTls(false) { qRegisterMetaType(); qRegisterMetaType(); } SessionPrivate::~SessionPrivate() { m_thread->quit(); m_thread->wait(10000); delete m_thread; } void SessionPrivate::handleSslError(const KSslErrorUiData &data) { QPointer _t = m_thread; const bool ignore = m_uiProxy && m_uiProxy->ignoreSslError(data); if (_t) { _t->handleSslErrorResponse(ignore); } } void SessionPrivate::setAuthenticationMethods(const QList &authMethods) { for (const QByteArray &method : authMethods) { QString m = QString::fromLatin1(method); if (!m_authModes.contains(m)) { m_authModes.append(m); } } } void SessionPrivate::startHandshake() { QString hostname = m_customHostname; if (hostname.isEmpty()) { // FIXME: QHostInfo::fromName can get a FQDN, but does a DNS lookup hostname = QHostInfo::localHostName(); if (hostname.isEmpty()) { hostname = QStringLiteral("localhost.invalid"); } else if (!hostname.contains(QLatin1Char('.'))) { hostname += QStringLiteral(".localnet"); } } QByteArray cmd; if (!m_ehloRejected) { cmd = "EHLO "; } else { cmd = "HELO "; } setState(Session::Handshake); sendData(cmd + QUrl::toAce(hostname)); } Session::Session(const QString &hostName, quint16 port, QObject *parent) : QObject(parent), d(new SessionPrivate(this)) { qRegisterMetaType("ServerResponse"); QHostAddress ip; QString saneHostName = hostName; if (ip.setAddress(hostName)) { //saneHostName = QStringLiteral("[%1]").arg(hostName); } d->m_thread = new SessionThread(saneHostName, port, this); d->m_thread->start(); connect(d->m_thread, &SessionThread::sslError, d, &SessionPrivate::handleSslError); } Session::~Session() { } void Session::setUiProxy(const SessionUiProxy::Ptr &uiProxy) { d->m_uiProxy = uiProxy; } SessionUiProxy::Ptr Session::uiProxy() const { return d->m_uiProxy; } QString Session::hostName() const { return d->m_thread->hostName(); } quint16 Session::port() const { return d->m_thread->port(); } Session::State Session::state() const { return d->m_state; } bool Session::allowsTls() const { return d->m_allowsTls; } QStringList Session::availableAuthModes() const { return d->m_authModes; } int Session::sizeLimit() const { return d->m_size; } void Session::setSocketTimeout(int ms) { bool timerActive = d->m_socketTimer.isActive(); if (timerActive) { d->stopSocketTimer(); } d->m_socketTimerInterval = ms; if (timerActive) { d->startSocketTimer(); } } int Session::socketTimeout() const { return d->m_socketTimerInterval; } void Session::setCustomHostname(const QString &hostname) { d->m_customHostname = hostname; } QString Session::customHostname() const { return d->m_customHostname; } void Session::open() { QTimer::singleShot(0, d->m_thread, &SessionThread::reconnect); d->startSocketTimer(); } void Session::openAndWait() { QEventLoop loop(nullptr); d->m_startLoop = &loop; open(); d->m_startLoop->exec(); d->m_startLoop = nullptr; } void Session::quit() { if (d->m_state == Session::Disconnected) { return; } d->setState(Quitting); d->sendData("QUIT"); } void Session::quitAndWait() { if (d->m_state == Session::Disconnected) { return; } QEventLoop loop; connect(this, &Session::stateChanged, this, [&](Session::State state) { if (state == Session::Disconnected) { loop.quit(); } }); d->setState(Quitting); d->sendData("QUIT"); loop.exec(); } void SessionPrivate::setState(Session::State s) { if (m_state == s) { return; } m_state = s; Q_EMIT q->stateChanged(m_state); // After a handshake success or failure, exit the startup event loop if any if (m_startLoop && (m_state == Session::NotAuthenticated || m_state == Session::Disconnected)) { m_startLoop->quit(); } } void SessionPrivate::sendData(const QByteArray &data) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) - QMetaObject::invokeMethod(m_thread, [this, data] { sendData(data); }, Qt::QueuedConnection); -#else QMetaObject::invokeMethod(m_thread, "sendData", Qt::QueuedConnection, Q_ARG(QByteArray, data)); -#endif } void SessionPrivate::responseReceived(const ServerResponse &r) { //qCDebug(KSMTP_LOG) << "S:: [" << r.code() << "]" << (r.isMultiline() ? "-" : " ") << r.text(); if (m_state == Session::Quitting) { m_thread->closeSocket(); return; } if (m_state == Session::Handshake) { if (r.isCode(500) || r.isCode(502)) { if (!m_ehloRejected) { setState(Session::Ready); m_ehloRejected = true; } else { qCWarning(KSMTP_LOG) << "KSmtp::Session: Handshake failed with both EHLO and HELO"; q->quit(); return; } } else if (r.isCode(25)) { if (r.text().startsWith("SIZE ")) { //krazy:exclude=strings m_size = r.text().remove(0, QByteArray("SIZE ").count()).toInt(); } else if (r.text() == "STARTTLS") { m_allowsTls = true; } else if (r.text().startsWith("AUTH ")) { //krazy:exclude=strings setAuthenticationMethods(r.text().remove(0, QByteArray("AUTH ").count()).split(' ')); } if (!r.isMultiline()) { setState(Session::NotAuthenticated); startNext(); } } } if (m_state == Session::Ready) { if (r.isCode(22) || m_ehloRejected) { startHandshake(); return; } } if (m_currentJob) { m_currentJob->handleResponse(r); } } void SessionPrivate::socketConnected() { stopSocketTimer(); setState(Session::Ready); bool useSsl = false; if (!m_queue.isEmpty()) { if (auto login = qobject_cast(m_queue.first())) { useSsl = login->encryptionMode() == LoginJob::SslV2 || login->encryptionMode() == LoginJob::SslV3 || login->encryptionMode() == LoginJob::TlsV1SslV3 || login->encryptionMode() == LoginJob::AnySslVersion; } } if (q->state() == Session::Ready && useSsl) { startNext(); } } void SessionPrivate::socketDisconnected() { qCDebug(KSMTP_LOG) << "Socket disconnected"; setState(Session::Disconnected); m_thread->closeSocket(); if (m_currentJob) { m_currentJob->connectionLost(); } else if (!m_queue.isEmpty()) { m_currentJob = m_queue.takeFirst(); m_currentJob->connectionLost(); } auto copy = m_queue; qDeleteAll(copy); m_queue.clear(); } void SessionPrivate::startSsl(KTcpSocket::SslVersion version) { QMetaObject::invokeMethod(m_thread, "startSsl", Qt::QueuedConnection, Q_ARG(KTcpSocket::SslVersion, version)); } KTcpSocket::SslVersion SessionPrivate::negotiatedEncryption() const { return m_sslVersion; } void SessionPrivate::encryptionNegotiationResult(bool encrypted, KTcpSocket::SslVersion version) { if (encrypted) { // Get the updated auth methods startHandshake(); } m_sslVersion = version; } void SessionPrivate::addJob(Job *job) { m_queue.append(job); //Q_EMIT q->jobQueueSizeChanged( q->jobQueueSize() ); connect(job, &KJob::result, this, &SessionPrivate::jobDone); connect(job, &KJob::destroyed, this, &SessionPrivate::jobDestroyed); if (m_state >= Session::NotAuthenticated) { startNext(); } else { m_thread->reconnect(); } } void SessionPrivate::startNext() { QTimer::singleShot(0, this, [this]() { doStartNext(); }); } void SessionPrivate::doStartNext() { if (m_queue.isEmpty() || m_jobRunning || m_state == Session::Disconnected) { return; } startSocketTimer(); m_jobRunning = true; m_currentJob = m_queue.dequeue(); m_currentJob->doStart(); // sending can take a while depending on bandwidth - don't fail with timeout // if it takes longer if (qobject_cast(m_currentJob)) { stopSocketTimer(); } } void SessionPrivate::jobDone(KJob *job) { Q_UNUSED(job); Q_ASSERT(job == m_currentJob); // If we're in disconnected state it's because we ended up // here because the inactivity timer triggered, so no need to // stop it (it is single shot) if (m_state != Session::Disconnected) { if (!qobject_cast(m_currentJob)) { stopSocketTimer(); } } m_jobRunning = false; m_currentJob = nullptr; //Q_EMIT q->jobQueueSizeChanged( q->jobQueueSize() ); startNext(); } void SessionPrivate::jobDestroyed(QObject *job) { m_queue.removeAll(static_cast(job)); if (m_currentJob == job) { m_currentJob = nullptr; } } void SessionPrivate::startSocketTimer() { if (m_socketTimerInterval < 0) { return; } Q_ASSERT(!m_socketTimer.isActive()); connect(&m_socketTimer, &QTimer::timeout, this, &SessionPrivate::onSocketTimeout); m_socketTimer.setSingleShot(true); m_socketTimer.start(m_socketTimerInterval); } void SessionPrivate::stopSocketTimer() { if (m_socketTimerInterval < 0) { return; } Q_ASSERT(m_socketTimer.isActive()); m_socketTimer.stop(); disconnect(&m_socketTimer, &QTimer::timeout, this, &SessionPrivate::onSocketTimeout); } void SessionPrivate::restartSocketTimer() { stopSocketTimer(); startSocketTimer(); } void SessionPrivate::onSocketTimeout() { socketDisconnected(); } ServerResponse::ServerResponse(int code, const QByteArray &text, bool multiline) : m_text(text), m_code(code), m_multiline(multiline) { } bool ServerResponse::isMultiline() const { return m_multiline; } int ServerResponse::code() const { return m_code; } QByteArray ServerResponse::text() const { return m_text; } bool ServerResponse::isCode(int other) const { int otherCpy = other; int codeLength = 0; if (other == 0) { codeLength = 1; } else { while (otherCpy > 0) { otherCpy /= 10; codeLength++; } } int div = 1; for (int i = 0; i < 3 - codeLength; i++) { div *= 10; } return m_code / div == other; }