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"