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/Job>
16 #include <KIO/SslUi>
17 #include <KLocalizedString>
18 #include <KMessageBox>
19 #include <KPasswordDialog>
20 #include <QRegularExpression>
21 #include <QUrlQuery>
22 #include <kwidgetsaddons_version.h>
23 
24 using namespace KManageSieve;
25 
26 Q_DECLARE_METATYPE(KManageSieve::AuthDetails)
Q_DECLARE_METATYPE(KManageSieve::Response)27 Q_DECLARE_METATYPE(KManageSieve::Response)
28 Q_DECLARE_METATYPE(KSslErrorUiData)
29 
30 Session::Session(QObject *parent)
31     : QObject(parent)
32     , m_thread(new SessionThread(this))
33 {
34     qRegisterMetaType<KManageSieve::AuthDetails>();
35     qRegisterMetaType<KManageSieve::Response>();
36     qRegisterMetaType<KSslErrorUiData>();
37 
38     static int counter = 0;
39     setObjectName(QStringLiteral("session") + QString::number(++counter));
40 
41     connect(m_thread, &SessionThread::responseReceived, this, &Session::processResponse);
42     connect(m_thread, &SessionThread::error, this, &Session::setErrorMessage);
43     connect(m_thread, &SessionThread::authenticationDone, this, &Session::authenticationDone);
44     connect(m_thread, &SessionThread::sslError, this, &Session::sslError);
45     connect(m_thread, &SessionThread::sslDone, this, &Session::sslDone);
46     connect(m_thread, &SessionThread::socketDisconnected, [=]() {
47         m_connected = false;
48         m_disconnected = true;
49     });
50 }
51 
~Session()52 Session::~Session()
53 {
54     qCDebug(KMANAGERSIEVE_LOG) << objectName() << Q_FUNC_INFO;
55     delete m_thread;
56 }
57 
connectToHost(const QUrl & url)58 void Session::connectToHost(const QUrl &url)
59 {
60     qCDebug(KMANAGERSIEVE_LOG) << objectName() << "connect to host url: " << url;
61     m_url = url;
62     m_disconnected = false;
63     m_thread->connectToHost(url);
64     m_state = PreTlsCapabilities;
65 }
66 
disconnectFromHost(bool sendLogout)67 void Session::disconnectFromHost(bool sendLogout)
68 {
69     qCDebug(KMANAGERSIEVE_LOG) << objectName() << "sendLogout=" << sendLogout;
70     m_thread->disconnectFromHost(sendLogout);
71     if (m_currentJob) {
72         killJob(m_currentJob, KJob::EmitResult);
73     }
74     for (SieveJob *job : std::as_const(m_jobs)) {
75         killJob(job, KJob::EmitResult);
76     }
77     deleteLater();
78 }
79 
processResponse(const KManageSieve::Response & response,const QByteArray & data)80 void Session::processResponse(const KManageSieve::Response &response, const QByteArray &data)
81 {
82     switch (m_state) {
83     // should probably be refactored into a capability job
84     case PreTlsCapabilities:
85     case PostTlsCapabilities:
86         if (response.type() == Response::Action) {
87             if (response.operationSuccessful()) {
88                 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Sieve server ready & awaiting authentication.";
89                 if (m_state == PreTlsCapabilities) {
90                     if (!allowUnencrypted() && !QSslSocket::supportsSsl()) {
91                         setErrorMessage(QAbstractSocket::UnknownSocketError, i18n("Cannot use TLS since the underlying Qt library does not support it."));
92                         disconnectFromHost();
93                         return;
94                     }
95                     if (!allowUnencrypted() && QSslSocket::supportsSsl() && !m_supportsStartTls
96                         && KMessageBox::warningContinueCancel(
97                                nullptr,
98                                i18n("TLS encryption was requested, but your Sieve server does not advertise TLS in its capabilities.\n"
99                                     "You can choose to try to initiate TLS negotiations nonetheless, or cancel the operation."),
100                                i18n("Sieve Server Does Not Advertise TLS"),
101                                KGuiItem(i18n("&Start TLS nonetheless")),
102                                KStandardGuiItem::cancel(),
103                                QStringLiteral("ask_starttls_%1").arg(m_url.host()))
104                             != KMessageBox::Continue) {
105                         setErrorMessage(QAbstractSocket::UnknownSocketError, i18n("TLS encryption requested, but not supported by server."));
106                         disconnectFromHost();
107                         return;
108                     }
109 
110                     if (m_supportsStartTls && QSslSocket::supportsSsl()) {
111                         m_state = StartTls;
112                         sendData("STARTTLS");
113                     } else {
114                         m_state = Authenticating;
115                         m_thread->startAuthentication();
116                     }
117                 } else {
118                     m_state = Authenticating;
119                     m_thread->startAuthentication();
120                 }
121             } else {
122                 qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Unknown action " << response.action() << ".";
123             }
124         } else if (response.key() == "IMPLEMENTATION") {
125             m_implementation = QString::fromLatin1(response.value());
126             qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Connected to Sieve server: " << response.value();
127         } else if (response.key() == "SASL") {
128             m_saslMethods = QString::fromLatin1(response.value()).split(QLatin1Char(' '), Qt::SkipEmptyParts);
129             qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Server SASL authentication methods: " << m_saslMethods;
130         } else if (response.key() == "SIEVE") {
131             // Save script capabilities
132             m_sieveExtensions = QString::fromLatin1(response.value()).split(QLatin1Char(' '), Qt::SkipEmptyParts);
133             qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Server script capabilities: " << m_sieveExtensions;
134         } else if (response.key() == "STARTTLS") {
135             qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Server supports TLS";
136             m_supportsStartTls = true;
137         } else {
138             qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Unrecognised key " << response.key();
139         }
140         break;
141     case StartTls:
142         if (response.operationSuccessful()) {
143             m_thread->startSsl();
144             m_state = None;
145         } else {
146             setErrorMessage(QAbstractSocket::UnknownSocketError,
147                             i18n("The server does not seem to support TLS. Disable TLS if you want to connect without encryption."));
148             disconnectFromHost();
149         }
150         break;
151     case Authenticating:
152         m_thread->continueAuthentication(response, data);
153         break;
154     default:
155         if (m_currentJob) {
156             if (m_currentJob->d->handleResponse(response, data)) {
157                 m_currentJob = nullptr;
158                 QMetaObject::invokeMethod(this, &Session::executeNextJob, Qt::QueuedConnection);
159             }
160             break;
161         } else {
162             // we can get here in the kill current job case
163             if (response.operationResult() != Response::Other) {
164                 QMetaObject::invokeMethod(this, &Session::executeNextJob, Qt::QueuedConnection);
165                 return;
166             }
167         }
168         qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Unhandled response! state=" << m_state << "response=" << response.key() << response.value()
169                                    << response.extra() << data;
170     }
171 }
172 
scheduleJob(SieveJob * job)173 void Session::scheduleJob(SieveJob *job)
174 {
175     qCDebug(KMANAGERSIEVE_LOG) << objectName() << Q_FUNC_INFO << job;
176     m_jobs.enqueue(job);
177     QMetaObject::invokeMethod(this, &Session::executeNextJob, Qt::QueuedConnection);
178 }
179 
killJob(SieveJob * job,KJob::KillVerbosity verbosity)180 void Session::killJob(SieveJob *job, KJob::KillVerbosity verbosity)
181 {
182     qCDebug(KMANAGERSIEVE_LOG) << objectName() << Q_FUNC_INFO << "job " << job << " m_currentJob " << m_currentJob << " verbosity " << verbosity;
183     if (m_currentJob == job) {
184         if (verbosity == KJob::EmitResult) {
185             m_currentJob->d->killed();
186         }
187         m_currentJob = nullptr;
188     } else {
189         m_jobs.removeAll(job);
190         if (verbosity == KJob::EmitResult) {
191             job->d->killed();
192         } else {
193             job->deleteLater();
194         }
195     }
196 }
197 
executeNextJob()198 void Session::executeNextJob()
199 {
200     if (!m_connected || m_state != None || m_currentJob || m_jobs.isEmpty()) {
201         return;
202     }
203     m_currentJob = m_jobs.dequeue();
204     qCDebug(KMANAGERSIEVE_LOG) << objectName() << Q_FUNC_INFO << "running job" << m_currentJob;
205     m_currentJob->d->run(this);
206 }
207 
disconnected() const208 bool Session::disconnected() const
209 {
210     return m_disconnected;
211 }
212 
sieveExtensions() const213 QStringList Session::sieveExtensions() const
214 {
215     return m_sieveExtensions;
216 }
217 
requestCapabilitiesAfterStartTls() const218 bool Session::requestCapabilitiesAfterStartTls() const
219 {
220     // Cyrus didn't send CAPABILITIES after STARTTLS until 2.3.11, which is
221     // not standard conform, but we need to support that anyway.
222     // m_implementation looks like this 'Cyrus timsieved v2.2.12' for Cyrus btw.
223     QRegularExpression regExp(QStringLiteral("Cyrus\\stimsieved\\sv(\\d+)\\.(\\d+)\\.(\\d+)([-\\w]*)"), QRegularExpression::CaseInsensitiveOption);
224     QRegularExpressionMatch matchExpression = regExp.match(m_implementation);
225     if (matchExpression.hasMatch()) {
226         const int major = matchExpression.captured(1).toInt();
227         const int minor = matchExpression.captured(2).toInt();
228         const int patch = matchExpression.captured(3).toInt();
229         const QString vendor = matchExpression.captured(4);
230         if (major < 2 || (major == 2 && (minor < 3 || (minor == 3 && patch < 11))) || (vendor == QLatin1String("-kolab-nocaps"))) {
231             qCDebug(KMANAGERSIEVE_LOG) << objectName() << "Enabling compat mode for Cyrus < 2.3.11 or Cyrus marked as \"kolab-nocaps\"";
232             return true;
233         }
234     }
235     return false;
236 }
237 
sendData(const QByteArray & data)238 void Session::sendData(const QByteArray &data)
239 {
240     m_thread->sendData(data);
241 }
242 
requestedSaslMethod() const243 QStringList Session::requestedSaslMethod() const
244 {
245     const QString m = QUrlQuery(m_url).queryItemValue(QStringLiteral("x-mech"));
246     if (!m.isEmpty()) {
247         return QStringList(m);
248     }
249     return m_saslMethods;
250 }
251 
requestAuthDetails(const QUrl & url)252 KManageSieve::AuthDetails Session::requestAuthDetails(const QUrl &url)
253 {
254     KIO::AuthInfo ai;
255     ai.url = url;
256     ai.username = url.userName();
257     ai.password = url.password();
258     ai.keepPassword = true;
259     ai.caption = i18n("Sieve Authentication Details");
260     ai.comment = i18n(
261         "Please enter your authentication details for your sieve account "
262         "(usually the same as your email password):");
263 
264     QPointer<KPasswordDialog> dlg = new KPasswordDialog(nullptr, KPasswordDialog::ShowUsernameLine | KPasswordDialog::ShowKeepPassword);
265     dlg->setRevealPasswordAvailable(KAuthorized::authorize(QStringLiteral("lineedit_reveal_password")));
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->password();
278         ad.password = dlg->password();
279         ad.valid = true;
280     }
281     delete dlg;
282     return ad;
283 }
284 
authenticationDone()285 void 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 
sslError(const KSslErrorUiData & data)293 void 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 
sslDone()303 void 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 
setErrorMessage(int error,const QString & msg)313 void 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
319         if (error != QAbstractSocket::RemoteHostClosedError && error != QAbstractSocket::SocketTimeoutError) {
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), i18n("Sieve Manager"));
322         }
323     }
324 }
325 
allowUnencrypted() const326 bool Session::allowUnencrypted() const
327 {
328     return QUrlQuery(m_url).queryItemValue(QStringLiteral("x-allow-unencrypted")) == QLatin1String("true");
329 }
330