File indexing completed on 2024-12-22 05:03:15

0001 /*
0002     SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
0003     SPDX-FileContributor: Volker Krause <volker.krause@kdab.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "session.h"
0009 #include "sessionthread_p.h"
0010 #include "sievejob_p.h"
0011 
0012 #include "kmanagersieve_debug.h"
0013 #include <KAuthorized>
0014 #include <KIO/AuthInfo>
0015 #include <KIO/SslUi>
0016 #include <KLocalizedString>
0017 #include <KMessageBox>
0018 #include <KPasswordDialog>
0019 #include <QRegularExpression>
0020 #include <QUrlQuery>
0021 
0022 using namespace KManageSieve;
0023 
0024 Q_DECLARE_METATYPE(KManageSieve::AuthDetails)
0025 Q_DECLARE_METATYPE(KManageSieve::Response)
0026 Q_DECLARE_METATYPE(KSslErrorUiData)
0027 
0028 Session::Session(QObject *parent)
0029     : QObject(parent)
0030     , m_thread(new SessionThread(this))
0031 {
0032     qRegisterMetaType<KManageSieve::AuthDetails>();
0033     qRegisterMetaType<KManageSieve::Response>();
0034     qRegisterMetaType<KSslErrorUiData>();
0035 
0036     static int counter = 0;
0037     setObjectName(QLatin1StringView("session") + QString::number(++counter));
0038 
0039     connect(m_thread, &SessionThread::responseReceived, this, &Session::processResponse);
0040     connect(m_thread, &SessionThread::error, this, &Session::setErrorMessage);
0041     connect(m_thread, &SessionThread::authenticationDone, this, &Session::authenticationDone);
0042     connect(m_thread, &SessionThread::sslError, this, &Session::sslError);
0043     connect(m_thread, &SessionThread::sslDone, this, &Session::sslDone);
0044     connect(m_thread, &SessionThread::socketDisconnected, [this]() {
0045         m_connected = false;
0046         m_disconnected = true;
0047     });
0048 }
0049 
0050 Session::~Session()
0051 {
0052     qCDebug(KMANAGERSIEVE_LOG) << objectName() << Q_FUNC_INFO;
0053     delete m_thread;
0054 }
0055 
0056 void Session::connectToHost(const QUrl &url)
0057 {
0058     qCDebug(KMANAGERSIEVE_LOG) << objectName() << "connect to host url: " << url;
0059     m_url = url;
0060     m_disconnected = false;
0061     m_thread->connectToHost(url);
0062     m_state = PreTlsCapabilities;
0063 }
0064 
0065 void Session::disconnectFromHost(bool sendLogout)
0066 {
0067     qCDebug(KMANAGERSIEVE_LOG) << objectName() << "sendLogout=" << sendLogout;
0068     m_thread->disconnectFromHost(sendLogout);
0069     if (m_currentJob) {
0070         killJob(m_currentJob, KJob::EmitResult);
0071     }
0072     for (SieveJob *job : std::as_const(m_jobs)) {
0073         killJob(job, KJob::EmitResult);
0074     }
0075     deleteLater();
0076 }
0077 
0078 void Session::processResponse(const KManageSieve::Response &response, const QByteArray &data)
0079 {
0080     switch (m_state) {
0081     // should probably be refactored into a capability job
0082     case PreTlsCapabilities:
0083     case PostTlsCapabilities:
0084         if (response.type() == Response::Action) {
0085             if (response.operationSuccessful()) {
0086                 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Sieve server ready & awaiting authentication.";
0087                 if (m_state == PreTlsCapabilities) {
0088                     if (!allowUnencrypted() && !QSslSocket::supportsSsl()) {
0089                         setErrorMessage(QAbstractSocket::UnknownSocketError, i18n("Cannot use TLS since the underlying Qt library does not support it."));
0090                         disconnectFromHost();
0091                         return;
0092                     }
0093                     if (!allowUnencrypted() && QSslSocket::supportsSsl() && !m_supportsStartTls
0094                         && KMessageBox::warningContinueCancel(
0095                                nullptr,
0096                                i18n("TLS encryption was requested, but your Sieve server does not advertise TLS in its capabilities.\n"
0097                                     "You can choose to try to initiate TLS negotiations nonetheless, or cancel the operation."),
0098                                i18nc("@title:window", "Sieve Server Does Not Advertise TLS"),
0099                                KGuiItem(i18n("&Start TLS nonetheless")),
0100                                KStandardGuiItem::cancel(),
0101                                QStringLiteral("ask_starttls_%1").arg(m_url.host()))
0102                             != KMessageBox::Continue) {
0103                         setErrorMessage(QAbstractSocket::UnknownSocketError, i18n("TLS encryption requested, but not supported by server."));
0104                         disconnectFromHost();
0105                         return;
0106                     }
0107 
0108                     if (m_supportsStartTls && QSslSocket::supportsSsl()) {
0109                         m_state = StartTls;
0110                         sendData("STARTTLS");
0111                     } else {
0112                         m_state = Authenticating;
0113                         m_thread->startAuthentication();
0114                     }
0115                 } else {
0116                     m_state = Authenticating;
0117                     m_thread->startAuthentication();
0118                 }
0119             } else {
0120                 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Unknown action " << response.action() << ".";
0121             }
0122         } else if (response.key() == "IMPLEMENTATION") {
0123             m_implementation = QString::fromLatin1(response.value());
0124             qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Connected to Sieve server: " << response.value();
0125         } else if (response.key() == "SASL") {
0126             m_saslMethods = QString::fromLatin1(response.value()).split(QLatin1Char(' '), Qt::SkipEmptyParts);
0127             qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Server SASL authentication methods: " << m_saslMethods;
0128         } else if (response.key() == "SIEVE") {
0129             // Save script capabilities
0130             m_sieveExtensions = QString::fromLatin1(response.value()).split(QLatin1Char(' '), Qt::SkipEmptyParts);
0131             qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Server script capabilities: " << m_sieveExtensions;
0132         } else if (response.key() == "STARTTLS") {
0133             qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Server supports TLS";
0134             m_supportsStartTls = true;
0135         } else {
0136             qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Unrecognised key " << response.key();
0137         }
0138         break;
0139     case StartTls:
0140         if (response.operationSuccessful()) {
0141             m_thread->startSsl();
0142             m_state = None;
0143         } else {
0144             setErrorMessage(QAbstractSocket::UnknownSocketError,
0145                             i18n("The server does not seem to support TLS. Disable TLS if you want to connect without encryption."));
0146             disconnectFromHost();
0147         }
0148         break;
0149     case Authenticating:
0150         m_thread->continueAuthentication(response, data);
0151         break;
0152     default:
0153         if (m_currentJob) {
0154             if (m_currentJob->d->handleResponse(response, data)) {
0155                 m_currentJob = nullptr;
0156                 QMetaObject::invokeMethod(this, &Session::executeNextJob, Qt::QueuedConnection);
0157             }
0158             break;
0159         } else {
0160             // we can get here in the kill current job case
0161             if (response.operationResult() != Response::Other) {
0162                 QMetaObject::invokeMethod(this, &Session::executeNextJob, Qt::QueuedConnection);
0163                 return;
0164             }
0165         }
0166         qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Unhandled response! state=" << m_state << "response=" << response.key() << response.value()
0167                                    << response.extra() << data;
0168     }
0169 }
0170 
0171 void Session::scheduleJob(SieveJob *job)
0172 {
0173     qCDebug(KMANAGERSIEVE_LOG) << objectName() << Q_FUNC_INFO << job;
0174     m_jobs.enqueue(job);
0175     QMetaObject::invokeMethod(this, &Session::executeNextJob, Qt::QueuedConnection);
0176 }
0177 
0178 void Session::killJob(SieveJob *job, KJob::KillVerbosity verbosity)
0179 {
0180     qCDebug(KMANAGERSIEVE_LOG) << objectName() << Q_FUNC_INFO << "job " << job << " m_currentJob " << m_currentJob << " verbosity " << verbosity;
0181     if (m_currentJob == job) {
0182         if (verbosity == KJob::EmitResult) {
0183             m_currentJob->d->killed();
0184         }
0185         m_currentJob = nullptr;
0186     } else {
0187         m_jobs.removeAll(job);
0188         if (verbosity == KJob::EmitResult) {
0189             job->d->killed();
0190         } else {
0191             job->deleteLater();
0192         }
0193     }
0194 }
0195 
0196 void Session::executeNextJob()
0197 {
0198     if (!m_connected || m_state != None || m_currentJob || m_jobs.isEmpty()) {
0199         return;
0200     }
0201     m_currentJob = m_jobs.dequeue();
0202     qCDebug(KMANAGERSIEVE_LOG) << objectName() << Q_FUNC_INFO << "running job" << m_currentJob;
0203     m_currentJob->d->run(this);
0204 }
0205 
0206 bool Session::disconnected() const
0207 {
0208     return m_disconnected;
0209 }
0210 
0211 QStringList Session::sieveExtensions() const
0212 {
0213     return m_sieveExtensions;
0214 }
0215 
0216 bool Session::requestCapabilitiesAfterStartTls() const
0217 {
0218     // Cyrus didn't send CAPABILITIES after STARTTLS until 2.3.11, which is
0219     // not standard conform, but we need to support that anyway.
0220     // m_implementation looks like this 'Cyrus timsieved v2.2.12' for Cyrus btw.
0221     QRegularExpression regExp(QStringLiteral("Cyrus\\stimsieved\\sv(\\d+)\\.(\\d+)\\.(\\d+)([-\\w]*)"), QRegularExpression::CaseInsensitiveOption);
0222     QRegularExpressionMatch matchExpression = regExp.match(m_implementation);
0223     if (matchExpression.hasMatch()) {
0224         const int major = matchExpression.captured(1).toInt();
0225         const int minor = matchExpression.captured(2).toInt();
0226         const int patch = matchExpression.captured(3).toInt();
0227         const QString vendor = matchExpression.captured(4);
0228         if (major < 2 || (major == 2 && (minor < 3 || (minor == 3 && patch < 11))) || (vendor == QLatin1StringView("-kolab-nocaps"))) {
0229             qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Enabling compat mode for Cyrus < 2.3.11 or Cyrus marked as \"kolab-nocaps\"";
0230             return true;
0231         }
0232     }
0233     return false;
0234 }
0235 
0236 void Session::sendData(const QByteArray &data)
0237 {
0238     m_thread->sendData(data);
0239 }
0240 
0241 QStringList Session::requestedSaslMethod() const
0242 {
0243     const QString m = QUrlQuery(m_url).queryItemValue(QStringLiteral("x-mech"));
0244     if (!m.isEmpty()) {
0245         return QStringList(m);
0246     }
0247     return m_saslMethods;
0248 }
0249 
0250 KManageSieve::AuthDetails Session::requestAuthDetails(const QUrl &url)
0251 {
0252     KIO::AuthInfo ai;
0253     ai.url = url;
0254     ai.username = url.userName();
0255     ai.password = url.password();
0256     ai.keepPassword = true;
0257     ai.caption = i18n("Sieve Authentication Details");
0258     ai.comment = i18n(
0259         "Please enter your authentication details for your sieve account \"%1\""
0260         " (usually the same as your email password):",
0261         url.host());
0262 
0263     QPointer<KPasswordDialog> dlg = new KPasswordDialog(nullptr, KPasswordDialog::ShowUsernameLine | KPasswordDialog::ShowKeepPassword);
0264     dlg->setRevealPasswordAvailable(KAuthorized::authorize(QStringLiteral("lineedit_reveal_password")));
0265     dlg->setUsername(ai.username);
0266     dlg->setPassword(ai.password);
0267     dlg->setKeepPassword(ai.keepPassword);
0268     dlg->setPrompt(ai.prompt);
0269     dlg->setUsernameReadOnly(ai.readOnly);
0270     dlg->setWindowTitle(ai.caption);
0271     dlg->addCommentLine(ai.commentLabel, ai.comment);
0272 
0273     AuthDetails ad;
0274     ad.valid = false;
0275     if (dlg->exec()) {
0276         ad.username = dlg->username();
0277         ad.password = dlg->password();
0278         ad.valid = true;
0279     }
0280     delete dlg;
0281     return ad;
0282 }
0283 
0284 void Session::authenticationDone()
0285 {
0286     m_state = None;
0287     m_connected = true;
0288     qCDebug(KMANAGERSIEVE_LOG) << objectName() << "authentication done, ready to execute jobs";
0289     QMetaObject::invokeMethod(this, &Session::executeNextJob, Qt::QueuedConnection);
0290 }
0291 
0292 void Session::sslError(const KSslErrorUiData &data)
0293 {
0294     const bool ignore = KIO::SslUi::askIgnoreSslErrors(data);
0295     if (ignore) {
0296         sslDone();
0297     } else {
0298         m_thread->disconnectFromHost(true);
0299     }
0300 }
0301 
0302 void Session::sslDone()
0303 {
0304     qCDebug(KMANAGERSIEVE_LOG) << objectName() << "TLS negotiation done.";
0305     if (requestCapabilitiesAfterStartTls()) {
0306         sendData("CAPABILITY");
0307     }
0308     m_state = PostTlsCapabilities;
0309     qCDebug(KMANAGERSIEVE_LOG) << objectName() << "TLS negotiation done, m_state=" << m_state;
0310 }
0311 
0312 void Session::setErrorMessage(int error, const QString &msg)
0313 {
0314     if (m_currentJob) {
0315         m_currentJob->setErrorMessage(msg);
0316     } else {
0317         // Don't bother the user about idle timeout
0318         if (error != QAbstractSocket::RemoteHostClosedError && error != QAbstractSocket::SocketTimeoutError) {
0319             qCWarning(KMANAGERSIEVE_LOG) << objectName() << "No job for reporting this error message!" << msg << "host" << m_url.host() << "error" << error;
0320             KMessageBox::error(nullptr, i18n("The Sieve server on %1 has reported an error:\n%2", m_url.host(), msg), i18nc("@title:window", "Sieve Manager"));
0321         }
0322     }
0323 }
0324 
0325 bool Session::allowUnencrypted() const
0326 {
0327     return QUrlQuery(m_url).queryItemValue(QStringLiteral("x-allow-unencrypted")) == QLatin1StringView("true");
0328 }
0329 
0330 #include "moc_session.cpp"