1 /*
2 SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
3 SPDX-FileContributor: Kevin Ottens <kevin@kdab.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7
8 #include "sessionpool.h"
9
10 #include <QSslSocket>
11 #include <QTimer>
12
13 #include "imapresource_debug.h"
14 #include <KLocalizedString>
15
16 #include <KIMAP/CapabilitiesJob>
17 #include <KIMAP/IdJob>
18 #include <KIMAP/LogoutJob>
19 #include <KIMAP/NamespaceJob>
20
21 #include "imapaccount.h"
22 #include "passwordrequesterinterface.h"
23
24 qint64 SessionPool::m_requestCounter = 0;
25
SessionPool(int maxPoolSize,QObject * parent)26 SessionPool::SessionPool(int maxPoolSize, QObject *parent)
27 : QObject(parent)
28 , m_maxPoolSize(maxPoolSize)
29 {
30 }
31
~SessionPool()32 SessionPool::~SessionPool()
33 {
34 disconnect(CloseSession);
35 }
36
passwordRequester() const37 PasswordRequesterInterface *SessionPool::passwordRequester() const
38 {
39 return m_passwordRequester;
40 }
41
setPasswordRequester(PasswordRequesterInterface * requester)42 void SessionPool::setPasswordRequester(PasswordRequesterInterface *requester)
43 {
44 delete m_passwordRequester;
45
46 m_passwordRequester = requester;
47 m_passwordRequester->setParent(this);
48 QObject::connect(m_passwordRequester, &PasswordRequesterInterface::done, this, &SessionPool::onPasswordRequestDone);
49 }
50
cancelPasswordRequests()51 void SessionPool::cancelPasswordRequests()
52 {
53 m_passwordRequester->cancelPasswordRequests();
54 }
55
sessionUiProxy() const56 KIMAP::SessionUiProxy::Ptr SessionPool::sessionUiProxy() const
57 {
58 return m_sessionUiProxy;
59 }
60
setSessionUiProxy(KIMAP::SessionUiProxy::Ptr proxy)61 void SessionPool::setSessionUiProxy(KIMAP::SessionUiProxy::Ptr proxy)
62 {
63 m_sessionUiProxy = proxy;
64 }
65
isConnected() const66 bool SessionPool::isConnected() const
67 {
68 return m_initialConnectDone;
69 }
70
requestPassword()71 void SessionPool::requestPassword()
72 {
73 if (m_account->authenticationMode() == KIMAP::LoginJob::GSSAPI) {
74 // for GSSAPI we don't have to ask for username/password, because it uses session wide tickets
75 QMetaObject::invokeMethod(this,
76 "onPasswordRequestDone",
77 Qt::QueuedConnection,
78 Q_ARG(int, PasswordRequesterInterface::PasswordRetrieved),
79 Q_ARG(QString, QString()));
80 } else {
81 m_passwordRequester->requestPassword();
82 }
83 }
84
connect(ImapAccount * account)85 bool SessionPool::connect(ImapAccount *account)
86 {
87 if (m_account) {
88 return false;
89 }
90
91 m_account = account;
92 requestPassword();
93
94 return true;
95 }
96
disconnect(SessionTermination termination)97 void SessionPool::disconnect(SessionTermination termination)
98 {
99 if (!m_account) {
100 return;
101 }
102
103 const auto session{m_unusedPool + m_reservedPool + m_connectingPool};
104 for (KIMAP::Session *s : session) {
105 killSession(s, termination);
106 }
107 m_unusedPool.clear();
108 m_reservedPool.clear();
109 m_connectingPool.clear();
110 m_pendingInitialSession = nullptr;
111 m_passwordRequester->cancelPasswordRequests();
112
113 delete m_account;
114 m_account = nullptr;
115 m_namespaces.clear();
116 m_capabilities.clear();
117
118 m_initialConnectDone = false;
119 Q_EMIT disconnectDone();
120 }
121
requestSession()122 qint64 SessionPool::requestSession()
123 {
124 if (!m_initialConnectDone) {
125 return -1;
126 }
127
128 qint64 requestNumber = ++m_requestCounter;
129
130 // The queue was empty, so trigger the processing
131 if (m_pendingRequests.isEmpty()) {
132 QTimer::singleShot(0, this, &SessionPool::processPendingRequests);
133 }
134
135 m_pendingRequests << requestNumber;
136
137 return requestNumber;
138 }
139
cancelSessionRequest(qint64 id)140 void SessionPool::cancelSessionRequest(qint64 id)
141 {
142 Q_ASSERT(id > 0);
143 m_pendingRequests.removeAll(id);
144 }
145
releaseSession(KIMAP::Session * session)146 void SessionPool::releaseSession(KIMAP::Session *session)
147 {
148 const int removeSession = m_reservedPool.removeAll(session);
149 if (removeSession > 0) {
150 m_unusedPool << session;
151 }
152 }
153
account() const154 ImapAccount *SessionPool::account() const
155 {
156 return m_account;
157 }
158
serverCapabilities() const159 QStringList SessionPool::serverCapabilities() const
160 {
161 return m_capabilities;
162 }
163
serverNamespaces() const164 QList<KIMAP::MailBoxDescriptor> SessionPool::serverNamespaces() const
165 {
166 return m_namespaces;
167 }
168
serverNamespaces(Namespace ns) const169 QList<KIMAP::MailBoxDescriptor> SessionPool::serverNamespaces(Namespace ns) const
170 {
171 switch (ns) {
172 case Personal:
173 return m_personalNamespaces;
174 case User:
175 return m_userNamespaces;
176 case Shared:
177 return m_sharedNamespaces;
178 default:
179 break;
180 }
181 Q_ASSERT(false);
182 return QList<KIMAP::MailBoxDescriptor>();
183 }
184
killSession(KIMAP::Session * session,SessionTermination termination)185 void SessionPool::killSession(KIMAP::Session *session, SessionTermination termination)
186 {
187 Q_ASSERT(session);
188
189 if (!m_unusedPool.contains(session) && !m_reservedPool.contains(session) && !m_connectingPool.contains(session)) {
190 qCWarning(IMAPRESOURCE_LOG) << "Unmanaged session" << session;
191 Q_ASSERT(false);
192 return;
193 }
194 QObject::disconnect(session, &KIMAP::Session::connectionLost, this, &SessionPool::onConnectionLost);
195 m_unusedPool.removeAll(session);
196 m_reservedPool.removeAll(session);
197 m_connectingPool.removeAll(session);
198
199 if (session->state() != KIMAP::Session::Disconnected && termination == LogoutSession) {
200 auto logout = new KIMAP::LogoutJob(session);
201 QObject::connect(logout, &KJob::result, session, &QObject::deleteLater);
202 logout->start();
203 } else {
204 session->close();
205 session->deleteLater();
206 }
207 }
208
declareSessionReady(KIMAP::Session * session)209 void SessionPool::declareSessionReady(KIMAP::Session *session)
210 {
211 // This can happen if we happen to disconnect while capabilities and namespace are being retrieved,
212 // resulting in us keeping a dangling pointer to a deleted session
213 if (!m_connectingPool.contains(session)) {
214 qCWarning(IMAPRESOURCE_LOG) << "Tried to declare a removed session ready";
215 return;
216 }
217
218 m_pendingInitialSession = nullptr;
219
220 if (!m_initialConnectDone) {
221 m_initialConnectDone = true;
222 Q_EMIT connectDone();
223 // If the slot connected to connectDone() decided to disconnect the SessionPool
224 // then we must end here, because we expect the pools to be empty now!
225 if (!m_initialConnectDone) {
226 return;
227 }
228 }
229
230 m_connectingPool.removeAll(session);
231
232 if (m_pendingRequests.isEmpty()) {
233 m_unusedPool << session;
234 } else {
235 m_reservedPool << session;
236 Q_EMIT sessionRequestDone(m_pendingRequests.takeFirst(), session);
237
238 if (!m_pendingRequests.isEmpty()) {
239 QTimer::singleShot(0, this, &SessionPool::processPendingRequests);
240 }
241 }
242 }
243
cancelSessionCreation(KIMAP::Session * session,int errorCode,const QString & errorMessage)244 void SessionPool::cancelSessionCreation(KIMAP::Session *session, int errorCode, const QString &errorMessage)
245 {
246 m_pendingInitialSession = nullptr;
247
248 QString msg;
249 if (m_account) {
250 msg = i18n("Could not connect to the IMAP-server %1.\n%2", m_account->server(), errorMessage);
251 } else {
252 // Can happen when we lose all ready connections while trying to establish
253 // a new connection, for example.
254 msg = i18n("Could not connect to the IMAP server.\n%1", errorMessage);
255 }
256
257 if (!m_initialConnectDone) {
258 disconnect(); // kills all sessions, including \a session
259 } else {
260 if (session) {
261 killSession(session, LogoutSession);
262 }
263 if (!m_pendingRequests.isEmpty()) {
264 Q_EMIT sessionRequestDone(m_pendingRequests.takeFirst(), nullptr, errorCode, errorMessage);
265 if (!m_pendingRequests.isEmpty()) {
266 QTimer::singleShot(0, this, &SessionPool::processPendingRequests);
267 }
268 }
269 }
270 // Always emit this at the end. This can call SessionPool::disconnect via ImapResource.
271 Q_EMIT connectDone(errorCode, msg);
272 }
273
processPendingRequests()274 void SessionPool::processPendingRequests()
275 {
276 if (!m_account) {
277 // The connection to the server is lost; no point processing pending requests
278 for (int request : std::as_const(m_pendingRequests)) {
279 Q_EMIT sessionRequestDone(request, nullptr, LoginFailError, i18n("Disconnected from server during login."));
280 }
281 return;
282 }
283
284 if (!m_unusedPool.isEmpty()) {
285 // We have a session ready to give out
286 KIMAP::Session *session = m_unusedPool.takeFirst();
287 m_reservedPool << session;
288 if (!m_pendingRequests.isEmpty()) {
289 Q_EMIT sessionRequestDone(m_pendingRequests.takeFirst(), session);
290 if (!m_pendingRequests.isEmpty()) {
291 QTimer::singleShot(0, this, &SessionPool::processPendingRequests);
292 }
293 }
294 } else if (m_unusedPool.size() + m_reservedPool.size() < m_maxPoolSize) {
295 // We didn't reach the max pool size yet so create a new one
296 requestPassword();
297 } else {
298 // No session available, and max pool size reached
299 if (!m_pendingRequests.isEmpty()) {
300 Q_EMIT sessionRequestDone(m_pendingRequests.takeFirst(),
301 nullptr,
302 NoAvailableSessionError,
303 i18n("Could not create another extra connection to the IMAP-server %1.", m_account->server()));
304 if (!m_pendingRequests.isEmpty()) {
305 QTimer::singleShot(0, this, &SessionPool::processPendingRequests);
306 }
307 }
308 }
309 }
310
onPasswordRequestDone(int resultType,const QString & password)311 void SessionPool::onPasswordRequestDone(int resultType, const QString &password)
312 {
313 QString errorMessage;
314
315 if (!m_account) {
316 // it looks like the connection was lost while we were waiting
317 // for the password, we should fail all the pending requests and stop there
318 for (int request : std::as_const(m_pendingRequests)) {
319 Q_EMIT sessionRequestDone(request, nullptr, LoginFailError, i18n("Disconnected from server during login."));
320 }
321 return;
322 }
323
324 switch (resultType) {
325 case PasswordRequesterInterface::PasswordRetrieved:
326 // All is fine
327 break;
328 case PasswordRequesterInterface::ReconnectNeeded:
329 cancelSessionCreation(m_pendingInitialSession, ReconnectNeededError, errorMessage);
330 return;
331 case PasswordRequesterInterface::UserRejected:
332 errorMessage = i18n("Could not read the password: user rejected wallet access");
333 if (m_pendingInitialSession) {
334 cancelSessionCreation(m_pendingInitialSession, LoginFailError, errorMessage);
335 } else {
336 Q_EMIT connectDone(PasswordRequestError, errorMessage);
337 }
338 return;
339 case PasswordRequesterInterface::EmptyPasswordEntered:
340 errorMessage = i18n("Empty password");
341 if (m_pendingInitialSession) {
342 cancelSessionCreation(m_pendingInitialSession, LoginFailError, errorMessage);
343 } else {
344 Q_EMIT connectDone(PasswordRequestError, errorMessage);
345 }
346 return;
347 }
348
349 if (m_account->encryptionMode() != KIMAP::LoginJob::Unencrypted && !QSslSocket::supportsSsl()) {
350 qCWarning(IMAPRESOURCE_LOG) << "Crypto not supported!";
351 Q_EMIT connectDone(EncryptionError,
352 i18n("You requested TLS/SSL to connect to %1, but your "
353 "system does not seem to be set up for that.",
354 m_account->server()));
355 disconnect();
356 return;
357 }
358
359 KIMAP::Session *session = nullptr;
360 if (m_pendingInitialSession) {
361 session = m_pendingInitialSession;
362 } else {
363 session = new KIMAP::Session(m_account->server(), m_account->port(), this);
364 QObject::connect(session, &QObject::destroyed, this, &SessionPool::onSessionDestroyed);
365 session->setUiProxy(m_sessionUiProxy);
366 session->setTimeout(m_account->timeout());
367 session->setUseNetworkProxy(m_account->useNetworkProxy());
368 m_connectingPool << session;
369 }
370
371 QObject::connect(session, &KIMAP::Session::connectionLost, this, &SessionPool::onConnectionLost);
372
373 auto loginJob = new KIMAP::LoginJob(session);
374 loginJob->setUserName(m_account->userName());
375 loginJob->setPassword(password);
376 loginJob->setEncryptionMode(m_account->encryptionMode());
377 loginJob->setAuthenticationMode(m_account->authenticationMode());
378
379 QObject::connect(loginJob, &KJob::result, this, &SessionPool::onLoginDone);
380 loginJob->start();
381 }
382
onLoginDone(KJob * job)383 void SessionPool::onLoginDone(KJob *job)
384 {
385 auto login = static_cast<KIMAP::LoginJob *>(job);
386 // Can happen if we disconnected meanwhile
387 if (!m_connectingPool.contains(login->session())) {
388 Q_EMIT connectDone(CancelledError, i18n("Disconnected from server during login."));
389 return;
390 }
391
392 if (job->error() == 0) {
393 if (m_initialConnectDone) {
394 declareSessionReady(login->session());
395 } else {
396 // On initial connection we ask for capabilities
397 auto capJob = new KIMAP::CapabilitiesJob(login->session());
398 QObject::connect(capJob, &KIMAP::CapabilitiesJob::result, this, &SessionPool::onCapabilitiesTestDone);
399 capJob->start();
400 }
401 } else {
402 if (job->error() == KIMAP::LoginJob::ERR_COULD_NOT_CONNECT) {
403 if (m_account) {
404 cancelSessionCreation(login->session(),
405 CouldNotConnectError,
406 i18n("Could not connect to the IMAP-server %1.\n%2", m_account->server(), job->errorString()));
407 } else {
408 // Can happen when we lose all ready connections while trying to login.
409 cancelSessionCreation(login->session(), CouldNotConnectError, i18n("Could not connect to the IMAP-server.\n%1", job->errorString()));
410 }
411 } else {
412 // Connection worked, but login failed -> ask for a different password or ssl settings.
413 m_pendingInitialSession = login->session();
414 m_passwordRequester->requestPassword(PasswordRequesterInterface::WrongPasswordRequest, job->errorString());
415 }
416 }
417 }
418
onCapabilitiesTestDone(KJob * job)419 void SessionPool::onCapabilitiesTestDone(KJob *job)
420 {
421 auto capJob = qobject_cast<KIMAP::CapabilitiesJob *>(job);
422 // Can happen if we disconnected meanwhile
423 if (!m_connectingPool.contains(capJob->session())) {
424 Q_EMIT connectDone(CancelledError, i18n("Disconnected from server during login."));
425 return;
426 }
427
428 if (job->error()) {
429 if (m_account) {
430 cancelSessionCreation(capJob->session(),
431 CapabilitiesTestError,
432 i18n("Could not test the capabilities supported by the "
433 "IMAP server %1.\n%2",
434 m_account->server(),
435 job->errorString()));
436 } else {
437 // Can happen when we lose all ready connections while trying to check capabilities.
438 cancelSessionCreation(capJob->session(),
439 CapabilitiesTestError,
440 i18n("Could not test the capabilities supported by the "
441 "IMAP server.\n%1",
442 job->errorString()));
443 }
444 return;
445 }
446
447 m_capabilities = capJob->capabilities();
448
449 QStringList missing;
450 const QStringList expected = {QStringLiteral("IMAP4REV1")};
451 for (const QString &capability : expected) {
452 if (!m_capabilities.contains(capability)) {
453 missing << capability;
454 }
455 }
456
457 if (!missing.isEmpty()) {
458 cancelSessionCreation(capJob->session(),
459 IncompatibleServerError,
460 i18n("Cannot use the IMAP server %1, "
461 "some mandatory capabilities are missing: %2. "
462 "Please ask your sysadmin to upgrade the server.",
463 m_account->server(),
464 missing.join(QLatin1String(", "))));
465 return;
466 }
467
468 // If the extension is supported, grab the namespaces from the server
469 if (m_capabilities.contains(QLatin1String("NAMESPACE"))) {
470 auto nsJob = new KIMAP::NamespaceJob(capJob->session());
471 QObject::connect(nsJob, &KIMAP::NamespaceJob::result, this, &SessionPool::onNamespacesTestDone);
472 nsJob->start();
473 return;
474 } else if (m_capabilities.contains(QLatin1String("ID"))) {
475 auto idJob = new KIMAP::IdJob(capJob->session());
476 idJob->setField("name", m_clientId);
477 QObject::connect(idJob, &KIMAP::IdJob::result, this, &SessionPool::onIdDone);
478 idJob->start();
479 return;
480 } else {
481 declareSessionReady(capJob->session());
482 }
483 }
484
setClientId(const QByteArray & clientId)485 void SessionPool::setClientId(const QByteArray &clientId)
486 {
487 m_clientId = clientId;
488 }
489
onNamespacesTestDone(KJob * job)490 void SessionPool::onNamespacesTestDone(KJob *job)
491 {
492 auto nsJob = qobject_cast<KIMAP::NamespaceJob *>(job);
493 // Can happen if we disconnect meanwhile
494 if (!m_connectingPool.contains(nsJob->session())) {
495 Q_EMIT connectDone(CancelledError, i18n("Disconnected from server during login."));
496 return;
497 }
498
499 m_personalNamespaces = nsJob->personalNamespaces();
500 m_userNamespaces = nsJob->userNamespaces();
501 m_sharedNamespaces = nsJob->sharedNamespaces();
502
503 if (nsJob->containsEmptyNamespace()) {
504 // When we got the empty namespace here, we assume that the other
505 // ones can be freely ignored and that the server will give us all
506 // the mailboxes if we list from the empty namespace itself...
507
508 m_namespaces.clear();
509 } else {
510 // ... otherwise we assume that we have to list explicitly each
511 // namespace
512
513 m_namespaces = nsJob->personalNamespaces() + nsJob->userNamespaces() + nsJob->sharedNamespaces();
514 }
515
516 if (m_capabilities.contains(QLatin1String("ID"))) {
517 auto idJob = new KIMAP::IdJob(nsJob->session());
518 idJob->setField("name", m_clientId);
519 QObject::connect(idJob, &KIMAP::IdJob::result, this, &SessionPool::onIdDone);
520 idJob->start();
521 return;
522 } else {
523 declareSessionReady(nsJob->session());
524 }
525 }
526
onIdDone(KJob * job)527 void SessionPool::onIdDone(KJob *job)
528 {
529 auto idJob = qobject_cast<KIMAP::IdJob *>(job);
530 // Can happen if we disconnected meanwhile
531 if (!m_connectingPool.contains(idJob->session())) {
532 Q_EMIT connectDone(CancelledError, i18n("Disconnected during login."));
533 return;
534 }
535 declareSessionReady(idJob->session());
536 }
537
onConnectionLost()538 void SessionPool::onConnectionLost()
539 {
540 auto session = static_cast<KIMAP::Session *>(sender());
541
542 m_unusedPool.removeAll(session);
543 m_reservedPool.removeAll(session);
544 m_connectingPool.removeAll(session);
545
546 if (m_unusedPool.isEmpty() && m_reservedPool.isEmpty()) {
547 m_passwordRequester->cancelPasswordRequests();
548 delete m_account;
549 m_account = nullptr;
550 m_namespaces.clear();
551 m_capabilities.clear();
552
553 m_initialConnectDone = false;
554 }
555
556 Q_EMIT connectionLost(session);
557
558 if (!m_pendingRequests.isEmpty()) {
559 cancelSessionCreation(nullptr, CouldNotConnectError, QString());
560 }
561
562 session->deleteLater();
563 if (session == m_pendingInitialSession) {
564 m_pendingInitialSession = nullptr;
565 }
566 }
567
onSessionDestroyed(QObject * object)568 void SessionPool::onSessionDestroyed(QObject *object)
569 {
570 // Safety net for bugs that cause dangling session pointers
571 auto session = static_cast<KIMAP::Session *>(object);
572 bool sessionInPool = false;
573 if (m_unusedPool.contains(session)) {
574 qCWarning(IMAPRESOURCE_LOG) << "Session" << object << "destroyed while still in unused pool!";
575 m_unusedPool.removeAll(session);
576 sessionInPool = true;
577 }
578 if (m_reservedPool.contains(session)) {
579 qCWarning(IMAPRESOURCE_LOG) << "Session" << object << "destroyed while still in reserved pool!";
580 m_reservedPool.removeAll(session);
581 sessionInPool = true;
582 }
583 if (m_connectingPool.contains(session)) {
584 qCWarning(IMAPRESOURCE_LOG) << "Session" << object << "destroyed while still in connecting pool!";
585 m_connectingPool.removeAll(session);
586 sessionInPool = true;
587 }
588 Q_ASSERT(!sessionInPool);
589 }
590