KIMAP

session.cpp
1/*
2 SPDX-FileCopyrightText: 2009 Kevin Ottens <ervin@kde.org>
3
4 SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
5 SPDX-FileContributor: Kevin Ottens <kevin@kdab.com>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "session.h"
11#include "session_p.h"
12
13#include <QPointer>
14#include <QTimer>
15
16#include "kimap_debug.h"
17
18#include "job.h"
19#include "job_p.h"
20#include "loginjob.h"
21#include "response_p.h"
22#include "rfccodecs.h"
23#include "sessionlogger_p.h"
24#include "sessionthread_p.h"
25
26Q_DECLARE_METATYPE(QSsl::SslProtocol)
27Q_DECLARE_METATYPE(QSslSocket::SslMode)
28static const int _kimap_sslVersionId = qRegisterMetaType<QSsl::SslProtocol>();
29
30using namespace KIMAP;
31
32Session::Session(const QString &hostName, quint16 port, QObject *parent)
33 : QObject(parent)
34 , d(new SessionPrivate(this))
35{
36 if (!qEnvironmentVariableIsEmpty("KIMAP_LOGFILE")) {
37 d->logger = new SessionLogger;
38 }
39
40 d->isSocketConnected = false;
41 d->state = Disconnected;
42 d->jobRunning = false;
43
44 d->thread = new SessionThread(hostName, port);
45 connect(d->thread, &SessionThread::encryptionNegotiationResult, d, &SessionPrivate::onEncryptionNegotiationResult);
46 connect(d->thread, &SessionThread::sslError, d, &SessionPrivate::handleSslError);
47 connect(d->thread, &SessionThread::socketDisconnected, d, &SessionPrivate::socketDisconnected);
48 connect(d->thread, &SessionThread::responseReceived, d, &SessionPrivate::responseReceived);
49 connect(d->thread, &SessionThread::socketConnected, d, &SessionPrivate::socketConnected);
50 connect(d->thread, &SessionThread::socketActivity, d, &SessionPrivate::socketActivity);
51 connect(d->thread, &SessionThread::socketError, d, &SessionPrivate::socketError);
52
53 d->socketTimer.setSingleShot(true);
54 connect(&d->socketTimer, &QTimer::timeout, d, &SessionPrivate::onSocketTimeout);
55
56 d->startSocketTimer();
57}
58
59Session::~Session()
60{
61 // Make sure all jobs know we're done
62 d->socketDisconnected();
63 delete d->thread;
64 d->thread = nullptr;
65}
66
67void Session::setUiProxy(const SessionUiProxy::Ptr &proxy)
68{
69 d->uiProxy = proxy;
70}
71
72void Session::setUiProxy(SessionUiProxy *proxy)
73{
74 setUiProxy(SessionUiProxy::Ptr(proxy));
75}
76
77QString Session::hostName() const
78{
79 return d->thread->hostName();
80}
81
82quint16 Session::port() const
83{
84 return d->thread->port();
85}
86
87void Session::setUseNetworkProxy(bool useProxy)
88{
89 d->thread->setUseNetworkProxy(useProxy);
90}
91
92Session::State Session::state() const
93{
94 return d->state;
95}
96
97QString Session::userName() const
98{
99 return d->userName;
100}
101
102QByteArray Session::serverGreeting() const
103{
104 return d->greeting;
105}
106
107int Session::jobQueueSize() const
108{
109 return d->queue.size() + (d->jobRunning ? 1 : 0);
110}
111
112void KIMAP::Session::close()
113{
114 d->thread->closeSocket();
115}
116
117void SessionPrivate::handleSslError(const KSslErrorUiData &errorData)
118{
119 // ignoreSslError is async, so the thread might already be gone when it returns
120 QPointer<SessionThread> _t = thread;
121 const bool ignoreSslError = uiProxy && uiProxy->ignoreSslError(errorData);
122 if (_t) {
123 _t->sslErrorHandlerResponse(ignoreSslError);
124 }
125}
126
127SessionPrivate::SessionPrivate(Session *session)
128 : QObject(session)
129 , q(session)
130 , isSocketConnected(false)
131 , state(Session::Disconnected)
132 , logger(nullptr)
133 , thread(nullptr)
134 , jobRunning(false)
135 , currentJob(nullptr)
136 , tagCount(0)
137 , sslVersion(QSsl::UnknownProtocol)
138 , socketTimerInterval(30000) // By default timeouts on 30s
139{
140}
141
142SessionPrivate::~SessionPrivate()
143{
144 delete logger;
145}
146
147void SessionPrivate::addJob(Job *job)
148{
149 queue.append(job);
150 Q_EMIT q->jobQueueSizeChanged(q->jobQueueSize());
151
152 QObject::connect(job, &KJob::result, this, &SessionPrivate::jobDone);
153 QObject::connect(job, &QObject::destroyed, this, &SessionPrivate::jobDestroyed);
154
155 if (state != Session::Disconnected) {
156 startNext();
157 }
158}
159
160void SessionPrivate::startNext()
161{
162 QMetaObject::invokeMethod(this, &SessionPrivate::doStartNext);
163}
164
165void SessionPrivate::doStartNext()
166{
167 if (queue.isEmpty() || jobRunning || !isSocketConnected) {
168 return;
169 }
170
171 restartSocketTimer();
172 jobRunning = true;
173
174 currentJob = queue.dequeue();
175 currentJob->doStart();
176}
177
178void SessionPrivate::jobDone(KJob *job)
179{
180 Q_UNUSED(job)
181 Q_ASSERT(job == currentJob);
182
183 stopSocketTimer();
184
185 jobRunning = false;
186 currentJob = nullptr;
187 Q_EMIT q->jobQueueSizeChanged(q->jobQueueSize());
188 startNext();
189}
190
191void SessionPrivate::jobDestroyed(QObject *job)
192{
193 queue.removeAll(static_cast<KIMAP::Job *>(job));
194 if (currentJob == job) {
195 currentJob = nullptr;
196 }
197}
198
199void SessionPrivate::responseReceived(const Response &response)
200{
201 if (logger && isConnected()) {
202 logger->dataReceived(response.toString());
203 }
204
205 QByteArray tag;
206 QByteArray code;
207
208 if (response.content.size() >= 1) {
209 tag = response.content[0].toString();
210 }
211
212 if (response.content.size() >= 2) {
213 code = response.content[1].toString();
214 }
215
216 // BYE may arrive as part of a LOGOUT sequence or before the server closes the connection after an error.
217 // In any case we should wait until the server closes the connection, so we don't have to do anything.
218 if (code == "BYE") {
219 Response simplified = response;
220 if (simplified.content.size() >= 2) {
221 simplified.content.removeFirst(); // Strip the tag
222 simplified.content.removeFirst(); // Strip the code
223 }
224 qCDebug(KIMAP_LOG) << "Received BYE: " << simplified.toString();
225 return;
226 }
227
228 switch (state) {
229 case Session::Disconnected:
230 if (socketTimer.isActive()) {
231 stopSocketTimer();
232 }
233 if (code == "OK") {
234 setState(Session::NotAuthenticated);
235
236 Response simplified = response;
237 simplified.content.removeFirst(); // Strip the tag
238 simplified.content.removeFirst(); // Strip the code
239 greeting = simplified.toString().trimmed(); // Save the server greeting
240 startNext();
241 } else if (code == "PREAUTH") {
242 setState(Session::Authenticated);
243
244 Response simplified = response;
245 simplified.content.removeFirst(); // Strip the tag
246 simplified.content.removeFirst(); // Strip the code
247 greeting = simplified.toString().trimmed(); // Save the server greeting
248
249 startNext();
250 } else {
251 thread->closeSocket();
252 }
253 return;
254 case Session::NotAuthenticated:
255 if (code == "OK" && tag == authTag) {
256 setState(Session::Authenticated);
257 }
258 break;
259 case Session::Authenticated:
260 if (code == "OK" && tag == selectTag) {
261 setState(Session::Selected);
262 currentMailBox = upcomingMailBox;
263 }
264 break;
265 case Session::Selected:
266 if ((code == "OK" && tag == closeTag) || (code != "OK" && tag == selectTag)) {
267 setState(Session::Authenticated);
268 currentMailBox = QByteArray();
269 } else if (code == "OK" && tag == selectTag) {
270 currentMailBox = upcomingMailBox;
271 }
272 break;
273 }
274
275 if (tag == authTag) {
276 authTag.clear();
277 }
278 if (tag == selectTag) {
279 selectTag.clear();
280 }
281 if (tag == closeTag) {
282 closeTag.clear();
283 }
284
285 // If a job is running forward it the response
286 if (currentJob != nullptr) {
287 restartSocketTimer();
288 currentJob->handleResponse(response);
289 } else {
290 qCWarning(KIMAP_LOG) << "A message was received from the server with no job to handle it:" << response.toString()
291 << '(' + response.toString().toHex() + ')';
292 }
293}
294
295void SessionPrivate::setState(Session::State s)
296{
297 if (s != state) {
298 Session::State oldState = state;
299 state = s;
300 Q_EMIT q->stateChanged(state, oldState);
301 }
302}
303
304QByteArray SessionPrivate::sendCommand(const QByteArray &command, const QByteArray &args)
305{
306 QByteArray tag = 'A' + QByteArray::number(++tagCount).rightJustified(6, '0');
307
308 QByteArray payload = tag + ' ' + command;
309 if (!args.isEmpty()) {
310 payload += ' ' + args;
311 }
312
313 sendData(payload);
314
315 if (command == "LOGIN" || command == "AUTHENTICATE") {
316 authTag = tag;
317 } else if (command == "SELECT" || command == "EXAMINE") {
318 selectTag = tag;
319 upcomingMailBox = args;
320 upcomingMailBox.remove(0, 1);
321 upcomingMailBox = upcomingMailBox.left(upcomingMailBox.indexOf('\"'));
322 upcomingMailBox = KIMAP::decodeImapFolderName(upcomingMailBox);
323 } else if (command == "CLOSE") {
324 closeTag = tag;
325 }
326 return tag;
327}
328
329void SessionPrivate::sendData(const QByteArray &data)
330{
331 restartSocketTimer();
332
333 if (logger && isConnected()) {
334 logger->dataSent(data);
335 }
336
337 thread->sendData(data + "\r\n");
338}
339
340void SessionPrivate::socketConnected()
341{
342 stopSocketTimer();
343 isSocketConnected = true;
344
345 bool willUseSsl = false;
346 if (!queue.isEmpty()) {
347 auto login = qobject_cast<KIMAP::LoginJob *>(queue.first());
348 if (login) {
349 willUseSsl = (login->encryptionMode() == KIMAP::LoginJob::SSLorTLS);
350
351 userName = login->userName();
352 }
353 }
354
355 if (state == Session::Disconnected && willUseSsl) {
356 startSsl(QSsl::SecureProtocols);
357 } else {
358 startSocketTimer();
359 }
360}
361
362bool SessionPrivate::isConnected() const
363{
364 return state == Session::Authenticated || state == Session::Selected;
365}
366
367void SessionPrivate::socketDisconnected()
368{
369 if (socketTimer.isActive()) {
370 stopSocketTimer();
371 }
372
373 if (logger && isConnected()) {
374 logger->disconnectionOccured();
375 }
376
377 if (isSocketConnected) {
378 setState(Session::Disconnected);
379 Q_EMIT q->connectionLost();
380 } else {
381 Q_EMIT q->connectionFailed();
382 }
383
384 isSocketConnected = false;
385
386 clearJobQueue();
387}
388
389void SessionPrivate::socketActivity()
390{
391 restartSocketTimer();
392}
393
394void SessionPrivate::socketError(QAbstractSocket::SocketError error)
395{
396 if (socketTimer.isActive()) {
397 stopSocketTimer();
398 }
399
400 if (currentJob) {
401 currentJob->d_ptr->setSocketError(error);
402 } else if (!queue.isEmpty()) {
403 currentJob = queue.takeFirst();
404 currentJob->d_ptr->setSocketError(error);
405 }
406
407 if (isSocketConnected) {
408 thread->closeSocket();
409 } else {
410 Q_EMIT q->connectionFailed();
411 clearJobQueue();
412 }
413}
414
415void SessionPrivate::clearJobQueue()
416{
417 if (currentJob) {
418 currentJob->connectionLost();
419 } else if (!queue.isEmpty()) {
420 currentJob = queue.takeFirst();
421 currentJob->connectionLost();
422 }
423
424 QQueue<Job *> queueCopy = queue; // copy because jobDestroyed calls removeAll
425 qDeleteAll(queueCopy);
426 queue.clear();
427 Q_EMIT q->jobQueueSizeChanged(0);
428}
429
430void SessionPrivate::startSsl(QSsl::SslProtocol protocol)
431{
432 thread->startSsl(protocol);
433}
434
435QString Session::selectedMailBox() const
436{
437 return QString::fromUtf8(d->currentMailBox);
438}
439
440void SessionPrivate::onEncryptionNegotiationResult(bool isEncrypted, QSsl::SslProtocol protocol)
441{
442 if (isEncrypted) {
443 sslVersion = protocol;
444 } else {
445 sslVersion = QSsl::UnknownProtocol;
446 }
447 Q_EMIT encryptionNegotiationResult(isEncrypted);
448}
449
450QSsl::SslProtocol SessionPrivate::negotiatedEncryption() const
451{
452 return sslVersion;
453}
454
455void SessionPrivate::setSocketTimeout(int ms)
456{
457 bool timerActive = socketTimer.isActive();
458
459 if (timerActive) {
460 stopSocketTimer();
461 }
462
463 socketTimerInterval = ms;
464
465 if (timerActive) {
466 startSocketTimer();
467 }
468}
469
470int SessionPrivate::socketTimeout() const
471{
472 return socketTimerInterval;
473}
474
475void SessionPrivate::startSocketTimer()
476{
477 if (socketTimerInterval < 0) {
478 return;
479 }
480 Q_ASSERT(!socketTimer.isActive());
481
482 socketTimer.start(socketTimerInterval);
483}
484
485void SessionPrivate::stopSocketTimer()
486{
487 if (socketTimerInterval < 0) {
488 return;
489 }
490
491 socketTimer.stop();
492}
493
494void SessionPrivate::restartSocketTimer()
495{
496 if (socketTimer.isActive()) {
497 stopSocketTimer();
498 }
499 startSocketTimer();
500}
501
502void SessionPrivate::onSocketTimeout()
503{
504 qCDebug(KIMAP_LOG) << "Socket timeout!";
505 thread->closeSocket();
506}
507
508void Session::setTimeout(int timeout)
509{
510 d->setSocketTimeout(timeout * 1000);
511}
512
513int Session::timeout() const
514{
515 return d->socketTimeout() / 1000;
516}
517
518#include "moc_session.cpp"
519#include "moc_session_p.cpp"
Interface to display communication errors and wait for user feedback.
void result(KJob *job)
QCA_EXPORT Logger * logger()
bool isEmpty() const const
QByteArray left(qsizetype len) const const
QByteArray number(double n, char format, int precision)
QByteArray & remove(qsizetype pos, qsizetype len)
QByteArray & removeFirst()
QByteArray rightJustified(qsizetype width, char fill, bool truncate) const const
qsizetype size() const const
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void destroyed(QObject *obj)
QString fromUtf8(QByteArrayView str)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
This file is part of the IMAP support library and defines the RfcCodecs class.
KIMAP_EXPORT QByteArray decodeImapFolderName(const QByteArray &inSrc)
Converts an UTF-7 encoded IMAP mailbox to a QByteArray.
Definition rfccodecs.cpp:53
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:14:37 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.