Libksieve

session.cpp
1/*
2 SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
3 SPDX-FileContributor: Volker Krause <volker.krause@kdab.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "session.h"
9#include "sessionthread_p.h"
10#include "sievejob_p.h"
11
12#include "kmanagersieve_debug.h"
13#include <KAuthorized>
14#include <KIO/AuthInfo>
15#include <KIO/SslUi>
16#include <KLocalizedString>
17#include <KMessageBox>
18#include <KPasswordDialog>
19#include <QRegularExpression>
20#include <QUrlQuery>
21
22using namespace KManageSieve;
23
24Q_DECLARE_METATYPE(KManageSieve::AuthDetails)
25Q_DECLARE_METATYPE(KManageSieve::Response)
26Q_DECLARE_METATYPE(KSslErrorUiData)
27
28Session::Session(QObject *parent)
29 : QObject(parent)
30 , m_thread(new SessionThread(this))
31{
32 qRegisterMetaType<KManageSieve::AuthDetails>();
33 qRegisterMetaType<KManageSieve::Response>();
34 qRegisterMetaType<KSslErrorUiData>();
35
36 static int counter = 0;
37 setObjectName(QLatin1StringView("session") + QString::number(++counter));
38
39 connect(m_thread, &SessionThread::responseReceived, this, &Session::processResponse);
40 connect(m_thread, &SessionThread::error, this, &Session::setErrorMessage);
41 connect(m_thread, &SessionThread::authenticationDone, this, &Session::authenticationDone);
42 connect(m_thread, &SessionThread::sslError, this, &Session::sslError);
43 connect(m_thread, &SessionThread::sslDone, this, &Session::sslDone);
44 connect(m_thread, &SessionThread::socketDisconnected, [this]() {
45 m_connected = false;
46 m_disconnected = true;
47 });
48}
49
50Session::~Session()
51{
52 qCDebug(KMANAGERSIEVE_LOG) << objectName() << Q_FUNC_INFO;
53 delete m_thread;
54}
55
56void Session::connectToHost(const QUrl &url)
57{
58 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "connect to host url: " << url;
59 m_url = url;
60 m_disconnected = false;
61 m_thread->connectToHost(url);
62 m_state = PreTlsCapabilities;
63}
64
65void Session::disconnectFromHost(bool sendLogout)
66{
67 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "sendLogout=" << sendLogout;
68 m_thread->disconnectFromHost(sendLogout);
69 if (m_currentJob) {
70 killJob(m_currentJob, KJob::EmitResult);
71 }
72 for (SieveJob *job : std::as_const(m_jobs)) {
73 killJob(job, KJob::EmitResult);
74 }
76}
77
78void Session::processResponse(const KManageSieve::Response &response, const QByteArray &data)
79{
80 switch (m_state) {
81 // should probably be refactored into a capability job
82 case PreTlsCapabilities:
83 case PostTlsCapabilities:
84 if (response.type() == Response::Action) {
85 if (response.operationSuccessful()) {
86 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Sieve server ready & awaiting authentication.";
87 if (m_state == PreTlsCapabilities) {
88 if (!allowUnencrypted() && !QSslSocket::supportsSsl()) {
89 setErrorMessage(QAbstractSocket::UnknownSocketError, i18n("Cannot use TLS since the underlying Qt library does not support it."));
90 disconnectFromHost();
91 return;
92 }
93 if (!allowUnencrypted() && QSslSocket::supportsSsl() && !m_supportsStartTls
95 nullptr,
96 i18n("TLS encryption was requested, but your Sieve server does not advertise TLS in its capabilities.\n"
97 "You can choose to try to initiate TLS negotiations nonetheless, or cancel the operation."),
98 i18nc("@title:window", "Sieve Server Does Not Advertise TLS"),
99 KGuiItem(i18n("&Start TLS nonetheless")),
101 QStringLiteral("ask_starttls_%1").arg(m_url.host()))
103 setErrorMessage(QAbstractSocket::UnknownSocketError, i18n("TLS encryption requested, but not supported by server."));
104 disconnectFromHost();
105 return;
106 }
107
108 if (m_supportsStartTls && QSslSocket::supportsSsl()) {
109 m_state = StartTls;
110 sendData("STARTTLS");
111 } else {
112 m_state = Authenticating;
113 m_thread->startAuthentication();
114 }
115 } else {
116 m_state = Authenticating;
117 m_thread->startAuthentication();
118 }
119 } else {
120 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Unknown action " << response.action() << ".";
121 }
122 } else if (response.key() == "IMPLEMENTATION") {
123 m_implementation = QString::fromLatin1(response.value());
124 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Connected to Sieve server: " << response.value();
125 } else if (response.key() == "SASL") {
126 m_saslMethods = QString::fromLatin1(response.value()).split(QLatin1Char(' '), Qt::SkipEmptyParts);
127 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Server SASL authentication methods: " << m_saslMethods;
128 } else if (response.key() == "SIEVE") {
129 // Save script capabilities
130 m_sieveExtensions = QString::fromLatin1(response.value()).split(QLatin1Char(' '), Qt::SkipEmptyParts);
131 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Server script capabilities: " << m_sieveExtensions;
132 } else if (response.key() == "STARTTLS") {
133 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Server supports TLS";
134 m_supportsStartTls = true;
135 } else {
136 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Unrecognised key " << response.key();
137 }
138 break;
139 case StartTls:
140 if (response.operationSuccessful()) {
141 m_thread->startSsl();
142 m_state = None;
143 } else {
145 i18n("The server does not seem to support TLS. Disable TLS if you want to connect without encryption."));
146 disconnectFromHost();
147 }
148 break;
149 case Authenticating:
150 m_thread->continueAuthentication(response, data);
151 break;
152 default:
153 if (m_currentJob) {
154 if (m_currentJob->d->handleResponse(response, data)) {
155 m_currentJob = nullptr;
156 QMetaObject::invokeMethod(this, &Session::executeNextJob, Qt::QueuedConnection);
157 }
158 break;
159 } else {
160 // we can get here in the kill current job case
161 if (response.operationResult() != Response::Other) {
162 QMetaObject::invokeMethod(this, &Session::executeNextJob, Qt::QueuedConnection);
163 return;
164 }
165 }
166 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Unhandled response! state=" << m_state << "response=" << response.key() << response.value()
167 << response.extra() << data;
168 }
169}
170
171void Session::scheduleJob(SieveJob *job)
172{
173 qCDebug(KMANAGERSIEVE_LOG) << objectName() << Q_FUNC_INFO << job;
174 m_jobs.enqueue(job);
175 QMetaObject::invokeMethod(this, &Session::executeNextJob, Qt::QueuedConnection);
176}
177
178void Session::killJob(SieveJob *job, KJob::KillVerbosity verbosity)
179{
180 qCDebug(KMANAGERSIEVE_LOG) << objectName() << Q_FUNC_INFO << "job " << job << " m_currentJob " << m_currentJob << " verbosity " << verbosity;
181 if (m_currentJob == job) {
182 if (verbosity == KJob::EmitResult) {
183 m_currentJob->d->killed();
184 }
185 m_currentJob = nullptr;
186 } else {
187 m_jobs.removeAll(job);
188 if (verbosity == KJob::EmitResult) {
189 job->d->killed();
190 } else {
191 job->deleteLater();
192 }
193 }
194}
195
196void Session::executeNextJob()
197{
198 if (!m_connected || m_state != None || m_currentJob || m_jobs.isEmpty()) {
199 return;
200 }
201 m_currentJob = m_jobs.dequeue();
202 qCDebug(KMANAGERSIEVE_LOG) << objectName() << Q_FUNC_INFO << "running job" << m_currentJob;
203 m_currentJob->d->run(this);
204}
205
206bool Session::disconnected() const
207{
208 return m_disconnected;
209}
210
211QStringList Session::sieveExtensions() const
212{
213 return m_sieveExtensions;
214}
215
216bool Session::requestCapabilitiesAfterStartTls() const
217{
218 // Cyrus didn't send CAPABILITIES after STARTTLS until 2.3.11, which is
219 // not standard conform, but we need to support that anyway.
220 // m_implementation looks like this 'Cyrus timsieved v2.2.12' for Cyrus btw.
221 QRegularExpression regExp(QStringLiteral("Cyrus\\stimsieved\\sv(\\d+)\\.(\\d+)\\.(\\d+)([-\\w]*)"), QRegularExpression::CaseInsensitiveOption);
222 QRegularExpressionMatch matchExpression = regExp.match(m_implementation);
223 if (matchExpression.hasMatch()) {
224 const int major = matchExpression.captured(1).toInt();
225 const int minor = matchExpression.captured(2).toInt();
226 const int patch = matchExpression.captured(3).toInt();
227 const QString vendor = matchExpression.captured(4);
228 if (major < 2 || (major == 2 && (minor < 3 || (minor == 3 && patch < 11))) || (vendor == QLatin1StringView("-kolab-nocaps"))) {
229 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Enabling compat mode for Cyrus < 2.3.11 or Cyrus marked as \"kolab-nocaps\"";
230 return true;
231 }
232 }
233 return false;
234}
235
236void Session::sendData(const QByteArray &data)
237{
238 m_thread->sendData(data);
239}
240
241QStringList Session::requestedSaslMethod() const
242{
243 const QString m = QUrlQuery(m_url).queryItemValue(QStringLiteral("x-mech"));
244 if (!m.isEmpty()) {
245 return QStringList(m);
246 }
247 return m_saslMethods;
248}
249
250KManageSieve::AuthDetails Session::requestAuthDetails(const QUrl &url)
251{
252 KIO::AuthInfo ai;
253 ai.url = url;
254 ai.username = url.userName();
255 ai.password = url.password();
256 ai.keepPassword = true;
257 ai.caption = i18n("Sieve Authentication Details");
258 ai.comment = i18n(
259 "Please enter your authentication details for your sieve account \"%1\""
260 " (usually the same as your email password):",
261 url.host());
262
264 dlg->setRevealPasswordMode(KAuthorized::authorize(QStringLiteral("lineedit_reveal_password")) ? KPassword::RevealMode::OnlyNew
265 : KPassword::RevealMode::Never);
266 dlg->setUsername(ai.username);
267 dlg->setPassword(ai.password);
268 dlg->setKeepPassword(ai.keepPassword);
269 dlg->setPrompt(ai.prompt);
270 dlg->setUsernameReadOnly(ai.readOnly);
271 dlg->setWindowTitle(ai.caption);
272 dlg->addCommentLine(ai.commentLabel, ai.comment);
273
274 AuthDetails ad;
275 ad.valid = false;
276 if (dlg->exec()) {
277 ad.username = dlg->username();
278 ad.password = dlg->password();
279 ad.valid = true;
280 }
281 delete dlg;
282 return ad;
283}
284
285void Session::authenticationDone()
286{
287 m_state = None;
288 m_connected = true;
289 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "authentication done, ready to execute jobs";
290 QMetaObject::invokeMethod(this, &Session::executeNextJob, Qt::QueuedConnection);
291}
292
293void Session::sslError(const KSslErrorUiData &data)
294{
295 const bool ignore = KIO::SslUi::askIgnoreSslErrors(data);
296 if (ignore) {
297 sslDone();
298 } else {
299 m_thread->disconnectFromHost(true);
300 }
301}
302
303void Session::sslDone()
304{
305 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "TLS negotiation done.";
306 if (requestCapabilitiesAfterStartTls()) {
307 sendData("CAPABILITY");
308 }
309 m_state = PostTlsCapabilities;
310 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "TLS negotiation done, m_state=" << m_state;
311}
312
313void Session::setErrorMessage(int error, const QString &msg)
314{
315 if (m_currentJob) {
316 m_currentJob->setErrorMessage(msg);
317 } else {
318 // Don't bother the user about idle timeout
320 qCWarning(KMANAGERSIEVE_LOG) << objectName() << "No job for reporting this error message!" << msg << "host" << m_url.host() << "error" << error;
321 KMessageBox::error(nullptr, i18n("The Sieve server on %1 has reported an error:\n%2", m_url.host(), msg), i18nc("@title:window", "Sieve Manager"));
322 }
323 }
324}
325
326bool Session::allowUnencrypted() const
327{
328 return QUrlQuery(m_url).queryItemValue(QStringLiteral("x-allow-unencrypted")) == QLatin1StringView("true");
329}
330
331#include "moc_session.cpp"
static Q_INVOKABLE bool authorize(const QString &action)
QString comment
QString caption
QString username
QString password
QString prompt
QString commentLabel
A response from a managesieve server.
Definition response.h:18
A job to manage sieve scripts.
Definition sievejob.h:31
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
KCRASH_EXPORT void setErrorMessage(const QString &message)
bool KIOWIDGETS_EXPORT askIgnoreSslErrors(const KSslErrorUiData &uiData, RulesStorage storedRules=RecallAndStoreRules)
ButtonCode warningContinueCancel(QWidget *parent, const QString &text, const QString &title=QString(), const KGuiItem &buttonContinue=KStandardGuiItem::cont(), const KGuiItem &buttonCancel=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Notify)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
KGuiItem cancel()
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
void deleteLater()
QString captured(QStringView name) const const
bool hasMatch() const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QString number(double n, char format, int precision)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
int toInt(bool *ok, int base) const const
QueuedConnection
SkipEmptyParts
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QString host(ComponentFormattingOptions options) const const
QString password(ComponentFormattingOptions options) const const
QString userName(ComponentFormattingOptions options) const const
QString queryItemValue(const QString &key, QUrl::ComponentFormattingOptions encoding) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:17:19 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.