KIMAP

session.cpp
1 /*
2  SPDX-FileCopyrightText: 2009 Kevin Ottens <[email protected]>
3 
4  SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
5  SPDX-FileContributor: Kevin Ottens <[email protected]>
6 
7  SPDX-License-Identifier: LGPL-2.0-or-later
8 */
9 
10 #include "session.h"
11 #include "session_p.h"
12 
13 #include <QPointer>
14 #include <QTimer>
15 
16 #include "kimap_debug.h"
17 
18 #include "job.h"
19 #include "job_p.h"
20 #include "loginjob.h"
21 #include "response_p.h"
22 #include "rfccodecs.h"
23 #include "sessionlogger_p.h"
24 #include "sessionthread_p.h"
25 
26 Q_DECLARE_METATYPE(QSsl::SslProtocol)
27 Q_DECLARE_METATYPE(QSslSocket::SslMode)
28 static const int _kimap_sslVersionId = qRegisterMetaType<QSsl::SslProtocol>();
29 
30 using namespace KIMAP;
31 
32 Session::Session(const QString &hostName, quint16 port, QObject *parent)
33  : QObject(parent)
34  , d(new SessionPrivate(this))
35 {
36  if (!qEnvironmentVariableIsEmpty("KIMAP_LOGFILE")) {
37  d->logger = new SessionLogger;
38  }
39 
40  d->isSocketConnected = false;
41  d->state = Disconnected;
42  d->jobRunning = false;
43 
44  d->thread = new SessionThread(hostName, port);
45  connect(d->thread, &SessionThread::encryptionNegotiationResult, d, &SessionPrivate::onEncryptionNegotiationResult);
46  connect(d->thread, &SessionThread::sslError, d, &SessionPrivate::handleSslError);
47  connect(d->thread, &SessionThread::socketDisconnected, d, &SessionPrivate::socketDisconnected);
48  connect(d->thread, &SessionThread::responseReceived, d, &SessionPrivate::responseReceived);
49  connect(d->thread, &SessionThread::socketConnected, d, &SessionPrivate::socketConnected);
50  connect(d->thread, &SessionThread::socketActivity, d, &SessionPrivate::socketActivity);
51  connect(d->thread, &SessionThread::socketError, d, &SessionPrivate::socketError);
52 
53  d->socketTimer.setSingleShot(true);
54  connect(&d->socketTimer, &QTimer::timeout, d, &SessionPrivate::onSocketTimeout);
55 
56  d->startSocketTimer();
57 }
58 
59 Session::~Session()
60 {
61  // Make sure all jobs know we're done
62  d->socketDisconnected();
63  delete d->thread;
64  d->thread = nullptr;
65 }
66 
67 void Session::setUiProxy(const SessionUiProxy::Ptr &proxy)
68 {
69  d->uiProxy = proxy;
70 }
71 
72 void Session::setUiProxy(SessionUiProxy *proxy)
73 {
74  setUiProxy(SessionUiProxy::Ptr(proxy));
75 }
76 
77 QString Session::hostName() const
78 {
79  return d->thread->hostName();
80 }
81 
82 quint16 Session::port() const
83 {
84  return d->thread->port();
85 }
86 
87 void Session::setUseNetworkProxy(bool useProxy)
88 {
89  d->thread->setUseNetworkProxy(useProxy);
90 }
91 
92 Session::State Session::state() const
93 {
94  return d->state;
95 }
96 
97 QString Session::userName() const
98 {
99  return d->userName;
100 }
101 
102 QByteArray Session::serverGreeting() const
103 {
104  return d->greeting;
105 }
106 
107 int Session::jobQueueSize() const
108 {
109  return d->queue.size() + (d->jobRunning ? 1 : 0);
110 }
111 
112 void KIMAP::Session::close()
113 {
114  d->thread->closeSocket();
115 }
116 
117 void SessionPrivate::handleSslError(const KSslErrorUiData &errorData)
118 {
119  // ignoreSslError is async, so the thread might already be gone when it returns
120  QPointer<SessionThread> _t = thread;
121  const bool ignoreSslError = uiProxy && uiProxy->ignoreSslError(errorData);
122  if (_t) {
123  _t->sslErrorHandlerResponse(ignoreSslError);
124  }
125 }
126 
127 SessionPrivate::SessionPrivate(Session *session)
128  : QObject(session)
129  , q(session)
130  , isSocketConnected(false)
131  , state(Session::Disconnected)
132  , logger(nullptr)
133  , thread(nullptr)
134  , jobRunning(false)
135  , currentJob(nullptr)
136  , tagCount(0)
137  , sslVersion(QSsl::UnknownProtocol)
138  , socketTimerInterval(30000) // By default timeouts on 30s
139 {
140 }
141 
142 SessionPrivate::~SessionPrivate()
143 {
144  delete logger;
145 }
146 
147 void SessionPrivate::addJob(Job *job)
148 {
149  queue.append(job);
150  Q_EMIT q->jobQueueSizeChanged(q->jobQueueSize());
151 
152  QObject::connect(job, &KJob::result, this, &SessionPrivate::jobDone);
153  QObject::connect(job, &QObject::destroyed, this, &SessionPrivate::jobDestroyed);
154 
155  if (state != Session::Disconnected) {
156  startNext();
157  }
158 }
159 
160 void SessionPrivate::startNext()
161 {
162  QMetaObject::invokeMethod(this, &SessionPrivate::doStartNext);
163 }
164 
165 void SessionPrivate::doStartNext()
166 {
167  if (queue.isEmpty() || jobRunning || !isSocketConnected) {
168  return;
169  }
170 
171  restartSocketTimer();
172  jobRunning = true;
173 
174  currentJob = queue.dequeue();
175  currentJob->doStart();
176 }
177 
178 void SessionPrivate::jobDone(KJob *job)
179 {
180  Q_UNUSED(job)
181  Q_ASSERT(job == currentJob);
182 
183  stopSocketTimer();
184 
185  jobRunning = false;
186  currentJob = nullptr;
187  Q_EMIT q->jobQueueSizeChanged(q->jobQueueSize());
188  startNext();
189 }
190 
191 void SessionPrivate::jobDestroyed(QObject *job)
192 {
193  queue.removeAll(static_cast<KIMAP::Job *>(job));
194  if (currentJob == job) {
195  currentJob = nullptr;
196  }
197 }
198 
199 void SessionPrivate::responseReceived(const Response &response)
200 {
201  if (logger && isConnected()) {
202  logger->dataReceived(response.toString());
203  }
204 
205  QByteArray tag;
206  QByteArray code;
207 
208  if (response.content.size() >= 1) {
209  tag = response.content[0].toString();
210  }
211 
212  if (response.content.size() >= 2) {
213  code = response.content[1].toString();
214  }
215 
216  // BYE may arrive as part of a LOGOUT sequence or before the server closes the connection after an error.
217  // In any case we should wait until the server closes the connection, so we don't have to do anything.
218  if (code == "BYE") {
219  Response simplified = response;
220  if (simplified.content.size() >= 2) {
221  simplified.content.removeFirst(); // Strip the tag
222  simplified.content.removeFirst(); // Strip the code
223  }
224  qCDebug(KIMAP_LOG) << "Received BYE: " << simplified.toString();
225  return;
226  }
227 
228  switch (state) {
229  case Session::Disconnected:
230  if (socketTimer.isActive()) {
231  stopSocketTimer();
232  }
233  if (code == "OK") {
234  setState(Session::NotAuthenticated);
235 
236  Response simplified = response;
237  simplified.content.removeFirst(); // Strip the tag
238  simplified.content.removeFirst(); // Strip the code
239  greeting = simplified.toString().trimmed(); // Save the server greeting
240  startNext();
241  } else if (code == "PREAUTH") {
242  setState(Session::Authenticated);
243 
244  Response simplified = response;
245  simplified.content.removeFirst(); // Strip the tag
246  simplified.content.removeFirst(); // Strip the code
247  greeting = simplified.toString().trimmed(); // Save the server greeting
248 
249  startNext();
250  } else {
251  thread->closeSocket();
252  }
253  return;
254  case Session::NotAuthenticated:
255  if (code == "OK" && tag == authTag) {
256  setState(Session::Authenticated);
257  }
258  break;
259  case Session::Authenticated:
260  if (code == "OK" && tag == selectTag) {
261  setState(Session::Selected);
262  currentMailBox = upcomingMailBox;
263  }
264  break;
265  case Session::Selected:
266  if ((code == "OK" && tag == closeTag) || (code != "OK" && tag == selectTag)) {
267  setState(Session::Authenticated);
268  currentMailBox = QByteArray();
269  } else if (code == "OK" && tag == selectTag) {
270  currentMailBox = upcomingMailBox;
271  }
272  break;
273  }
274 
275  if (tag == authTag) {
276  authTag.clear();
277  }
278  if (tag == selectTag) {
279  selectTag.clear();
280  }
281  if (tag == closeTag) {
282  closeTag.clear();
283  }
284 
285  // If a job is running forward it the response
286  if (currentJob != nullptr) {
287  restartSocketTimer();
288  currentJob->handleResponse(response);
289  } else {
290  qCWarning(KIMAP_LOG) << "A message was received from the server with no job to handle it:" << response.toString()
291  << '(' + response.toString().toHex() + ')';
292  }
293 }
294 
295 void SessionPrivate::setState(Session::State s)
296 {
297  if (s != state) {
298  Session::State oldState = state;
299  state = s;
300  Q_EMIT q->stateChanged(state, oldState);
301  }
302 }
303 
304 QByteArray SessionPrivate::sendCommand(const QByteArray &command, const QByteArray &args)
305 {
306  QByteArray tag = 'A' + QByteArray::number(++tagCount).rightJustified(6, '0');
307 
308  QByteArray payload = tag + ' ' + command;
309  if (!args.isEmpty()) {
310  payload += ' ' + args;
311  }
312 
313  sendData(payload);
314 
315  if (command == "LOGIN" || command == "AUTHENTICATE") {
316  authTag = tag;
317  } else if (command == "SELECT" || command == "EXAMINE") {
318  selectTag = tag;
319  upcomingMailBox = args;
320  upcomingMailBox.remove(0, 1);
321  upcomingMailBox = upcomingMailBox.left(upcomingMailBox.indexOf('\"'));
322  upcomingMailBox = KIMAP::decodeImapFolderName(upcomingMailBox);
323  } else if (command == "CLOSE") {
324  closeTag = tag;
325  }
326  return tag;
327 }
328 
329 void SessionPrivate::sendData(const QByteArray &data)
330 {
331  restartSocketTimer();
332 
333  if (logger && isConnected()) {
334  logger->dataSent(data);
335  }
336 
337  thread->sendData(data + "\r\n");
338 }
339 
340 void SessionPrivate::socketConnected()
341 {
342  stopSocketTimer();
343  isSocketConnected = true;
344 
345  bool willUseSsl = false;
346  if (!queue.isEmpty()) {
347  auto login = qobject_cast<KIMAP::LoginJob *>(queue.first());
348  if (login) {
349  willUseSsl = (login->encryptionMode() == KIMAP::LoginJob::SSLorTLS);
350 
351  userName = login->userName();
352  }
353  }
354 
355  if (state == Session::Disconnected && willUseSsl) {
356  startSsl(QSsl::SecureProtocols);
357  } else {
358  startSocketTimer();
359  }
360 }
361 
362 bool SessionPrivate::isConnected() const
363 {
364  return state == Session::Authenticated || state == Session::Selected;
365 }
366 
367 void SessionPrivate::socketDisconnected()
368 {
369  if (socketTimer.isActive()) {
370  stopSocketTimer();
371  }
372 
373  if (logger && isConnected()) {
374  logger->disconnectionOccured();
375  }
376 
377  if (isSocketConnected) {
378  setState(Session::Disconnected);
379  Q_EMIT q->connectionLost();
380  } else {
381  Q_EMIT q->connectionFailed();
382  }
383 
384  isSocketConnected = false;
385 
386  clearJobQueue();
387 }
388 
389 void SessionPrivate::socketActivity()
390 {
391  restartSocketTimer();
392 }
393 
394 void SessionPrivate::socketError(QAbstractSocket::SocketError error)
395 {
396  if (socketTimer.isActive()) {
397  stopSocketTimer();
398  }
399 
400  if (currentJob) {
401  currentJob->d_ptr->setSocketError(error);
402  } else if (!queue.isEmpty()) {
403  currentJob = queue.takeFirst();
404  currentJob->d_ptr->setSocketError(error);
405  }
406 
407  if (isSocketConnected) {
408  thread->closeSocket();
409  } else {
410  Q_EMIT q->connectionFailed();
411  clearJobQueue();
412  }
413 }
414 
415 void SessionPrivate::clearJobQueue()
416 {
417  if (currentJob) {
418  currentJob->connectionLost();
419  } else if (!queue.isEmpty()) {
420  currentJob = queue.takeFirst();
421  currentJob->connectionLost();
422  }
423 
424  QQueue<Job *> queueCopy = queue; // copy because jobDestroyed calls removeAll
425  qDeleteAll(queueCopy);
426  queue.clear();
427  Q_EMIT q->jobQueueSizeChanged(0);
428 }
429 
430 void SessionPrivate::startSsl(QSsl::SslProtocol protocol)
431 {
432  thread->startSsl(protocol);
433 }
434 
435 QString Session::selectedMailBox() const
436 {
437  return QString::fromUtf8(d->currentMailBox);
438 }
439 
440 void SessionPrivate::onEncryptionNegotiationResult(bool isEncrypted, QSsl::SslProtocol protocol)
441 {
442  if (isEncrypted) {
443  sslVersion = protocol;
444  } else {
445  sslVersion = QSsl::UnknownProtocol;
446  }
447  Q_EMIT encryptionNegotiationResult(isEncrypted);
448 }
449 
450 QSsl::SslProtocol SessionPrivate::negotiatedEncryption() const
451 {
452  return sslVersion;
453 }
454 
455 void SessionPrivate::setSocketTimeout(int ms)
456 {
457  bool timerActive = socketTimer.isActive();
458 
459  if (timerActive) {
460  stopSocketTimer();
461  }
462 
463  socketTimerInterval = ms;
464 
465  if (timerActive) {
466  startSocketTimer();
467  }
468 }
469 
470 int SessionPrivate::socketTimeout() const
471 {
472  return socketTimerInterval;
473 }
474 
475 void SessionPrivate::startSocketTimer()
476 {
477  if (socketTimerInterval < 0) {
478  return;
479  }
480  Q_ASSERT(!socketTimer.isActive());
481 
482  socketTimer.start(socketTimerInterval);
483 }
484 
485 void SessionPrivate::stopSocketTimer()
486 {
487  if (socketTimerInterval < 0) {
488  return;
489  }
490 
491  socketTimer.stop();
492 }
493 
494 void SessionPrivate::restartSocketTimer()
495 {
496  if (socketTimer.isActive()) {
497  stopSocketTimer();
498  }
499  startSocketTimer();
500 }
501 
502 void SessionPrivate::onSocketTimeout()
503 {
504  qCDebug(KIMAP_LOG) << "Socket timeout!";
505  thread->closeSocket();
506 }
507 
508 void Session::setTimeout(int timeout)
509 {
510  d->setSocketTimeout(timeout * 1000);
511 }
512 
513 int Session::timeout() const
514 {
515  return d->socketTimeout() / 1000;
516 }
517 
518 #include "moc_session.cpp"
519 #include "moc_session_p.cpp"
QString fromUtf8(const char *str, int size)
void result(KJob *job)
QByteArray number(int n, int base)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
Interface to display communication errors and wait for user feedback.
void destroyed(QObject *obj)
QByteArray & remove(int pos, int len)
void timeout()
QCA_EXPORT Logger * logger()
KIMAP_EXPORT QString decodeImapFolderName(const QString &inSrc)
Converts an UTF-7 encoded IMAP mailbox to a Unicode QString.
Definition: rfccodecs.cpp:142
QByteArray rightJustified(int width, char fill, bool truncate) const const
QByteArray left(int len) const const
bool isEmpty() const const
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)
int size() const const
Provides handlers for various RFC/MIME encodings.
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Dec 3 2023 03:51:44 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.