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"