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

0001 /*
0002     Copyright (c) 2009 Kevin Ottens <ervin@kde.org>
0003     Copyright (c) 2017 Christian Mollekopf <mollekopf@kolabsys.com>
0004 
0005     Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
0006     Author: Kevin Ottens <kevin@kdab.com>
0007 
0008     This library is free software; you can redistribute it and/or modify it
0009     under the terms of the GNU Library General Public License as published by
0010     the Free Software Foundation; either version 2 of the License, or (at your
0011     option) any later version.
0012 
0013     This library is distributed in the hope that it will be useful, but WITHOUT
0014     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
0015     FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
0016     License for more details.
0017 
0018     You should have received a copy of the GNU Library General Public License
0019     along with this library; see the file COPYING.LIB.  If not, write to the
0020     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
0021     02110-1301, USA.
0022 */
0023 
0024 #include "session.h"
0025 #include "session_p.h"
0026 
0027 #include <QDebug>
0028 
0029 #include "kimap_debug.h"
0030 
0031 #include "job.h"
0032 #include "message_p.h"
0033 #include "sessionlogger_p.h"
0034 #include "rfccodecs.h"
0035 #include "imapstreamparser.h"
0036 
0037 Q_DECLARE_METATYPE(QSsl::SslProtocol)
0038 Q_DECLARE_METATYPE(QSslSocket::SslMode)
0039 static const int _kimap_sslVersionId = qRegisterMetaType<QSsl::SslProtocol>();
0040 
0041 using namespace KIMAP2;
0042 
0043 Session::Session(const QString &hostName, quint16 port, QObject *parent)
0044     : QObject(parent), d(new SessionPrivate(this))
0045 {
0046     if (!qEnvironmentVariableIsEmpty("KIMAP2_LOGFILE")) {
0047         d->logger.reset(new SessionLogger);
0048         qCInfo(KIMAP2_LOG) << "Logging traffic to: " << QLatin1String(qgetenv("KIMAP2_LOGFILE"));
0049     }
0050     if (qEnvironmentVariableIsSet("KIMAP2_TRAFFIC")) {
0051         d->dumpTraffic = true;
0052         qCInfo(KIMAP2_LOG) << "Dumping traffic.";
0053     }
0054     if (qEnvironmentVariableIsSet("KIMAP2_TIMING")) {
0055         d->trackTime = true;
0056         qCInfo(KIMAP2_LOG) << "Tracking timings.";
0057     }
0058 
0059     d->state = Disconnected;
0060     d->jobRunning = false;
0061     d->hostName = hostName;
0062     d->port = port;
0063 
0064     connect(d->socket.data(), &QIODevice::readyRead, d, &SessionPrivate::readMessage);
0065 
0066     connect(d->socket.data(), &QSslSocket::connected,
0067             d, &SessionPrivate::socketConnected);
0068     connect(d->socket.data(), static_cast<void (QSslSocket::*)(const QList<QSslError>&)>(&QSslSocket::sslErrors),
0069             d, &SessionPrivate::handleSslErrors);
0070     connect(d->socket.data(), static_cast<void (QSslSocket::*)(QAbstractSocket::SocketError)>(&QSslSocket::error),
0071             d, &SessionPrivate::socketError);
0072 
0073     connect(d->socket.data(), &QIODevice::bytesWritten,
0074             d, &SessionPrivate::socketActivity);
0075     connect(d->socket.data(), &QSslSocket::encryptedBytesWritten,
0076             d, &SessionPrivate::socketActivity);
0077     connect(d->socket.data(), &QIODevice::readyRead,
0078             d, &SessionPrivate::socketActivity);
0079     connect(d->socket.data(), &QAbstractSocket::stateChanged, [this](QAbstractSocket::SocketState state) {
0080         qCDebug(KIMAP2_LOG) << "Socket state changed: " << state;
0081         //The disconnected signal will not fire if we fail to lookup the host, but this will.
0082         if (state == QAbstractSocket::UnconnectedState) {
0083             d->socketDisconnected();
0084         }
0085         if (state == QAbstractSocket::HostLookupState) {
0086             d->hostLookupInProgress = true;
0087         } else {
0088             d->hostLookupInProgress = false;
0089         }
0090     });
0091 
0092     d->socketTimer.setSingleShot(true);
0093     connect(&d->socketTimer, &QTimer::timeout,
0094             d, &SessionPrivate::onSocketTimeout);
0095 
0096     d->socketProgressTimer.setSingleShot(false);
0097     connect(&d->socketProgressTimer, &QTimer::timeout,
0098             d, &SessionPrivate::onSocketProgressTimeout);
0099 
0100     d->startSocketTimer();
0101     qCDebug(KIMAP2_LOG) << "Connecting to: " << hostName << port;
0102     d->socket->connectToHost(hostName, port);
0103 }
0104 
0105 Session::~Session()
0106 {
0107     //Make sure all jobs know we're done
0108     d->clearJobQueue();
0109     delete d;
0110 }
0111 
0112 QString Session::hostName() const
0113 {
0114     return d->hostName;
0115 }
0116 
0117 quint16 Session::port() const
0118 {
0119     return d->port;
0120 }
0121 
0122 Session::State Session::state() const
0123 {
0124     return d->state;
0125 }
0126 
0127 bool Session::isConnected() const
0128 {
0129     return (d->state == Authenticated || d->state == Selected);
0130 }
0131 
0132 QString Session::userName() const
0133 {
0134     return d->userName;
0135 }
0136 
0137 QByteArray Session::serverGreeting() const
0138 {
0139     return d->greeting;
0140 }
0141 
0142 int Session::jobQueueSize() const
0143 {
0144     return d->queue.size() + (d->jobRunning ? 1 : 0);
0145 }
0146 
0147 void Session::close()
0148 {
0149     d->closeSocket();
0150 }
0151 
0152 void Session::ignoreErrors(const QList<QSslError> &errors)
0153 {
0154     d->socket->ignoreSslErrors(errors);
0155 }
0156 
0157 void Session::setTimeout(int timeout)
0158 {
0159     d->setSocketTimeout(timeout * 1000);
0160 }
0161 
0162 int Session::timeout() const
0163 {
0164     return d->socketTimeout() / 1000;
0165 }
0166 
0167 QString Session::selectedMailBox() const
0168 {
0169     return QString::fromUtf8(d->currentMailBox);
0170 }
0171 
0172 
0173 SessionPrivate::SessionPrivate(Session *session)
0174     : QObject(session),
0175       q(session),
0176       state(Session::Disconnected),
0177       hostLookupInProgress(false),
0178       logger(Q_NULLPTR),
0179       currentJob(Q_NULLPTR),
0180       tagCount(0),
0181       socketTimerInterval(30000),   // By default timeouts on 30s
0182       socketProgressInterval(3000),   // mention we're still alive every 3s
0183       socket(new QSslSocket),
0184       stream(new ImapStreamParser(socket.data())),
0185       accumulatedWaitTime(0),
0186       accumulatedProcessingTime(0),
0187       trackTime(false),
0188       dumpTraffic(false)
0189 {
0190     //For windows this needs to be set before connecting according to the docs
0191     socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1);
0192     stream->onResponseReceived([this](const Message &message) {
0193         responseReceived(message);
0194     });
0195 }
0196 
0197 SessionPrivate::~SessionPrivate()
0198 {
0199 }
0200 
0201 void SessionPrivate::handleSslErrors(const QList<QSslError> &errors)
0202 {
0203     emit q->sslErrors(errors);
0204 }
0205 
0206 void SessionPrivate::addJob(Job *job)
0207 {
0208     queue.append(job);
0209     emit q->jobQueueSizeChanged(q->jobQueueSize());
0210 
0211     QObject::connect(job, &KJob::result, this, &SessionPrivate::jobDone);
0212     QObject::connect(job, &QObject::destroyed, this, &SessionPrivate::jobDestroyed);
0213     startNext();
0214 }
0215 
0216 void SessionPrivate::startNext()
0217 {
0218     QMetaObject::invokeMethod(this, "doStartNext");
0219 }
0220 
0221 void SessionPrivate::doStartNext()
0222 {
0223     //Wait until we are ready to process
0224     if (queue.isEmpty()
0225         || jobRunning
0226         || socket->state() == QSslSocket::ConnectingState
0227         || socket->state() == QSslSocket::HostLookupState) {
0228         return;
0229     }
0230 
0231     currentJob = queue.dequeue();
0232 
0233     //Since we aren't connecting we may never get back. Cancel the job
0234     if (socket->state() == QSslSocket::UnconnectedState) {
0235         qCDebug(KIMAP2_LOG) << "Cancelling job due to lack of connection: " << currentJob->metaObject()->className();
0236         currentJob->connectionLost();
0237         return;
0238     }
0239 
0240     if (trackTime) {
0241         time.start();
0242     }
0243     restartSocketTimer();
0244     jobRunning = true;
0245     currentJob->doStart();
0246 }
0247 
0248 void SessionPrivate::jobDone(KJob *job)
0249 {
0250     Q_UNUSED(job);
0251     Q_ASSERT(job == currentJob);
0252     qCDebug(KIMAP2_LOG) << "Job done: " << job->metaObject()->className();
0253 
0254     stopSocketTimer();
0255 
0256     jobRunning = false;
0257     currentJob = Q_NULLPTR;
0258     emit q->jobQueueSizeChanged(q->jobQueueSize());
0259     startNext();
0260 }
0261 
0262 void SessionPrivate::jobDestroyed(QObject *job)
0263 {
0264     queue.removeAll(static_cast<KIMAP2::Job *>(job));
0265     if (currentJob == job) {
0266         currentJob = Q_NULLPTR;
0267     }
0268 }
0269 
0270 void SessionPrivate::responseReceived(const Message &response)
0271 {
0272     if (dumpTraffic) {
0273         qCInfo(KIMAP2_LOG) << "S: " << QString::fromLatin1(response.toString());
0274     }
0275     if (logger && q->isConnected()) {
0276         logger->dataReceived(response.toString());
0277     }
0278 
0279     QByteArray tag;
0280     QByteArray code;
0281 
0282     if (response.content.size() >= 1) {
0283         tag = response.content[0].toString();
0284     }
0285 
0286     if (response.content.size() >= 2) {
0287         code = response.content[1].toString();
0288     }
0289 
0290     // BYE may arrive as part of a LOGOUT sequence or before the server closes the connection after an error.
0291     // In any case we should wait until the server closes the connection, so we don't have to do anything.
0292     if (code == "BYE") {
0293         Message simplified = response;
0294         if (simplified.content.size() >= 2) {
0295             simplified.content.removeFirst(); // Strip the tag
0296             simplified.content.removeFirst(); // Strip the code
0297         }
0298         qCDebug(KIMAP2_LOG) << "Received BYE: " << simplified.toString();
0299         return;
0300     }
0301 
0302     switch (state) {
0303     case Session::Disconnected:
0304         stopSocketTimer();
0305         if (code == "OK") {
0306             Message simplified = response;
0307             simplified.content.removeFirst(); // Strip the tag
0308             simplified.content.removeFirst(); // Strip the code
0309             greeting = simplified.toString().trimmed(); // Save the server greeting
0310             setState(Session::NotAuthenticated);
0311         } else if (code == "PREAUTH") {
0312             Message simplified = response;
0313             simplified.content.removeFirst(); // Strip the tag
0314             simplified.content.removeFirst(); // Strip the code
0315             greeting = simplified.toString().trimmed(); // Save the server greeting
0316             setState(Session::Authenticated);
0317         } else {
0318             //We have been rejected
0319             closeSocket();
0320         }
0321         return;
0322     case Session::NotAuthenticated:
0323         if (code == "OK" && tag == authTag) {
0324             setState(Session::Authenticated);
0325         }
0326         break;
0327     case Session::Authenticated:
0328         if (code == "OK" && tag == selectTag) {
0329             setState(Session::Selected);
0330             currentMailBox = upcomingMailBox;
0331         }
0332         break;
0333     case Session::Selected:
0334         if ((code == "OK" && tag == closeTag) ||
0335                 (code != "OK" && tag == selectTag)) {
0336             setState(Session::Authenticated);
0337             currentMailBox = QByteArray();
0338         } else if (code == "OK" && tag == selectTag) {
0339             currentMailBox = upcomingMailBox;
0340         }
0341         break;
0342     }
0343 
0344     if (tag == authTag) {
0345         authTag.clear();
0346     }
0347     if (tag == selectTag) {
0348         selectTag.clear();
0349     }
0350     if (tag == closeTag) {
0351         closeTag.clear();
0352     }
0353 
0354     // If a job is running forward it the response
0355     if (currentJob) {
0356         restartSocketTimer();
0357         currentJob->handleResponse(response);
0358     } else {
0359         qCWarning(KIMAP2_LOG) << "A message was received from the server with no job to handle it:"
0360                              << response.toString()
0361                              << '(' + response.toString().toHex() + ')';
0362     }
0363 }
0364 
0365 void SessionPrivate::setState(Session::State s)
0366 {
0367     if (s != state) {
0368         Session::State oldState = state;
0369         state = s;
0370         emit q->stateChanged(state, oldState);
0371     }
0372 }
0373 
0374 QByteArray SessionPrivate::sendCommand(const QByteArray &command, const QByteArray &args)
0375 {
0376     QByteArray tag = 'A' + QByteArray::number(++tagCount).rightJustified(6, '0');
0377 
0378     QByteArray payload = tag + ' ' + command;
0379     if (!args.isEmpty()) {
0380         payload += ' ' + args;
0381     }
0382 
0383     sendData(payload);
0384 
0385     if (command == "LOGIN" || command == "AUTHENTICATE") {
0386         authTag = tag;
0387     } else if (command == "SELECT" || command == "EXAMINE") {
0388         selectTag = tag;
0389         upcomingMailBox = args;
0390         upcomingMailBox.remove(0, 1);
0391         upcomingMailBox = upcomingMailBox.left(upcomingMailBox.indexOf('\"'));
0392         upcomingMailBox = KIMAP2::decodeImapFolderName(upcomingMailBox);
0393     } else if (command == "CLOSE") {
0394         closeTag = tag;
0395     }
0396     return tag;
0397 }
0398 
0399 void SessionPrivate::sendData(const QByteArray &data)
0400 {
0401     restartSocketTimer();
0402 
0403     if (dumpTraffic) {
0404         qCInfo(KIMAP2_LOG) << "C: " << data;
0405     }
0406     if (logger && q->isConnected()) {
0407         logger->dataSent(data);
0408     }
0409 
0410     dataQueue.enqueue(data + "\r\n");
0411     QMetaObject::invokeMethod(this, "writeDataQueue");
0412 }
0413 
0414 void SessionPrivate::socketConnected()
0415 {
0416     qCInfo(KIMAP2_LOG) << "Socket connected.";
0417     //Detect if the connection is no longer available
0418     socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1);
0419     startNext();
0420 }
0421 
0422 void SessionPrivate::socketDisconnected()
0423 {
0424     qCInfo(KIMAP2_LOG) << "Socket disconnected.";
0425     stopSocketTimer();
0426 
0427     if (logger && q->isConnected()) {
0428         logger->disconnectionOccured();
0429     }
0430 
0431     if (state != Session::Disconnected) {
0432         setState(Session::Disconnected);
0433     } else {
0434         //If we timeout during host lookup we don't receive an explicit host lookup error
0435         if (hostLookupInProgress) {
0436             socketError(QAbstractSocket::HostNotFoundError);
0437             hostLookupInProgress = false;
0438         }
0439         emit q->connectionFailed();
0440     }
0441 
0442     clearJobQueue();
0443 }
0444 
0445 void SessionPrivate::socketActivity()
0446 {
0447     //This slot can be called after the job has already finished, in that case we don't want to restart the timer
0448     if (currentJob) {
0449         restartSocketTimer();
0450     }
0451 }
0452 
0453 void SessionPrivate::socketError(QAbstractSocket::SocketError error)
0454 {
0455     qCDebug(KIMAP2_LOG) << "Socket error: " << error;
0456     stopSocketTimer();
0457 
0458     if (currentJob) {
0459         qCWarning(KIMAP2_LOG) << "Socket error:" << error;
0460         currentJob->setSocketError(error);
0461     } else if (!queue.isEmpty()) {
0462         qCWarning(KIMAP2_LOG) << "Socket error:" << error;
0463         currentJob = queue.takeFirst();
0464         currentJob->setSocketError(error);
0465     }
0466 
0467     closeSocket();
0468 }
0469 
0470 void SessionPrivate::clearJobQueue()
0471 {
0472     if (!currentJob && !queue.isEmpty()) {
0473         currentJob = queue.takeFirst();
0474     }
0475     if (currentJob) {
0476         currentJob->connectionLost();
0477     }
0478 
0479     QQueue<Job *> queueCopy = queue; // copy because jobDestroyed calls removeAll
0480     qDeleteAll(queueCopy);
0481     queue.clear();
0482     emit q->jobQueueSizeChanged(0);
0483 }
0484 
0485 void SessionPrivate::startSsl(QSsl::SslProtocol protocol)
0486 {
0487     socket->setProtocol(protocol);
0488     connect(socket.data(), &QSslSocket::encrypted, this, &SessionPrivate::sslConnected);
0489     if (socket->state() == QAbstractSocket::ConnectedState) {
0490         qCDebug(KIMAP2_LOG) << "Starting client encryption";
0491         Q_ASSERT(socket->mode() == QSslSocket::UnencryptedMode);
0492         socket->startClientEncryption();
0493     } else {
0494         qCWarning(KIMAP2_LOG) << "The socket is not yet connected";
0495     }
0496 }
0497 
0498 void SessionPrivate::sslConnected()
0499 {
0500     qCDebug(KIMAP2_LOG) << "ssl is connected";
0501     emit encryptionNegotiationResult(true);
0502 }
0503 
0504 void SessionPrivate::setSocketTimeout(int ms)
0505 {
0506     bool timerActive = socketTimer.isActive();
0507 
0508     if (timerActive) {
0509         stopSocketTimer();
0510     }
0511 
0512     socketTimerInterval = ms;
0513 
0514     if (timerActive) {
0515         startSocketTimer();
0516     }
0517 }
0518 
0519 int SessionPrivate::socketTimeout() const
0520 {
0521     return socketTimerInterval;
0522 }
0523 
0524 void SessionPrivate::startSocketTimer()
0525 {
0526     if (socketTimerInterval < 0) {
0527         return;
0528     }
0529     Q_ASSERT(!socketTimer.isActive());
0530 
0531     socketTimer.start(socketTimerInterval);
0532     socketProgressTimer.start(socketProgressInterval);
0533 }
0534 
0535 void SessionPrivate::stopSocketTimer()
0536 {
0537     socketTimer.stop();
0538     socketProgressTimer.stop();
0539 }
0540 
0541 void SessionPrivate::restartSocketTimer()
0542 {
0543     stopSocketTimer();
0544     startSocketTimer();
0545 }
0546 
0547 void SessionPrivate::onSocketTimeout()
0548 {
0549     qCWarning(KIMAP2_LOG) << "Aborting on socket timeout. " << socketTimerInterval;
0550     if (!currentJob && !queue.isEmpty()) {
0551         currentJob = queue.takeFirst();
0552     }
0553     if (currentJob) {
0554         qCWarning(KIMAP2_LOG) << "Current job: " << currentJob->metaObject()->className();
0555         currentJob->setErrorMessage("Aborting on socket timeout. Interval " + QString::number(socketTimerInterval) + " ms");
0556     }
0557     socket->abort();
0558     socketProgressTimer.stop();
0559 }
0560 
0561 QString SessionPrivate::getStateName() const 
0562 {
0563     if (hostLookupInProgress) {
0564         return "Host lookup";
0565     }
0566     switch (state) {
0567         case Session::Disconnected:
0568             return "Disconnected";
0569         case Session::NotAuthenticated:
0570             return "NotAuthenticated";
0571         case Session::Authenticated:
0572             return "Authenticated";
0573         case Session::Selected:
0574         default:
0575             break;
0576     }
0577     return "Unknown State";
0578 }
0579 
0580 void SessionPrivate::onSocketProgressTimeout()
0581 {
0582     if (currentJob) {
0583         qCDebug(KIMAP2_LOG) << "Processing job: " << currentJob->metaObject()->className() << "Current state: " << getStateName() << (socket ? socket->state() : QAbstractSocket::UnconnectedState);
0584     } else {
0585         qCDebug(KIMAP2_LOG) << "Next job: " << (queue.isEmpty() ? "No job" : queue.head()->metaObject()->className()) << "Current state: " << getStateName() << (socket ? socket->state() : QAbstractSocket::UnconnectedState);
0586     }
0587 }
0588 
0589 void SessionPrivate::writeDataQueue()
0590 {
0591     while (!dataQueue.isEmpty()) {
0592         socket->write(dataQueue.dequeue());
0593     }
0594 }
0595 
0596 void SessionPrivate::readMessage()
0597 {
0598     if (trackTime) {
0599         accumulatedWaitTime += time.elapsed();
0600         time.start();
0601     }
0602     stream->parseStream();
0603     if (stream->error()) {
0604         qCWarning(KIMAP2_LOG) << "Error while parsing, closing connection.";
0605         qCDebug(KIMAP2_LOG) << "Current buffer: " << stream->currentBuffer();
0606         socket->close();
0607     }
0608     if (trackTime) {
0609         accumulatedProcessingTime += time.elapsed();
0610         time.start();
0611         qCDebug(KIMAP2_LOG) << "Wait vs process vs total: " << accumulatedWaitTime << accumulatedProcessingTime << accumulatedWaitTime + accumulatedProcessingTime;
0612     }
0613 }
0614 
0615 void SessionPrivate::closeSocket()
0616 {
0617     qCDebug(KIMAP2_LOG) << "Closing socket.";
0618     socket->close();
0619 }
0620 
0621 #include "moc_session.cpp"
0622 #include "moc_session_p.cpp"