File indexing completed on 2024-05-12 05:17:17

0001 /*
0002     SPDX-FileCopyrightText: 2009 Kevin Ottens <ervin@kde.org>
0003 
0004     SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
0005     SPDX-FileContributor: Kevin Ottens <kevin@kdab.com>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "session.h"
0011 #include "session_p.h"
0012 
0013 #include <QPointer>
0014 #include <QTimer>
0015 
0016 #include "kimap_debug.h"
0017 
0018 #include "job.h"
0019 #include "job_p.h"
0020 #include "loginjob.h"
0021 #include "response_p.h"
0022 #include "rfccodecs.h"
0023 #include "sessionlogger_p.h"
0024 #include "sessionthread_p.h"
0025 
0026 Q_DECLARE_METATYPE(QSsl::SslProtocol)
0027 Q_DECLARE_METATYPE(QSslSocket::SslMode)
0028 static const int _kimap_sslVersionId = qRegisterMetaType<QSsl::SslProtocol>();
0029 
0030 using namespace KIMAP;
0031 
0032 Session::Session(const QString &hostName, quint16 port, QObject *parent)
0033     : QObject(parent)
0034     , d(new SessionPrivate(this))
0035 {
0036     if (!qEnvironmentVariableIsEmpty("KIMAP_LOGFILE")) {
0037         d->logger = new SessionLogger;
0038     }
0039 
0040     d->isSocketConnected = false;
0041     d->state = Disconnected;
0042     d->jobRunning = false;
0043 
0044     d->thread = new SessionThread(hostName, port);
0045     connect(d->thread, &SessionThread::encryptionNegotiationResult, d, &SessionPrivate::onEncryptionNegotiationResult);
0046     connect(d->thread, &SessionThread::sslError, d, &SessionPrivate::handleSslError);
0047     connect(d->thread, &SessionThread::socketDisconnected, d, &SessionPrivate::socketDisconnected);
0048     connect(d->thread, &SessionThread::responseReceived, d, &SessionPrivate::responseReceived);
0049     connect(d->thread, &SessionThread::socketConnected, d, &SessionPrivate::socketConnected);
0050     connect(d->thread, &SessionThread::socketActivity, d, &SessionPrivate::socketActivity);
0051     connect(d->thread, &SessionThread::socketError, d, &SessionPrivate::socketError);
0052 
0053     d->socketTimer.setSingleShot(true);
0054     connect(&d->socketTimer, &QTimer::timeout, d, &SessionPrivate::onSocketTimeout);
0055 
0056     d->startSocketTimer();
0057 }
0058 
0059 Session::~Session()
0060 {
0061     // Make sure all jobs know we're done
0062     d->socketDisconnected();
0063     delete d->thread;
0064     d->thread = nullptr;
0065 }
0066 
0067 void Session::setUiProxy(const SessionUiProxy::Ptr &proxy)
0068 {
0069     d->uiProxy = proxy;
0070 }
0071 
0072 void Session::setUiProxy(SessionUiProxy *proxy)
0073 {
0074     setUiProxy(SessionUiProxy::Ptr(proxy));
0075 }
0076 
0077 QString Session::hostName() const
0078 {
0079     return d->thread->hostName();
0080 }
0081 
0082 quint16 Session::port() const
0083 {
0084     return d->thread->port();
0085 }
0086 
0087 void Session::setUseNetworkProxy(bool useProxy)
0088 {
0089     d->thread->setUseNetworkProxy(useProxy);
0090 }
0091 
0092 Session::State Session::state() const
0093 {
0094     return d->state;
0095 }
0096 
0097 QString Session::userName() const
0098 {
0099     return d->userName;
0100 }
0101 
0102 QByteArray Session::serverGreeting() const
0103 {
0104     return d->greeting;
0105 }
0106 
0107 int Session::jobQueueSize() const
0108 {
0109     return d->queue.size() + (d->jobRunning ? 1 : 0);
0110 }
0111 
0112 void KIMAP::Session::close()
0113 {
0114     d->thread->closeSocket();
0115 }
0116 
0117 void SessionPrivate::handleSslError(const KSslErrorUiData &errorData)
0118 {
0119     // ignoreSslError is async, so the thread might already be gone when it returns
0120     QPointer<SessionThread> _t = thread;
0121     const bool ignoreSslError = uiProxy && uiProxy->ignoreSslError(errorData);
0122     if (_t) {
0123         _t->sslErrorHandlerResponse(ignoreSslError);
0124     }
0125 }
0126 
0127 SessionPrivate::SessionPrivate(Session *session)
0128     : QObject(session)
0129     , q(session)
0130     , isSocketConnected(false)
0131     , state(Session::Disconnected)
0132     , logger(nullptr)
0133     , thread(nullptr)
0134     , jobRunning(false)
0135     , currentJob(nullptr)
0136     , tagCount(0)
0137     , sslVersion(QSsl::UnknownProtocol)
0138     , socketTimerInterval(30000) // By default timeouts on 30s
0139 {
0140 }
0141 
0142 SessionPrivate::~SessionPrivate()
0143 {
0144     delete logger;
0145 }
0146 
0147 void SessionPrivate::addJob(Job *job)
0148 {
0149     queue.append(job);
0150     Q_EMIT q->jobQueueSizeChanged(q->jobQueueSize());
0151 
0152     QObject::connect(job, &KJob::result, this, &SessionPrivate::jobDone);
0153     QObject::connect(job, &QObject::destroyed, this, &SessionPrivate::jobDestroyed);
0154 
0155     if (state != Session::Disconnected) {
0156         startNext();
0157     }
0158 }
0159 
0160 void SessionPrivate::startNext()
0161 {
0162     QMetaObject::invokeMethod(this, &SessionPrivate::doStartNext);
0163 }
0164 
0165 void SessionPrivate::doStartNext()
0166 {
0167     if (queue.isEmpty() || jobRunning || !isSocketConnected) {
0168         return;
0169     }
0170 
0171     restartSocketTimer();
0172     jobRunning = true;
0173 
0174     currentJob = queue.dequeue();
0175     currentJob->doStart();
0176 }
0177 
0178 void SessionPrivate::jobDone(KJob *job)
0179 {
0180     Q_UNUSED(job)
0181     Q_ASSERT(job == currentJob);
0182 
0183     stopSocketTimer();
0184 
0185     jobRunning = false;
0186     currentJob = nullptr;
0187     Q_EMIT q->jobQueueSizeChanged(q->jobQueueSize());
0188     startNext();
0189 }
0190 
0191 void SessionPrivate::jobDestroyed(QObject *job)
0192 {
0193     queue.removeAll(static_cast<KIMAP::Job *>(job));
0194     if (currentJob == job) {
0195         currentJob = nullptr;
0196     }
0197 }
0198 
0199 void SessionPrivate::responseReceived(const Response &response)
0200 {
0201     if (logger && isConnected()) {
0202         logger->dataReceived(response.toString());
0203     }
0204 
0205     QByteArray tag;
0206     QByteArray code;
0207 
0208     if (response.content.size() >= 1) {
0209         tag = response.content[0].toString();
0210     }
0211 
0212     if (response.content.size() >= 2) {
0213         code = response.content[1].toString();
0214     }
0215 
0216     // BYE may arrive as part of a LOGOUT sequence or before the server closes the connection after an error.
0217     // In any case we should wait until the server closes the connection, so we don't have to do anything.
0218     if (code == "BYE") {
0219         Response simplified = response;
0220         if (simplified.content.size() >= 2) {
0221             simplified.content.removeFirst(); // Strip the tag
0222             simplified.content.removeFirst(); // Strip the code
0223         }
0224         qCDebug(KIMAP_LOG) << "Received BYE: " << simplified.toString();
0225         return;
0226     }
0227 
0228     switch (state) {
0229     case Session::Disconnected:
0230         if (socketTimer.isActive()) {
0231             stopSocketTimer();
0232         }
0233         if (code == "OK") {
0234             setState(Session::NotAuthenticated);
0235 
0236             Response simplified = response;
0237             simplified.content.removeFirst(); // Strip the tag
0238             simplified.content.removeFirst(); // Strip the code
0239             greeting = simplified.toString().trimmed(); // Save the server greeting
0240             startNext();
0241         } else if (code == "PREAUTH") {
0242             setState(Session::Authenticated);
0243 
0244             Response simplified = response;
0245             simplified.content.removeFirst(); // Strip the tag
0246             simplified.content.removeFirst(); // Strip the code
0247             greeting = simplified.toString().trimmed(); // Save the server greeting
0248 
0249             startNext();
0250         } else {
0251             thread->closeSocket();
0252         }
0253         return;
0254     case Session::NotAuthenticated:
0255         if (code == "OK" && tag == authTag) {
0256             setState(Session::Authenticated);
0257         }
0258         break;
0259     case Session::Authenticated:
0260         if (code == "OK" && tag == selectTag) {
0261             setState(Session::Selected);
0262             currentMailBox = upcomingMailBox;
0263         }
0264         break;
0265     case Session::Selected:
0266         if ((code == "OK" && tag == closeTag) || (code != "OK" && tag == selectTag)) {
0267             setState(Session::Authenticated);
0268             currentMailBox = QByteArray();
0269         } else if (code == "OK" && tag == selectTag) {
0270             currentMailBox = upcomingMailBox;
0271         }
0272         break;
0273     }
0274 
0275     if (tag == authTag) {
0276         authTag.clear();
0277     }
0278     if (tag == selectTag) {
0279         selectTag.clear();
0280     }
0281     if (tag == closeTag) {
0282         closeTag.clear();
0283     }
0284 
0285     // If a job is running forward it the response
0286     if (currentJob != nullptr) {
0287         restartSocketTimer();
0288         currentJob->handleResponse(response);
0289     } else {
0290         qCWarning(KIMAP_LOG) << "A message was received from the server with no job to handle it:" << response.toString()
0291                              << '(' + response.toString().toHex() + ')';
0292     }
0293 }
0294 
0295 void SessionPrivate::setState(Session::State s)
0296 {
0297     if (s != state) {
0298         Session::State oldState = state;
0299         state = s;
0300         Q_EMIT q->stateChanged(state, oldState);
0301     }
0302 }
0303 
0304 QByteArray SessionPrivate::sendCommand(const QByteArray &command, const QByteArray &args)
0305 {
0306     QByteArray tag = 'A' + QByteArray::number(++tagCount).rightJustified(6, '0');
0307 
0308     QByteArray payload = tag + ' ' + command;
0309     if (!args.isEmpty()) {
0310         payload += ' ' + args;
0311     }
0312 
0313     sendData(payload);
0314 
0315     if (command == "LOGIN" || command == "AUTHENTICATE") {
0316         authTag = tag;
0317     } else if (command == "SELECT" || command == "EXAMINE") {
0318         selectTag = tag;
0319         upcomingMailBox = args;
0320         upcomingMailBox.remove(0, 1);
0321         upcomingMailBox = upcomingMailBox.left(upcomingMailBox.indexOf('\"'));
0322         upcomingMailBox = KIMAP::decodeImapFolderName(upcomingMailBox);
0323     } else if (command == "CLOSE") {
0324         closeTag = tag;
0325     }
0326     return tag;
0327 }
0328 
0329 void SessionPrivate::sendData(const QByteArray &data)
0330 {
0331     restartSocketTimer();
0332 
0333     if (logger && isConnected()) {
0334         logger->dataSent(data);
0335     }
0336 
0337     thread->sendData(data + "\r\n");
0338 }
0339 
0340 void SessionPrivate::socketConnected()
0341 {
0342     stopSocketTimer();
0343     isSocketConnected = true;
0344 
0345     bool willUseSsl = false;
0346     if (!queue.isEmpty()) {
0347         auto login = qobject_cast<KIMAP::LoginJob *>(queue.first());
0348         if (login) {
0349             willUseSsl = (login->encryptionMode() == KIMAP::LoginJob::SSLorTLS);
0350 
0351             userName = login->userName();
0352         }
0353     }
0354 
0355     if (state == Session::Disconnected && willUseSsl) {
0356         startSsl(QSsl::SecureProtocols);
0357     } else {
0358         startSocketTimer();
0359     }
0360 }
0361 
0362 bool SessionPrivate::isConnected() const
0363 {
0364     return state == Session::Authenticated || state == Session::Selected;
0365 }
0366 
0367 void SessionPrivate::socketDisconnected()
0368 {
0369     if (socketTimer.isActive()) {
0370         stopSocketTimer();
0371     }
0372 
0373     if (logger && isConnected()) {
0374         logger->disconnectionOccured();
0375     }
0376 
0377     if (isSocketConnected) {
0378         setState(Session::Disconnected);
0379         Q_EMIT q->connectionLost();
0380     } else {
0381         Q_EMIT q->connectionFailed();
0382     }
0383 
0384     isSocketConnected = false;
0385 
0386     clearJobQueue();
0387 }
0388 
0389 void SessionPrivate::socketActivity()
0390 {
0391     restartSocketTimer();
0392 }
0393 
0394 void SessionPrivate::socketError(QAbstractSocket::SocketError error)
0395 {
0396     if (socketTimer.isActive()) {
0397         stopSocketTimer();
0398     }
0399 
0400     if (currentJob) {
0401         currentJob->d_ptr->setSocketError(error);
0402     } else if (!queue.isEmpty()) {
0403         currentJob = queue.takeFirst();
0404         currentJob->d_ptr->setSocketError(error);
0405     }
0406 
0407     if (isSocketConnected) {
0408         thread->closeSocket();
0409     } else {
0410         Q_EMIT q->connectionFailed();
0411         clearJobQueue();
0412     }
0413 }
0414 
0415 void SessionPrivate::clearJobQueue()
0416 {
0417     if (currentJob) {
0418         currentJob->connectionLost();
0419     } else if (!queue.isEmpty()) {
0420         currentJob = queue.takeFirst();
0421         currentJob->connectionLost();
0422     }
0423 
0424     QQueue<Job *> queueCopy = queue; // copy because jobDestroyed calls removeAll
0425     qDeleteAll(queueCopy);
0426     queue.clear();
0427     Q_EMIT q->jobQueueSizeChanged(0);
0428 }
0429 
0430 void SessionPrivate::startSsl(QSsl::SslProtocol protocol)
0431 {
0432     thread->startSsl(protocol);
0433 }
0434 
0435 QString Session::selectedMailBox() const
0436 {
0437     return QString::fromUtf8(d->currentMailBox);
0438 }
0439 
0440 void SessionPrivate::onEncryptionNegotiationResult(bool isEncrypted, QSsl::SslProtocol protocol)
0441 {
0442     if (isEncrypted) {
0443         sslVersion = protocol;
0444     } else {
0445         sslVersion = QSsl::UnknownProtocol;
0446     }
0447     Q_EMIT encryptionNegotiationResult(isEncrypted);
0448 }
0449 
0450 QSsl::SslProtocol SessionPrivate::negotiatedEncryption() const
0451 {
0452     return sslVersion;
0453 }
0454 
0455 void SessionPrivate::setSocketTimeout(int ms)
0456 {
0457     bool timerActive = socketTimer.isActive();
0458 
0459     if (timerActive) {
0460         stopSocketTimer();
0461     }
0462 
0463     socketTimerInterval = ms;
0464 
0465     if (timerActive) {
0466         startSocketTimer();
0467     }
0468 }
0469 
0470 int SessionPrivate::socketTimeout() const
0471 {
0472     return socketTimerInterval;
0473 }
0474 
0475 void SessionPrivate::startSocketTimer()
0476 {
0477     if (socketTimerInterval < 0) {
0478         return;
0479     }
0480     Q_ASSERT(!socketTimer.isActive());
0481 
0482     socketTimer.start(socketTimerInterval);
0483 }
0484 
0485 void SessionPrivate::stopSocketTimer()
0486 {
0487     if (socketTimerInterval < 0) {
0488         return;
0489     }
0490 
0491     socketTimer.stop();
0492 }
0493 
0494 void SessionPrivate::restartSocketTimer()
0495 {
0496     if (socketTimer.isActive()) {
0497         stopSocketTimer();
0498     }
0499     startSocketTimer();
0500 }
0501 
0502 void SessionPrivate::onSocketTimeout()
0503 {
0504     qCDebug(KIMAP_LOG) << "Socket timeout!";
0505     thread->closeSocket();
0506 }
0507 
0508 void Session::setTimeout(int timeout)
0509 {
0510     d->setSocketTimeout(timeout * 1000);
0511 }
0512 
0513 int Session::timeout() const
0514 {
0515     return d->socketTimeout() / 1000;
0516 }
0517 
0518 #include "moc_session.cpp"
0519 #include "moc_session_p.cpp"