Libksieve

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

KDE's Doxygen guidelines are available online.