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"