diff --git a/src/session.cpp b/src/session.cpp index 0be26a9..940ad10 100644 --- a/src/session.cpp +++ b/src/session.cpp @@ -1,519 +1,523 @@ /* 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) { QMetaObject::invokeMethod(m_thread, "sendData", Qt::QueuedConnection, Q_ARG(QByteArray, data)); } 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) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + QMetaObject::invokeMethod(m_thread, [this, version] {m_thread->startSsl(version); }, Qt::QueuedConnection); +#else QMetaObject::invokeMethod(m_thread, "startSsl", Qt::QueuedConnection, Q_ARG(KTcpSocket::SslVersion, version)); +#endif } 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; } diff --git a/src/sessionthread.cpp b/src/sessionthread.cpp index c195826..c291ba3 100644 --- a/src/sessionthread.cpp +++ b/src/sessionthread.cpp @@ -1,251 +1,255 @@ /* 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 "sessionthread_p.h" #include "serverresponse_p.h" #include "session.h" #include "session_p.h" #include "ksmtp_debug.h" #include #include #include #include using namespace KSmtp; SessionThread::SessionThread(const QString &hostName, quint16 port, Session *session) : QThread(), m_socket(nullptr), m_logFile(nullptr), m_parentSession(session), m_hostName(hostName), m_port(port) { moveToThread(this); const auto logfile = qgetenv("KSMTP_SESSION_LOG"); if (!logfile.isEmpty()) { static uint sSessionCount = 0; const QString filename = QStringLiteral("%1.%2.%3").arg(QString::fromUtf8(logfile)) .arg(qApp->applicationPid()) .arg(++sSessionCount); m_logFile = new QFile(filename); if (!m_logFile->open(QIODevice::WriteOnly | QIODevice::Truncate)) { qCWarning(KSMTP_LOG) << "Failed to open log file" << filename << ":" << m_logFile->errorString(); delete m_logFile; m_logFile = nullptr; } } } SessionThread::~SessionThread() { delete m_logFile; } QString SessionThread::hostName() const { return m_hostName; } quint16 SessionThread::port() const { return m_port; } void SessionThread::sendData(const QByteArray &payload) { QMutexLocker locker(&m_mutex); //qCDebug(KSMTP_LOG) << "C:: " << payload; if (m_logFile) { m_logFile->write("C: " + payload + '\n'); m_logFile->flush(); } m_dataQueue.enqueue(payload + "\r\n"); QTimer::singleShot(0, this, &SessionThread::writeDataQueue); } void SessionThread::writeDataQueue() { QMutexLocker locker(&m_mutex); while (!m_dataQueue.isEmpty()) { m_socket->write(m_dataQueue.dequeue()); } } void SessionThread::readResponse() { QMutexLocker locker(&m_mutex); if (!m_socket->bytesAvailable()) { return; } const QByteArray data = m_socket->readLine(); //qCDebug(KSMTP_LOG) << "S:" << data; if (m_logFile) { m_logFile->write("S: " + data); m_logFile->flush(); } const ServerResponse response = parseResponse(data); Q_EMIT responseReceived(response); if (m_socket->bytesAvailable()) { QTimer::singleShot(0, this, &SessionThread::readResponse); } } void SessionThread::closeSocket() { QTimer::singleShot(0, this, &SessionThread::doCloseSocket); } void SessionThread::doCloseSocket() { m_socket->close(); } void SessionThread::reconnect() { QMutexLocker locker(&m_mutex); if (m_socket->state() != KTcpSocket::ConnectedState && m_socket->state() != KTcpSocket::ConnectingState) { m_socket->connectToHost(hostName(), port()); } } void SessionThread::run() { m_socket = new KTcpSocket; connect(m_socket, &KTcpSocket::readyRead, this, &SessionThread::readResponse, Qt::QueuedConnection); connect(m_socket, &KTcpSocket::disconnected, m_parentSession->d, &SessionPrivate::socketDisconnected); connect(m_socket, &KTcpSocket::connected, m_parentSession->d, &SessionPrivate::socketConnected); connect(m_socket, static_cast(&KTcpSocket::error), this, [this](KTcpSocket::Error err) { qCWarning(KSMTP_LOG) << "Socket error:" << err << m_socket->errorString(); Q_EMIT m_parentSession->connectionError(m_socket->errorString()); }); connect(this, &SessionThread::encryptionNegotiationResult, m_parentSession->d, &SessionPrivate::encryptionNegotiationResult); connect(this, &SessionThread::responseReceived, m_parentSession->d, &SessionPrivate::responseReceived); exec(); delete m_socket; } ServerResponse SessionThread::parseResponse(const QByteArray &resp) { QByteArray response(resp); // Remove useless CRLF int indexOfCR = response.indexOf("\r"); int indexOfLF = response.indexOf("\n"); if (indexOfCR > 0) { response.truncate(indexOfCR); } if (indexOfLF > 0) { response.truncate(indexOfLF); } // Server response code QByteArray code = response.left(3); bool ok = false; const int returnCode = code.toInt(&ok); if (!ok) { return ServerResponse(); } // RFC821, Appendix E const bool multiline = (response.at(3) == '-'); if (returnCode) { response = response.mid(4); // Keep the text part } return ServerResponse(returnCode, response, multiline); } void SessionThread::startSsl(KTcpSocket::SslVersion version) { QMutexLocker locker(&m_mutex); m_socket->setAdvertisedSslVersion(version); m_socket->ignoreSslErrors(); connect(m_socket, &KTcpSocket::encrypted, this, &SessionThread::sslConnected); m_socket->startClientEncryption(); } void SessionThread::sslConnected() { QMutexLocker locker(&m_mutex); KSslCipher cipher = m_socket->sessionCipher(); if (!m_socket->sslErrors().isEmpty() || m_socket->encryptionMode() != KTcpSocket::SslClientMode || cipher.isNull() || cipher.usedBits() == 0) { qCDebug(KSMTP_LOG) << "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."; KSslErrorUiData errorData(m_socket); Q_EMIT sslError(errorData); } else { qCDebug(KSMTP_LOG) << "TLS negotiation done."; Q_EMIT encryptionNegotiationResult(true, m_socket->negotiatedSslVersion()); } } void SessionThread::handleSslErrorResponse(bool ignoreError) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + QMetaObject::invokeMethod(this, [this, ignoreError] {doHandleSslErrorResponse(ignoreError); }, Qt::QueuedConnection); +#else QMetaObject::invokeMethod(this, "doHandleSslErrorResponse", Qt::QueuedConnection, Q_ARG(bool, ignoreError)); +#endif } void SessionThread::doHandleSslErrorResponse(bool ignoreError) { Q_ASSERT(QThread::currentThread() == thread()); if (!m_socket) { return; } if (ignoreError) { Q_EMIT encryptionNegotiationResult(true, m_socket->negotiatedSslVersion()); } else { //reconnect in unencrypted mode, so new commands can be issued m_socket->disconnectFromHost(); m_socket->waitForDisconnected(); m_socket->connectToHost(m_hostName, m_port); Q_EMIT encryptionNegotiationResult(false, KTcpSocket::UnknownSslVersion); } }