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