KIMAP2

session.cpp
1 /*
2  Copyright (c) 2009 Kevin Ottens <[email protected]>
3  Copyright (c) 2017 Christian Mollekopf <[email protected]>
4 
5  Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
6  Author: Kevin Ottens <[email protected]>
7 
8  This library is free software; you can redistribute it and/or modify it
9  under the terms of the GNU Library General Public License as published by
10  the Free Software Foundation; either version 2 of the License, or (at your
11  option) any later version.
12 
13  This library is distributed in the hope that it will be useful, but WITHOUT
14  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
16  License for more details.
17 
18  You should have received a copy of the GNU Library General Public License
19  along with this library; see the file COPYING.LIB. If not, write to the
20  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21  02110-1301, USA.
22 */
23 
24 #include "session.h"
25 #include "session_p.h"
26 
27 #include <QDebug>
28 
29 #include "kimap_debug.h"
30 
31 #include "job.h"
32 #include "message_p.h"
33 #include "sessionlogger_p.h"
34 #include "rfccodecs.h"
35 #include "imapstreamparser.h"
36 
37 Q_DECLARE_METATYPE(QSsl::SslProtocol)
38 Q_DECLARE_METATYPE(QSslSocket::SslMode)
39 static const int _kimap_sslVersionId = qRegisterMetaType<QSsl::SslProtocol>();
40 
41 using namespace KIMAP2;
42 
43 Session::Session(const QString &hostName, quint16 port, QObject *parent)
44  : QObject(parent), d(new SessionPrivate(this))
45 {
46  if (!qEnvironmentVariableIsEmpty("KIMAP2_LOGFILE")) {
47  d->logger.reset(new SessionLogger);
48  qCInfo(KIMAP2_LOG) << "Logging traffic to: " << QLatin1String(qgetenv("KIMAP2_LOGFILE"));
49  }
50  if (qEnvironmentVariableIsSet("KIMAP2_TRAFFIC")) {
51  d->dumpTraffic = true;
52  qCInfo(KIMAP2_LOG) << "Dumping traffic.";
53  }
54  if (qEnvironmentVariableIsSet("KIMAP2_TIMING")) {
55  d->trackTime = true;
56  qCInfo(KIMAP2_LOG) << "Tracking timings.";
57  }
58 
59  d->state = Disconnected;
60  d->jobRunning = false;
61  d->hostName = hostName;
62  d->port = port;
63 
64  connect(d->socket.data(), &QIODevice::readyRead, d, &SessionPrivate::readMessage);
65 
66  connect(d->socket.data(), &QSslSocket::connected,
67  d, &SessionPrivate::socketConnected);
68  connect(d->socket.data(), static_cast<void (QSslSocket::*)(const QList<QSslError>&)>(&QSslSocket::sslErrors),
69  d, &SessionPrivate::handleSslErrors);
70  connect(d->socket.data(), static_cast<void (QSslSocket::*)(QAbstractSocket::SocketError)>(&QSslSocket::error),
71  d, &SessionPrivate::socketError);
72 
73  connect(d->socket.data(), &QIODevice::bytesWritten,
74  d, &SessionPrivate::socketActivity);
75  connect(d->socket.data(), &QSslSocket::encryptedBytesWritten,
76  d, &SessionPrivate::socketActivity);
77  connect(d->socket.data(), &QIODevice::readyRead,
78  d, &SessionPrivate::socketActivity);
79  connect(d->socket.data(), &QAbstractSocket::stateChanged, [this](QAbstractSocket::SocketState state) {
80  qCDebug(KIMAP2_LOG) << "Socket state changed: " << state;
81  //The disconnected signal will not fire if we fail to lookup the host, but this will.
82  if (state == QAbstractSocket::UnconnectedState) {
83  d->socketDisconnected();
84  }
85  if (state == QAbstractSocket::HostLookupState) {
86  d->hostLookupInProgress = true;
87  } else {
88  d->hostLookupInProgress = false;
89  }
90  });
91 
92  d->socketTimer.setSingleShot(true);
93  connect(&d->socketTimer, &QTimer::timeout,
94  d, &SessionPrivate::onSocketTimeout);
95 
96  d->socketProgressTimer.setSingleShot(false);
97  connect(&d->socketProgressTimer, &QTimer::timeout,
98  d, &SessionPrivate::onSocketProgressTimeout);
99 
100  d->startSocketTimer();
101  qCDebug(KIMAP2_LOG) << "Connecting to: " << hostName << port;
102  d->socket->connectToHost(hostName, port);
103 }
104 
105 Session::~Session()
106 {
107  //Make sure all jobs know we're done
108  d->clearJobQueue();
109  delete d;
110 }
111 
112 QString Session::hostName() const
113 {
114  return d->hostName;
115 }
116 
117 quint16 Session::port() const
118 {
119  return d->port;
120 }
121 
122 Session::State Session::state() const
123 {
124  return d->state;
125 }
126 
127 bool Session::isConnected() const
128 {
129  return (d->state == Authenticated || d->state == Selected);
130 }
131 
132 QString Session::userName() const
133 {
134  return d->userName;
135 }
136 
137 QByteArray Session::serverGreeting() const
138 {
139  return d->greeting;
140 }
141 
142 int Session::jobQueueSize() const
143 {
144  return d->queue.size() + (d->jobRunning ? 1 : 0);
145 }
146 
147 void Session::close()
148 {
149  d->closeSocket();
150 }
151 
152 void Session::ignoreErrors(const QList<QSslError> &errors)
153 {
154  d->socket->ignoreSslErrors(errors);
155 }
156 
157 void Session::setTimeout(int timeout)
158 {
159  d->setSocketTimeout(timeout * 1000);
160 }
161 
162 int Session::timeout() const
163 {
164  return d->socketTimeout() / 1000;
165 }
166 
167 QString Session::selectedMailBox() const
168 {
169  return QString::fromUtf8(d->currentMailBox);
170 }
171 
172 
173 SessionPrivate::SessionPrivate(Session *session)
174  : QObject(session),
175  q(session),
176  state(Session::Disconnected),
177  hostLookupInProgress(false),
178  logger(Q_NULLPTR),
179  currentJob(Q_NULLPTR),
180  tagCount(0),
181  socketTimerInterval(30000), // By default timeouts on 30s
182  socketProgressInterval(3000), // mention we're still alive every 3s
183  socket(new QSslSocket),
184  stream(new ImapStreamParser(socket.data())),
185  accumulatedWaitTime(0),
186  accumulatedProcessingTime(0),
187  trackTime(false),
188  dumpTraffic(false)
189 {
190  //For windows this needs to be set before connecting according to the docs
191  socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1);
192  stream->onResponseReceived([this](const Message &message) {
193  responseReceived(message);
194  });
195 }
196 
197 SessionPrivate::~SessionPrivate()
198 {
199 }
200 
201 void SessionPrivate::handleSslErrors(const QList<QSslError> &errors)
202 {
203  emit q->sslErrors(errors);
204 }
205 
206 void SessionPrivate::addJob(Job *job)
207 {
208  queue.append(job);
209  emit q->jobQueueSizeChanged(q->jobQueueSize());
210 
211  QObject::connect(job, &KJob::result, this, &SessionPrivate::jobDone);
212  QObject::connect(job, &QObject::destroyed, this, &SessionPrivate::jobDestroyed);
213  startNext();
214 }
215 
216 void SessionPrivate::startNext()
217 {
218  QMetaObject::invokeMethod(this, "doStartNext");
219 }
220 
221 void SessionPrivate::doStartNext()
222 {
223  //Wait until we are ready to process
224  if (queue.isEmpty()
225  || jobRunning
226  || socket->state() == QSslSocket::ConnectingState
227  || socket->state() == QSslSocket::HostLookupState) {
228  return;
229  }
230 
231  currentJob = queue.dequeue();
232 
233  //Since we aren't connecting we may never get back. Cancel the job
234  if (socket->state() == QSslSocket::UnconnectedState) {
235  qCDebug(KIMAP2_LOG) << "Cancelling job due to lack of connection: " << currentJob->metaObject()->className();
236  currentJob->connectionLost();
237  return;
238  }
239 
240  if (trackTime) {
241  time.start();
242  }
243  restartSocketTimer();
244  jobRunning = true;
245  currentJob->doStart();
246 }
247 
248 void SessionPrivate::jobDone(KJob *job)
249 {
250  Q_UNUSED(job);
251  Q_ASSERT(job == currentJob);
252  qCDebug(KIMAP2_LOG) << "Job done: " << job->metaObject()->className();
253 
254  stopSocketTimer();
255 
256  jobRunning = false;
257  currentJob = Q_NULLPTR;
258  emit q->jobQueueSizeChanged(q->jobQueueSize());
259  startNext();
260 }
261 
262 void SessionPrivate::jobDestroyed(QObject *job)
263 {
264  queue.removeAll(static_cast<KIMAP2::Job *>(job));
265  if (currentJob == job) {
266  currentJob = Q_NULLPTR;
267  }
268 }
269 
270 void SessionPrivate::responseReceived(const Message &response)
271 {
272  if (dumpTraffic) {
273  qCInfo(KIMAP2_LOG) << "S: " << QString::fromLatin1(response.toString());
274  }
275  if (logger && q->isConnected()) {
276  logger->dataReceived(response.toString());
277  }
278 
279  QByteArray tag;
280  QByteArray code;
281 
282  if (response.content.size() >= 1) {
283  tag = response.content[0].toString();
284  }
285 
286  if (response.content.size() >= 2) {
287  code = response.content[1].toString();
288  }
289 
290  // BYE may arrive as part of a LOGOUT sequence or before the server closes the connection after an error.
291  // In any case we should wait until the server closes the connection, so we don't have to do anything.
292  if (code == "BYE") {
293  Message simplified = response;
294  if (simplified.content.size() >= 2) {
295  simplified.content.removeFirst(); // Strip the tag
296  simplified.content.removeFirst(); // Strip the code
297  }
298  qCDebug(KIMAP2_LOG) << "Received BYE: " << simplified.toString();
299  return;
300  }
301 
302  switch (state) {
303  case Session::Disconnected:
304  stopSocketTimer();
305  if (code == "OK") {
306  Message simplified = response;
307  simplified.content.removeFirst(); // Strip the tag
308  simplified.content.removeFirst(); // Strip the code
309  greeting = simplified.toString().trimmed(); // Save the server greeting
310  setState(Session::NotAuthenticated);
311  } else if (code == "PREAUTH") {
312  Message simplified = response;
313  simplified.content.removeFirst(); // Strip the tag
314  simplified.content.removeFirst(); // Strip the code
315  greeting = simplified.toString().trimmed(); // Save the server greeting
316  setState(Session::Authenticated);
317  } else {
318  //We have been rejected
319  closeSocket();
320  }
321  return;
322  case Session::NotAuthenticated:
323  if (code == "OK" && tag == authTag) {
324  setState(Session::Authenticated);
325  }
326  break;
327  case Session::Authenticated:
328  if (code == "OK" && tag == selectTag) {
329  setState(Session::Selected);
330  currentMailBox = upcomingMailBox;
331  }
332  break;
333  case Session::Selected:
334  if ((code == "OK" && tag == closeTag) ||
335  (code != "OK" && tag == selectTag)) {
336  setState(Session::Authenticated);
337  currentMailBox = QByteArray();
338  } else if (code == "OK" && tag == selectTag) {
339  currentMailBox = upcomingMailBox;
340  }
341  break;
342  }
343 
344  if (tag == authTag) {
345  authTag.clear();
346  }
347  if (tag == selectTag) {
348  selectTag.clear();
349  }
350  if (tag == closeTag) {
351  closeTag.clear();
352  }
353 
354  // If a job is running forward it the response
355  if (currentJob) {
356  restartSocketTimer();
357  currentJob->handleResponse(response);
358  } else {
359  qCWarning(KIMAP2_LOG) << "A message was received from the server with no job to handle it:"
360  << response.toString()
361  << '(' + response.toString().toHex() + ')';
362  }
363 }
364 
365 void SessionPrivate::setState(Session::State s)
366 {
367  if (s != state) {
368  Session::State oldState = state;
369  state = s;
370  emit q->stateChanged(state, oldState);
371  }
372 }
373 
374 QByteArray SessionPrivate::sendCommand(const QByteArray &command, const QByteArray &args)
375 {
376  QByteArray tag = 'A' + QByteArray::number(++tagCount).rightJustified(6, '0');
377 
378  QByteArray payload = tag + ' ' + command;
379  if (!args.isEmpty()) {
380  payload += ' ' + args;
381  }
382 
383  sendData(payload);
384 
385  if (command == "LOGIN" || command == "AUTHENTICATE") {
386  authTag = tag;
387  } else if (command == "SELECT" || command == "EXAMINE") {
388  selectTag = tag;
389  upcomingMailBox = args;
390  upcomingMailBox.remove(0, 1);
391  upcomingMailBox = upcomingMailBox.left(upcomingMailBox.indexOf('\"'));
392  upcomingMailBox = KIMAP2::decodeImapFolderName(upcomingMailBox);
393  } else if (command == "CLOSE") {
394  closeTag = tag;
395  }
396  return tag;
397 }
398 
399 void SessionPrivate::sendData(const QByteArray &data)
400 {
401  restartSocketTimer();
402 
403  if (dumpTraffic) {
404  qCInfo(KIMAP2_LOG) << "C: " << data;
405  }
406  if (logger && q->isConnected()) {
407  logger->dataSent(data);
408  }
409 
410  dataQueue.enqueue(data + "\r\n");
411  QMetaObject::invokeMethod(this, "writeDataQueue");
412 }
413 
414 void SessionPrivate::socketConnected()
415 {
416  qCInfo(KIMAP2_LOG) << "Socket connected.";
417  //Detect if the connection is no longer available
418  socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1);
419  startNext();
420 }
421 
422 void SessionPrivate::socketDisconnected()
423 {
424  qCInfo(KIMAP2_LOG) << "Socket disconnected.";
425  stopSocketTimer();
426 
427  if (logger && q->isConnected()) {
428  logger->disconnectionOccured();
429  }
430 
431  if (state != Session::Disconnected) {
432  setState(Session::Disconnected);
433  } else {
434  //If we timeout during host lookup we don't receive an explicit host lookup error
435  if (hostLookupInProgress) {
437  hostLookupInProgress = false;
438  }
439  emit q->connectionFailed();
440  }
441 
442  clearJobQueue();
443 }
444 
445 void SessionPrivate::socketActivity()
446 {
447  //This slot can be called after the job has already finished, in that case we don't want to restart the timer
448  if (currentJob) {
449  restartSocketTimer();
450  }
451 }
452 
453 void SessionPrivate::socketError(QAbstractSocket::SocketError error)
454 {
455  qCDebug(KIMAP2_LOG) << "Socket error: " << error;
456  stopSocketTimer();
457 
458  if (currentJob) {
459  qCWarning(KIMAP2_LOG) << "Socket error:" << error;
460  currentJob->setSocketError(error);
461  } else if (!queue.isEmpty()) {
462  qCWarning(KIMAP2_LOG) << "Socket error:" << error;
463  currentJob = queue.takeFirst();
464  currentJob->setSocketError(error);
465  }
466 
467  closeSocket();
468 }
469 
470 void SessionPrivate::clearJobQueue()
471 {
472  if (!currentJob && !queue.isEmpty()) {
473  currentJob = queue.takeFirst();
474  }
475  if (currentJob) {
476  currentJob->connectionLost();
477  }
478 
479  QQueue<Job *> queueCopy = queue; // copy because jobDestroyed calls removeAll
480  qDeleteAll(queueCopy);
481  queue.clear();
482  emit q->jobQueueSizeChanged(0);
483 }
484 
485 void SessionPrivate::startSsl(QSsl::SslProtocol protocol)
486 {
487  socket->setProtocol(protocol);
488  connect(socket.data(), &QSslSocket::encrypted, this, &SessionPrivate::sslConnected);
489  if (socket->state() == QAbstractSocket::ConnectedState) {
490  qCDebug(KIMAP2_LOG) << "Starting client encryption";
491  Q_ASSERT(socket->mode() == QSslSocket::UnencryptedMode);
492  socket->startClientEncryption();
493  } else {
494  qCWarning(KIMAP2_LOG) << "The socket is not yet connected";
495  }
496 }
497 
498 void SessionPrivate::sslConnected()
499 {
500  qCDebug(KIMAP2_LOG) << "ssl is connected";
501  emit encryptionNegotiationResult(true);
502 }
503 
504 void SessionPrivate::setSocketTimeout(int ms)
505 {
506  bool timerActive = socketTimer.isActive();
507 
508  if (timerActive) {
509  stopSocketTimer();
510  }
511 
512  socketTimerInterval = ms;
513 
514  if (timerActive) {
515  startSocketTimer();
516  }
517 }
518 
519 int SessionPrivate::socketTimeout() const
520 {
521  return socketTimerInterval;
522 }
523 
524 void SessionPrivate::startSocketTimer()
525 {
526  if (socketTimerInterval < 0) {
527  return;
528  }
529  Q_ASSERT(!socketTimer.isActive());
530 
531  socketTimer.start(socketTimerInterval);
532  socketProgressTimer.start(socketProgressInterval);
533 }
534 
535 void SessionPrivate::stopSocketTimer()
536 {
537  socketTimer.stop();
538  socketProgressTimer.stop();
539 }
540 
541 void SessionPrivate::restartSocketTimer()
542 {
543  stopSocketTimer();
544  startSocketTimer();
545 }
546 
547 void SessionPrivate::onSocketTimeout()
548 {
549  qCWarning(KIMAP2_LOG) << "Aborting on socket timeout. " << socketTimerInterval;
550  if (!currentJob && !queue.isEmpty()) {
551  currentJob = queue.takeFirst();
552  }
553  if (currentJob) {
554  qCWarning(KIMAP2_LOG) << "Current job: " << currentJob->metaObject()->className();
555  currentJob->setErrorMessage("Aborting on socket timeout. Interval " + QString::number(socketTimerInterval) + " ms");
556  }
557  socket->abort();
558  socketProgressTimer.stop();
559 }
560 
561 QString SessionPrivate::getStateName() const
562 {
563  if (hostLookupInProgress) {
564  return "Host lookup";
565  }
566  switch (state) {
567  case Session::Disconnected:
568  return "Disconnected";
569  case Session::NotAuthenticated:
570  return "NotAuthenticated";
571  case Session::Authenticated:
572  return "Authenticated";
573  case Session::Selected:
574  default:
575  break;
576  }
577  return "Unknown State";
578 }
579 
580 void SessionPrivate::onSocketProgressTimeout()
581 {
582  if (currentJob) {
583  qCDebug(KIMAP2_LOG) << "Processing job: " << currentJob->metaObject()->className() << "Current state: " << getStateName() << (socket ? socket->state() : QAbstractSocket::UnconnectedState);
584  } else {
585  qCDebug(KIMAP2_LOG) << "Next job: " << (queue.isEmpty() ? "No job" : queue.head()->metaObject()->className()) << "Current state: " << getStateName() << (socket ? socket->state() : QAbstractSocket::UnconnectedState);
586  }
587 }
588 
589 void SessionPrivate::writeDataQueue()
590 {
591  while (!dataQueue.isEmpty()) {
592  socket->write(dataQueue.dequeue());
593  }
594 }
595 
596 void SessionPrivate::readMessage()
597 {
598  if (trackTime) {
599  accumulatedWaitTime += time.elapsed();
600  time.start();
601  }
602  stream->parseStream();
603  if (stream->error()) {
604  qCWarning(KIMAP2_LOG) << "Error while parsing, closing connection.";
605  qCDebug(KIMAP2_LOG) << "Current buffer: " << stream->currentBuffer();
606  socket->close();
607  }
608  if (trackTime) {
609  accumulatedProcessingTime += time.elapsed();
610  time.start();
611  qCDebug(KIMAP2_LOG) << "Wait vs process vs total: " << accumulatedWaitTime << accumulatedProcessingTime << accumulatedWaitTime + accumulatedProcessingTime;
612  }
613 }
614 
615 void SessionPrivate::closeSocket()
616 {
617  qCDebug(KIMAP2_LOG) << "Closing socket.";
618  socket->close();
619 }
620 
621 #include "moc_session.cpp"
622 #include "moc_session_p.cpp"
void stateChanged(QAbstractSocket::SocketState socketState)
T * data() const const
QString number(int n, int base)
QString fromUtf8(const char *str, int size)
void encryptedBytesWritten(qint64 written)
void result(KJob *job)
void bytesWritten(qint64 bytes)
QByteArray number(int n, int base)
QList< QSslError > sslErrors() const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void destroyed(QObject *obj)
QByteArray & remove(int pos, int len)
KIMAP2_EXPORT QString decodeImapFolderName(const QString &inSrc)
Converts an UTF-7 encoded IMAP mailbox to a Unicode QString.
Definition: rfccodecs.cpp:154
void timeout()
void readyRead()
QCA_EXPORT Logger * logger()
Parser for IMAP messages that operates on a local socket stream.
QByteArray rightJustified(int width, char fill, bool truncate) const const
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
virtual const QMetaObject * metaObject() const const
const char * className() const const
QByteArray left(int len) const const
bool isEmpty() const const
QAbstractSocket::SocketError error() const const
QString fromLatin1(const char *str, int size)
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
void encrypted()
Provides handlers for various RFC/MIME encodings.
QString message
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Feb 5 2023 04:11:00 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.