KIMAP2

session.cpp
1/*
2 Copyright (c) 2009 Kevin Ottens <ervin@kde.org>
3 Copyright (c) 2017 Christian Mollekopf <mollekopf@kolabsys.com>
4
5 Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
6 Author: Kevin Ottens <kevin@kdab.com>
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
37Q_DECLARE_METATYPE(QSsl::SslProtocol)
38Q_DECLARE_METATYPE(QSslSocket::SslMode)
39static const int _kimap_sslVersionId = qRegisterMetaType<QSsl::SslProtocol>();
40
41using namespace KIMAP2;
42
43Session::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);
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 }
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
105Session::~Session()
106{
107 //Make sure all jobs know we're done
108 d->clearJobQueue();
109 delete d;
110}
111
112QString Session::hostName() const
113{
114 return d->hostName;
115}
116
117quint16 Session::port() const
118{
119 return d->port;
120}
121
122Session::State Session::state() const
123{
124 return d->state;
125}
126
127bool Session::isConnected() const
128{
129 return (d->state == Authenticated || d->state == Selected);
130}
131
132QString Session::userName() const
133{
134 return d->userName;
135}
136
137QByteArray Session::serverGreeting() const
138{
139 return d->greeting;
140}
141
142int Session::jobQueueSize() const
143{
144 return d->queue.size() + (d->jobRunning ? 1 : 0);
145}
146
147void Session::close()
148{
149 d->closeSocket();
150}
151
152void Session::ignoreErrors(const QList<QSslError> &errors)
153{
154 d->socket->ignoreSslErrors(errors);
155}
156
157void Session::setTimeout(int timeout)
158{
159 d->setSocketTimeout(timeout * 1000);
160}
161
162int Session::timeout() const
163{
164 return d->socketTimeout() / 1000;
165}
166
167QString Session::selectedMailBox() const
168{
169 return QString::fromUtf8(d->currentMailBox);
170}
171
172
173SessionPrivate::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
197SessionPrivate::~SessionPrivate()
198{
199}
200
201void SessionPrivate::handleSslErrors(const QList<QSslError> &errors)
202{
203 emit q->sslErrors(errors);
204}
205
206void 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
216void SessionPrivate::startNext()
217{
218 QMetaObject::invokeMethod(this, "doStartNext");
219}
220
221void 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
248void 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
262void SessionPrivate::jobDestroyed(QObject *job)
263{
264 queue.removeAll(static_cast<KIMAP2::Job *>(job));
265 if (currentJob == job) {
266 currentJob = Q_NULLPTR;
267 }
268}
269
270void 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
365void 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
374QByteArray 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
399void 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
414void 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
422void 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
445void 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
453void 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
470void 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
485void 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
498void SessionPrivate::sslConnected()
499{
500 qCDebug(KIMAP2_LOG) << "ssl is connected";
501 emit encryptionNegotiationResult(true);
502}
503
504void 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
519int SessionPrivate::socketTimeout() const
520{
521 return socketTimerInterval;
522}
523
524void 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
535void SessionPrivate::stopSocketTimer()
536{
537 socketTimer.stop();
538 socketProgressTimer.stop();
539}
540
541void SessionPrivate::restartSocketTimer()
542{
543 stopSocketTimer();
544 startSocketTimer();
545}
546
547void 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
561QString 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
580void 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
589void SessionPrivate::writeDataQueue()
590{
591 while (!dataQueue.isEmpty()) {
592 socket->write(dataQueue.dequeue());
593 }
594}
595
596void 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
615void 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"
Parser for IMAP messages that operates on a local socket stream.
void result(KJob *job)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QCA_EXPORT Logger * logger()
void stateChanged(QAbstractSocket::SocketState socketState)
bool isEmpty() const const
QByteArray left(qsizetype len) const const
QByteArray number(double n, char format, int precision)
QByteArray & remove(qsizetype pos, qsizetype len)
QByteArray & removeFirst()
QByteArray rightJustified(qsizetype width, char fill, bool truncate) const const
qsizetype size() const const
void bytesWritten(qint64 bytes)
void readyRead()
const char * className() const const
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void destroyed(QObject *obj)
virtual const QMetaObject * metaObject() const const
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
QString number(double n, char format, int precision)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
This file is part of the IMAP support library and defines the RfcCodecs class.
KIMAP2_EXPORT QByteArray decodeImapFolderName(const QByteArray &inSrc)
Converts an UTF-7 encoded IMAP mailbox to a QByteArray.
Definition rfccodecs.cpp:70
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:21:18 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.