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 <QTimer>
14 #include <QPointer>
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 "sessionlogger_p.h"
23 #include "sessionthread_p.h"
24 #include "rfccodecs.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), d(new SessionPrivate(this))
34 {
35  if (!qEnvironmentVariableIsEmpty("KIMAP_LOGFILE")) {
36  d->logger = new SessionLogger;
37  }
38 
39  d->isSocketConnected = false;
40  d->state = Disconnected;
41  d->jobRunning = false;
42 
43  d->thread = new SessionThread(hostName, port);
44  connect(d->thread, &SessionThread::encryptionNegotiationResult, d, &SessionPrivate::onEncryptionNegotiationResult);
45  connect(d->thread, &SessionThread::sslError, d, &SessionPrivate::handleSslError);
46  connect(d->thread, &SessionThread::socketDisconnected, d, &SessionPrivate::socketDisconnected);
47  connect(d->thread, &SessionThread::responseReceived, d, &SessionPrivate::responseReceived);
48  connect(d->thread, &SessionThread::socketConnected, d, &SessionPrivate::socketConnected);
49  connect(d->thread, &SessionThread::socketActivity, d, &SessionPrivate::socketActivity);
50  connect(d->thread, &SessionThread::socketError, d, &SessionPrivate::socketError);
51 
52  d->socketTimer.setSingleShot(true);
53  connect(&d->socketTimer, &QTimer::timeout,
54  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) ||
267  (code != "OK" && tag == selectTag)) {
268  setState(Session::Authenticated);
269  currentMailBox = QByteArray();
270  } else if (code == "OK" && tag == selectTag) {
271  currentMailBox = upcomingMailBox;
272  }
273  break;
274  }
275 
276  if (tag == authTag) {
277  authTag.clear();
278  }
279  if (tag == selectTag) {
280  selectTag.clear();
281  }
282  if (tag == closeTag) {
283  closeTag.clear();
284  }
285 
286  // If a job is running forward it the response
287  if (currentJob != nullptr) {
288  restartSocketTimer();
289  currentJob->handleResponse(response);
290  } else {
291  qCWarning(KIMAP_LOG) << "A message was received from the server with no job to handle it:"
292  << response.toString()
293  << '(' + response.toString().toHex() + ')';
294  }
295 }
296 
297 void SessionPrivate::setState(Session::State s)
298 {
299  if (s != state) {
300  Session::State oldState = state;
301  state = s;
302  Q_EMIT q->stateChanged(state, oldState);
303  }
304 }
305 
306 QByteArray SessionPrivate::sendCommand(const QByteArray &command, const QByteArray &args)
307 {
308  QByteArray tag = 'A' + QByteArray::number(++tagCount).rightJustified(6, '0');
309 
310  QByteArray payload = tag + ' ' + command;
311  if (!args.isEmpty()) {
312  payload += ' ' + args;
313  }
314 
315  sendData(payload);
316 
317  if (command == "LOGIN" || command == "AUTHENTICATE") {
318  authTag = tag;
319  } else if (command == "SELECT" || command == "EXAMINE") {
320  selectTag = tag;
321  upcomingMailBox = args;
322  upcomingMailBox.remove(0, 1);
323  upcomingMailBox = upcomingMailBox.left(upcomingMailBox.indexOf('\"'));
324  upcomingMailBox = KIMAP::decodeImapFolderName(upcomingMailBox);
325  } else if (command == "CLOSE") {
326  closeTag = tag;
327  }
328  return tag;
329 }
330 
331 void SessionPrivate::sendData(const QByteArray &data)
332 {
333  restartSocketTimer();
334 
335  if (logger && isConnected()) {
336  logger->dataSent(data);
337  }
338 
339  thread->sendData(data + "\r\n");
340 }
341 
342 void SessionPrivate::socketConnected()
343 {
344  stopSocketTimer();
345  isSocketConnected = true;
346 
347  bool willUseSsl = false;
348  if (!queue.isEmpty()) {
349  KIMAP::LoginJob *login = qobject_cast<KIMAP::LoginJob *>(queue.first());
350  if (login) {
351  willUseSsl = (login->encryptionMode() == KIMAP::LoginJob::SSLorTLS);
352 
353  userName = login->userName();
354  }
355  }
356 
357  if (state == Session::Disconnected && willUseSsl) {
358  startNext();
359  } else {
360  startSocketTimer();
361  }
362 }
363 
364 bool SessionPrivate::isConnected() const
365 {
366  return state == Session::Authenticated || state == Session::Selected;
367 }
368 
369 void SessionPrivate::socketDisconnected()
370 {
371  if (socketTimer.isActive()) {
372  stopSocketTimer();
373  }
374 
375  if (logger && isConnected()) {
376  logger->disconnectionOccured();
377  }
378 
379  if (isSocketConnected) {
380  setState(Session::Disconnected);
381  Q_EMIT q->connectionLost();
382  } else {
383  Q_EMIT q->connectionFailed();
384  }
385 
386  isSocketConnected = false;
387 
388  clearJobQueue();
389 }
390 
391 void SessionPrivate::socketActivity()
392 {
393  restartSocketTimer();
394 }
395 
396 void SessionPrivate::socketError(QAbstractSocket::SocketError error)
397 {
398  if (socketTimer.isActive()) {
399  stopSocketTimer();
400  }
401 
402  if (currentJob) {
403  currentJob->d_ptr->setSocketError(error);
404  } else if (!queue.isEmpty()) {
405  currentJob = queue.takeFirst();
406  currentJob->d_ptr->setSocketError(error);
407  }
408 
409  if (isSocketConnected) {
410  thread->closeSocket();
411  } else {
412  Q_EMIT q->connectionFailed();
413  clearJobQueue();
414  }
415 }
416 
417 void SessionPrivate::clearJobQueue()
418 {
419  if (currentJob) {
420  currentJob->connectionLost();
421  } else if (!queue.isEmpty()) {
422  currentJob = queue.takeFirst();
423  currentJob->connectionLost();
424  }
425 
426  QQueue<Job *> queueCopy = queue; // copy because jobDestroyed calls removeAll
427  qDeleteAll(queueCopy);
428  queue.clear();
429  Q_EMIT q->jobQueueSizeChanged(0);
430 }
431 
432 void SessionPrivate::startSsl(QSsl::SslProtocol protocol)
433 {
434  thread->startSsl(protocol);
435 }
436 
437 QString Session::selectedMailBox() const
438 {
439  return QString::fromUtf8(d->currentMailBox);
440 }
441 
442 void SessionPrivate::onEncryptionNegotiationResult(bool isEncrypted, QSsl::SslProtocol protocol)
443 {
444  if (isEncrypted) {
445  sslVersion = protocol;
446  } else {
447  sslVersion = QSsl::UnknownProtocol;
448  }
449  Q_EMIT encryptionNegotiationResult(isEncrypted);
450 }
451 
452 QSsl::SslProtocol SessionPrivate::negotiatedEncryption() const
453 {
454  return sslVersion;
455 }
456 
457 void SessionPrivate::setSocketTimeout(int ms)
458 {
459  bool timerActive = socketTimer.isActive();
460 
461  if (timerActive) {
462  stopSocketTimer();
463  }
464 
465  socketTimerInterval = ms;
466 
467  if (timerActive) {
468  startSocketTimer();
469  }
470 }
471 
472 int SessionPrivate::socketTimeout() const
473 {
474  return socketTimerInterval;
475 }
476 
477 void SessionPrivate::startSocketTimer()
478 {
479  if (socketTimerInterval < 0) {
480  return;
481  }
482  Q_ASSERT(!socketTimer.isActive());
483 
484  socketTimer.start(socketTimerInterval);
485 }
486 
487 void SessionPrivate::stopSocketTimer()
488 {
489  if (socketTimerInterval < 0) {
490  return;
491  }
492 
493  socketTimer.stop();
494 }
495 
496 void SessionPrivate::restartSocketTimer()
497 {
498  if (socketTimer.isActive()) {
499  stopSocketTimer();
500  }
501  startSocketTimer();
502 }
503 
504 void SessionPrivate::onSocketTimeout()
505 {
506  qCDebug(KIMAP_LOG) << "Socket timeout!";
507  thread->closeSocket();
508 }
509 
510 void Session::setTimeout(int timeout)
511 {
512  d->setSocketTimeout(timeout * 1000);
513 }
514 
515 int Session::timeout() const
516 {
517  return d->socketTimeout() / 1000;
518 }
519 
520 #include "moc_session.cpp"
521 #include "moc_session_p.cpp"
This file is part of the IMAP support library and defines the RfcCodecs class.
QCA_EXPORT Logger * logger()
bool isEmpty() const const
KIMAP_EXPORT QString decodeImapFolderName(const QString &inSrc)
Converts an UTF-7 encoded IMAP mailbox to a Unicode QString.
Definition: rfccodecs.cpp:140
void timeout()
QString fromUtf8(const char *str, int size)
QByteArray number(int n, int base)
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)
QByteArray left(int len) const const
Interface to display communication errors and wait for user feedback.
void result(KJob *job)
QByteArray rightJustified(int width, char fill, bool truncate) const const
Definition: acl.cpp:12
int size() const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QByteArray & remove(int pos, int len)
void destroyed(QObject *obj)
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Sat Oct 24 2020 23:16:51 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.