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