File indexing completed on 2024-11-17 04:45:09

0001 /*
0002     SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
0003     SPDX-FileContributor: Kevin Ottens <kevin@kdab.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "sessionpool.h"
0009 
0010 #include <QSslSocket>
0011 #include <QTimer>
0012 
0013 #include "imapresource_debug.h"
0014 #include <KLocalizedString>
0015 
0016 #include <KIMAP/CapabilitiesJob>
0017 #include <KIMAP/IdJob>
0018 #include <KIMAP/LogoutJob>
0019 #include <KIMAP/NamespaceJob>
0020 
0021 #include "imapaccount.h"
0022 #include "passwordrequesterinterface.h"
0023 
0024 qint64 SessionPool::m_requestCounter = 0;
0025 
0026 SessionPool::SessionPool(int maxPoolSize, QObject *parent)
0027     : QObject(parent)
0028     , m_maxPoolSize(maxPoolSize)
0029 {
0030 }
0031 
0032 SessionPool::~SessionPool()
0033 {
0034     disconnect(CloseSession);
0035 }
0036 
0037 PasswordRequesterInterface *SessionPool::passwordRequester() const
0038 {
0039     return m_passwordRequester;
0040 }
0041 
0042 void SessionPool::setPasswordRequester(PasswordRequesterInterface *requester)
0043 {
0044     delete m_passwordRequester;
0045 
0046     m_passwordRequester = requester;
0047     m_passwordRequester->setParent(this);
0048     QObject::connect(m_passwordRequester, &PasswordRequesterInterface::done, this, &SessionPool::onPasswordRequestDone);
0049 }
0050 
0051 void SessionPool::cancelPasswordRequests()
0052 {
0053     m_passwordRequester->cancelPasswordRequests();
0054 }
0055 
0056 KIMAP::SessionUiProxy::Ptr SessionPool::sessionUiProxy() const
0057 {
0058     return m_sessionUiProxy;
0059 }
0060 
0061 void SessionPool::setSessionUiProxy(KIMAP::SessionUiProxy::Ptr proxy)
0062 {
0063     m_sessionUiProxy = proxy;
0064 }
0065 
0066 bool SessionPool::isConnected() const
0067 {
0068     return m_initialConnectDone;
0069 }
0070 
0071 void SessionPool::requestPassword()
0072 {
0073     if (m_account->authenticationMode() == KIMAP::LoginJob::GSSAPI) {
0074         // for GSSAPI we don't have to ask for username/password, because it uses session wide tickets
0075         QMetaObject::invokeMethod(this,
0076                                   "onPasswordRequestDone",
0077                                   Qt::QueuedConnection,
0078                                   Q_ARG(int, PasswordRequesterInterface::PasswordRetrieved),
0079                                   Q_ARG(QString, QString()));
0080     } else {
0081         m_passwordRequester->requestPassword();
0082     }
0083 }
0084 
0085 bool SessionPool::connect(ImapAccount *account)
0086 {
0087     if (!account) {
0088         return false;
0089     }
0090 
0091     m_account = account;
0092     requestPassword();
0093 
0094     return true;
0095 }
0096 
0097 void SessionPool::disconnect(SessionTermination termination)
0098 {
0099     if (!m_account) {
0100         return;
0101     }
0102 
0103     const auto session{m_unusedPool + m_reservedPool + m_connectingPool};
0104     for (KIMAP::Session *s : session) {
0105         killSession(s, termination);
0106     }
0107     m_unusedPool.clear();
0108     m_reservedPool.clear();
0109     m_connectingPool.clear();
0110     m_pendingInitialSession = nullptr;
0111     m_passwordRequester->cancelPasswordRequests();
0112 
0113     delete m_account;
0114     m_account = nullptr;
0115     m_namespaces.clear();
0116     m_capabilities.clear();
0117 
0118     m_initialConnectDone = false;
0119     Q_EMIT disconnectDone();
0120 }
0121 
0122 qint64 SessionPool::requestSession()
0123 {
0124     if (!m_initialConnectDone) {
0125         return -1;
0126     }
0127 
0128     qint64 requestNumber = ++m_requestCounter;
0129 
0130     // The queue was empty, so trigger the processing
0131     if (m_pendingRequests.isEmpty()) {
0132         QTimer::singleShot(0, this, &SessionPool::processPendingRequests);
0133     }
0134 
0135     m_pendingRequests << requestNumber;
0136 
0137     return requestNumber;
0138 }
0139 
0140 void SessionPool::cancelSessionRequest(qint64 id)
0141 {
0142     Q_ASSERT(id > 0);
0143     m_pendingRequests.removeAll(id);
0144 }
0145 
0146 void SessionPool::releaseSession(KIMAP::Session *session)
0147 {
0148     const int removeSession = m_reservedPool.removeAll(session);
0149     if (removeSession > 0) {
0150         m_unusedPool << session;
0151     }
0152 }
0153 
0154 ImapAccount *SessionPool::account() const
0155 {
0156     return m_account;
0157 }
0158 
0159 QStringList SessionPool::serverCapabilities() const
0160 {
0161     return m_capabilities;
0162 }
0163 
0164 QList<KIMAP::MailBoxDescriptor> SessionPool::serverNamespaces() const
0165 {
0166     return m_namespaces;
0167 }
0168 
0169 QList<KIMAP::MailBoxDescriptor> SessionPool::serverNamespaces(Namespace ns) const
0170 {
0171     switch (ns) {
0172     case Personal:
0173         return m_personalNamespaces;
0174     case User:
0175         return m_userNamespaces;
0176     case Shared:
0177         return m_sharedNamespaces;
0178     default:
0179         break;
0180     }
0181     Q_ASSERT(false);
0182     return {};
0183 }
0184 
0185 void SessionPool::killSession(KIMAP::Session *session, SessionTermination termination)
0186 {
0187     Q_ASSERT(session);
0188 
0189     if (!m_unusedPool.contains(session) && !m_reservedPool.contains(session) && !m_connectingPool.contains(session)) {
0190         qCWarning(IMAPRESOURCE_LOG) << "Unmanaged session" << session;
0191         Q_ASSERT(false);
0192         return;
0193     }
0194     QObject::disconnect(session, &KIMAP::Session::connectionLost, this, &SessionPool::onConnectionLost);
0195     m_unusedPool.removeAll(session);
0196     m_reservedPool.removeAll(session);
0197     m_connectingPool.removeAll(session);
0198 
0199     if (session->state() != KIMAP::Session::Disconnected && termination == LogoutSession) {
0200         auto logout = new KIMAP::LogoutJob(session);
0201         QObject::connect(logout, &KJob::result, session, &QObject::deleteLater);
0202         logout->start();
0203     } else {
0204         session->close();
0205         session->deleteLater();
0206     }
0207 }
0208 
0209 void SessionPool::declareSessionReady(KIMAP::Session *session)
0210 {
0211     // This can happen if we happen to disconnect while capabilities and namespace are being retrieved,
0212     // resulting in us keeping a dangling pointer to a deleted session
0213     if (!m_connectingPool.contains(session)) {
0214         qCWarning(IMAPRESOURCE_LOG) << "Tried to declare a removed session ready";
0215         return;
0216     }
0217 
0218     m_pendingInitialSession = nullptr;
0219 
0220     if (!m_initialConnectDone) {
0221         m_initialConnectDone = true;
0222         Q_EMIT connectDone();
0223         // If the slot connected to connectDone() decided to disconnect the SessionPool
0224         // then we must end here, because we expect the pools to be empty now!
0225         if (!m_initialConnectDone) {
0226             return;
0227         }
0228     }
0229 
0230     m_connectingPool.removeAll(session);
0231 
0232     if (m_pendingRequests.isEmpty()) {
0233         m_unusedPool << session;
0234     } else {
0235         m_reservedPool << session;
0236         Q_EMIT sessionRequestDone(m_pendingRequests.takeFirst(), session);
0237 
0238         if (!m_pendingRequests.isEmpty()) {
0239             QTimer::singleShot(0, this, &SessionPool::processPendingRequests);
0240         }
0241     }
0242 }
0243 
0244 void SessionPool::cancelSessionCreation(KIMAP::Session *session, int errorCode, const QString &errorMessage)
0245 {
0246     m_pendingInitialSession = nullptr;
0247 
0248     QString msg;
0249     if (m_account) {
0250         msg = i18n("Could not connect to the IMAP-server %1.\n%2", m_account->server(), errorMessage);
0251     } else {
0252         // Can happen when we lose all ready connections while trying to establish
0253         // a new connection, for example.
0254         msg = i18n("Could not connect to the IMAP server.\n%1", errorMessage);
0255     }
0256 
0257     if (!m_initialConnectDone) {
0258         disconnect(); // kills all sessions, including \a session
0259     } else {
0260         if (session) {
0261             killSession(session, LogoutSession);
0262         }
0263         if (!m_pendingRequests.isEmpty()) {
0264             Q_EMIT sessionRequestDone(m_pendingRequests.takeFirst(), nullptr, errorCode, errorMessage);
0265             if (!m_pendingRequests.isEmpty()) {
0266                 QTimer::singleShot(0, this, &SessionPool::processPendingRequests);
0267             }
0268         }
0269     }
0270     // Always emit this at the end. This can call SessionPool::disconnect via ImapResource.
0271     Q_EMIT connectDone(errorCode, msg);
0272 }
0273 
0274 void SessionPool::processPendingRequests()
0275 {
0276     if (!m_account) {
0277         // The connection to the server is lost; no point processing pending requests
0278         for (int request : std::as_const(m_pendingRequests)) {
0279             Q_EMIT sessionRequestDone(request, nullptr, LoginFailError, i18n("Disconnected from server during login."));
0280         }
0281         return;
0282     }
0283 
0284     if (!m_unusedPool.isEmpty()) {
0285         // We have a session ready to give out
0286         KIMAP::Session *session = m_unusedPool.takeFirst();
0287         m_reservedPool << session;
0288         if (!m_pendingRequests.isEmpty()) {
0289             Q_EMIT sessionRequestDone(m_pendingRequests.takeFirst(), session);
0290             if (!m_pendingRequests.isEmpty()) {
0291                 QTimer::singleShot(0, this, &SessionPool::processPendingRequests);
0292             }
0293         }
0294     } else if (m_unusedPool.size() + m_reservedPool.size() < m_maxPoolSize) {
0295         // We didn't reach the max pool size yet so create a new one
0296         requestPassword();
0297     } else {
0298         // No session available, and max pool size reached
0299         if (!m_pendingRequests.isEmpty()) {
0300             Q_EMIT sessionRequestDone(m_pendingRequests.takeFirst(),
0301                                       nullptr,
0302                                       NoAvailableSessionError,
0303                                       i18n("Could not create another extra connection to the IMAP-server %1.", m_account->server()));
0304             if (!m_pendingRequests.isEmpty()) {
0305                 QTimer::singleShot(0, this, &SessionPool::processPendingRequests);
0306             }
0307         }
0308     }
0309 }
0310 
0311 void SessionPool::onPasswordRequestDone(int resultType, const QString &password)
0312 {
0313     QString errorMessage;
0314 
0315     if (!m_account) {
0316         // it looks like the connection was lost while we were waiting
0317         // for the password, we should fail all the pending requests and stop there
0318         for (int request : std::as_const(m_pendingRequests)) {
0319             Q_EMIT sessionRequestDone(request, nullptr, LoginFailError, i18n("Disconnected from server during login."));
0320         }
0321         return;
0322     }
0323 
0324     switch (resultType) {
0325     case PasswordRequesterInterface::PasswordRetrieved:
0326         // All is fine
0327         break;
0328     case PasswordRequesterInterface::ReconnectNeeded:
0329         cancelSessionCreation(m_pendingInitialSession, ReconnectNeededError, errorMessage);
0330         return;
0331     case PasswordRequesterInterface::UserRejected:
0332         errorMessage = i18n("Could not read the password: user rejected wallet access");
0333         if (m_pendingInitialSession) {
0334             cancelSessionCreation(m_pendingInitialSession, LoginFailError, errorMessage);
0335         } else {
0336             Q_EMIT connectDone(PasswordRequestError, errorMessage);
0337         }
0338         return;
0339     case PasswordRequesterInterface::EmptyPasswordEntered:
0340         errorMessage = i18n("Empty password");
0341         if (m_pendingInitialSession) {
0342             cancelSessionCreation(m_pendingInitialSession, LoginFailError, errorMessage);
0343         } else {
0344             Q_EMIT connectDone(PasswordRequestError, errorMessage);
0345         }
0346         return;
0347     }
0348 
0349     if (m_account->encryptionMode() != KIMAP::LoginJob::Unencrypted && !QSslSocket::supportsSsl()) {
0350         qCWarning(IMAPRESOURCE_LOG) << "Crypto not supported!";
0351         Q_EMIT connectDone(EncryptionError,
0352                            i18n("You requested TLS/SSL to connect to %1, but your "
0353                                 "system does not seem to be set up for that.",
0354                                 m_account->server()));
0355         disconnect();
0356         return;
0357     }
0358 
0359     KIMAP::Session *session = nullptr;
0360     if (m_pendingInitialSession) {
0361         session = m_pendingInitialSession;
0362     } else {
0363         session = new KIMAP::Session(m_account->server(), m_account->port(), this);
0364         QObject::connect(session, &QObject::destroyed, this, &SessionPool::onSessionDestroyed);
0365         session->setUiProxy(m_sessionUiProxy);
0366         session->setTimeout(m_account->timeout());
0367         session->setUseNetworkProxy(m_account->useNetworkProxy());
0368         m_connectingPool << session;
0369     }
0370 
0371     QObject::connect(session, &KIMAP::Session::connectionLost, this, &SessionPool::onConnectionLost);
0372 
0373     auto loginJob = new KIMAP::LoginJob(session);
0374     loginJob->setUserName(m_account->userName());
0375     loginJob->setPassword(password);
0376     loginJob->setEncryptionMode(m_account->encryptionMode());
0377     loginJob->setAuthenticationMode(m_account->authenticationMode());
0378 
0379     QObject::connect(loginJob, &KJob::result, this, &SessionPool::onLoginDone);
0380     loginJob->start();
0381 }
0382 
0383 void SessionPool::onLoginDone(KJob *job)
0384 {
0385     auto login = static_cast<KIMAP::LoginJob *>(job);
0386     // Can happen if we disconnected meanwhile
0387     if (!m_connectingPool.contains(login->session())) {
0388         Q_EMIT connectDone(CancelledError, i18n("Disconnected from server during login."));
0389         return;
0390     }
0391 
0392     if (job->error() == 0) {
0393         if (m_initialConnectDone) {
0394             declareSessionReady(login->session());
0395         } else {
0396             // On initial connection we ask for capabilities
0397             auto capJob = new KIMAP::CapabilitiesJob(login->session());
0398             QObject::connect(capJob, &KIMAP::CapabilitiesJob::result, this, &SessionPool::onCapabilitiesTestDone);
0399             capJob->start();
0400         }
0401     } else {
0402         if (job->error() == KIMAP::LoginJob::ERR_COULD_NOT_CONNECT) {
0403             if (m_account) {
0404                 cancelSessionCreation(login->session(),
0405                                       CouldNotConnectError,
0406                                       i18n("Could not connect to the IMAP-server %1.\n%2", m_account->server(), job->errorString()));
0407             } else {
0408                 // Can happen when we lose all ready connections while trying to login.
0409                 cancelSessionCreation(login->session(), CouldNotConnectError, i18n("Could not connect to the IMAP-server.\n%1", job->errorString()));
0410             }
0411         } else {
0412             // Connection worked, but login failed -> ask for a different password or ssl settings.
0413             m_pendingInitialSession = login->session();
0414             m_passwordRequester->requestPassword(PasswordRequesterInterface::WrongPasswordRequest, job->errorString());
0415         }
0416     }
0417 }
0418 
0419 void SessionPool::onCapabilitiesTestDone(KJob *job)
0420 {
0421     auto capJob = qobject_cast<KIMAP::CapabilitiesJob *>(job);
0422     // Can happen if we disconnected meanwhile
0423     if (!m_connectingPool.contains(capJob->session())) {
0424         Q_EMIT connectDone(CancelledError, i18n("Disconnected from server during login."));
0425         return;
0426     }
0427 
0428     if (job->error()) {
0429         if (m_account) {
0430             cancelSessionCreation(capJob->session(),
0431                                   CapabilitiesTestError,
0432                                   i18n("Could not test the capabilities supported by the "
0433                                        "IMAP server %1.\n%2",
0434                                        m_account->server(),
0435                                        job->errorString()));
0436         } else {
0437             // Can happen when we lose all ready connections while trying to check capabilities.
0438             cancelSessionCreation(capJob->session(),
0439                                   CapabilitiesTestError,
0440                                   i18n("Could not test the capabilities supported by the "
0441                                        "IMAP server.\n%1",
0442                                        job->errorString()));
0443         }
0444         return;
0445     }
0446 
0447     m_capabilities = capJob->capabilities();
0448 
0449     QStringList missing;
0450     const QStringList expected = {QStringLiteral("IMAP4REV1")};
0451     for (const QString &capability : expected) {
0452         if (!m_capabilities.contains(capability)) {
0453             missing << capability;
0454         }
0455     }
0456 
0457     if (!missing.isEmpty()) {
0458         cancelSessionCreation(capJob->session(),
0459                               IncompatibleServerError,
0460                               i18n("Cannot use the IMAP server %1, "
0461                                    "some mandatory capabilities are missing: %2. "
0462                                    "Please ask your sysadmin to upgrade the server.",
0463                                    m_account->server(),
0464                                    missing.join(QLatin1StringView(", "))));
0465         return;
0466     }
0467 
0468     // If the extension is supported, grab the namespaces from the server
0469     if (m_capabilities.contains(QLatin1StringView("NAMESPACE"))) {
0470         auto nsJob = new KIMAP::NamespaceJob(capJob->session());
0471         QObject::connect(nsJob, &KIMAP::NamespaceJob::result, this, &SessionPool::onNamespacesTestDone);
0472         nsJob->start();
0473         return;
0474     } else if (m_capabilities.contains(QLatin1StringView("ID"))) {
0475         auto idJob = new KIMAP::IdJob(capJob->session());
0476         idJob->setField("name", m_clientId);
0477         QObject::connect(idJob, &KIMAP::IdJob::result, this, &SessionPool::onIdDone);
0478         idJob->start();
0479         return;
0480     } else {
0481         declareSessionReady(capJob->session());
0482     }
0483 }
0484 
0485 void SessionPool::setClientId(const QByteArray &clientId)
0486 {
0487     m_clientId = clientId;
0488 }
0489 
0490 void SessionPool::onNamespacesTestDone(KJob *job)
0491 {
0492     auto nsJob = qobject_cast<KIMAP::NamespaceJob *>(job);
0493     // Can happen if we disconnect meanwhile
0494     if (!m_connectingPool.contains(nsJob->session())) {
0495         Q_EMIT connectDone(CancelledError, i18n("Disconnected from server during login."));
0496         return;
0497     }
0498 
0499     m_personalNamespaces = nsJob->personalNamespaces();
0500     m_userNamespaces = nsJob->userNamespaces();
0501     m_sharedNamespaces = nsJob->sharedNamespaces();
0502 
0503     if (nsJob->containsEmptyNamespace()) {
0504         // When we got the empty namespace here, we assume that the other
0505         // ones can be freely ignored and that the server will give us all
0506         // the mailboxes if we list from the empty namespace itself...
0507 
0508         m_namespaces.clear();
0509     } else {
0510         // ... otherwise we assume that we have to list explicitly each
0511         // namespace
0512 
0513         m_namespaces = nsJob->personalNamespaces() + nsJob->userNamespaces() + nsJob->sharedNamespaces();
0514     }
0515 
0516     if (m_capabilities.contains(QLatin1StringView("ID"))) {
0517         auto idJob = new KIMAP::IdJob(nsJob->session());
0518         idJob->setField("name", m_clientId);
0519         QObject::connect(idJob, &KIMAP::IdJob::result, this, &SessionPool::onIdDone);
0520         idJob->start();
0521         return;
0522     } else {
0523         declareSessionReady(nsJob->session());
0524     }
0525 }
0526 
0527 void SessionPool::onIdDone(KJob *job)
0528 {
0529     auto idJob = qobject_cast<KIMAP::IdJob *>(job);
0530     // Can happen if we disconnected meanwhile
0531     if (!m_connectingPool.contains(idJob->session())) {
0532         Q_EMIT connectDone(CancelledError, i18n("Disconnected during login."));
0533         return;
0534     }
0535     declareSessionReady(idJob->session());
0536 }
0537 
0538 void SessionPool::onConnectionLost()
0539 {
0540     auto session = static_cast<KIMAP::Session *>(sender());
0541 
0542     m_unusedPool.removeAll(session);
0543     m_reservedPool.removeAll(session);
0544     m_connectingPool.removeAll(session);
0545 
0546     if (m_unusedPool.isEmpty() && m_reservedPool.isEmpty()) {
0547         m_passwordRequester->cancelPasswordRequests();
0548         delete m_account;
0549         m_account = nullptr;
0550         m_namespaces.clear();
0551         m_capabilities.clear();
0552 
0553         m_initialConnectDone = false;
0554     }
0555 
0556     Q_EMIT connectionLost(session);
0557 
0558     if (!m_pendingRequests.isEmpty()) {
0559         cancelSessionCreation(nullptr, CouldNotConnectError, QString());
0560     }
0561 
0562     session->deleteLater();
0563     if (session == m_pendingInitialSession) {
0564         m_pendingInitialSession = nullptr;
0565     }
0566 }
0567 
0568 void SessionPool::onSessionDestroyed(QObject *object)
0569 {
0570     // Safety net for bugs that cause dangling session pointers
0571     auto session = static_cast<KIMAP::Session *>(object);
0572     bool sessionInPool = false;
0573     if (m_unusedPool.contains(session)) {
0574         qCWarning(IMAPRESOURCE_LOG) << "Session" << object << "destroyed while still in unused pool!";
0575         m_unusedPool.removeAll(session);
0576         sessionInPool = true;
0577     }
0578     if (m_reservedPool.contains(session)) {
0579         qCWarning(IMAPRESOURCE_LOG) << "Session" << object << "destroyed while still in reserved pool!";
0580         m_reservedPool.removeAll(session);
0581         sessionInPool = true;
0582     }
0583     if (m_connectingPool.contains(session)) {
0584         qCWarning(IMAPRESOURCE_LOG) << "Session" << object << "destroyed while still in connecting pool!";
0585         m_connectingPool.removeAll(session);
0586         sessionInPool = true;
0587     }
0588     Q_ASSERT(!sessionInPool);
0589 }
0590 
0591 #include "moc_sessionpool.cpp"