1/*
2 Copyright (c) 2009 Kevin Ottens <ervin@kde.org>
3
4 Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
5 Author: Kevin Ottens <kevin@kdab.com>
6
7 This library is free software; you can redistribute it and/or modify it
8 under the terms of the GNU Library General Public License as published by
9 the Free Software Foundation; either version 2 of the License, or (at your
10 option) any later version.
11
12 This library is distributed in the hope that it will be useful, but WITHOUT
13 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
15 License for more details.
16
17 You should have received a copy of the GNU Library General Public License
18 along with this library; see the file COPYING.LIB. If not, write to the
19 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
20 02110-1301, USA.
21*/
22
23#include "session.h"
24#include "session_p.h"
25#include "sessionuiproxy.h"
26
27#include <QtCore/QDebug>
28#include <QtCore/QTimer>
29
30#include <KDebug>
31#include <KDE/KLocalizedString>
32
33#include "job.h"
34#include "loginjob.h"
35#include "message_p.h"
36#include "sessionlogger_p.h"
37#include "sessionthread_p.h"
38#include "rfccodecs.h"
39
40Q_DECLARE_METATYPE( KTcpSocket::SslVersion )
41Q_DECLARE_METATYPE( QSslSocket::SslMode )
42static const int _kimap_sslVersionId = qRegisterMetaType<KTcpSocket::SslVersion>();
43
44using namespace KIMAP;
45
46Session::Session( const QString &hostName, quint16 port, QObject *parent)
47 : QObject( parent ), d( new SessionPrivate( this ) )
48{
49 if ( !qgetenv( "KIMAP_LOGFILE" ).isEmpty() ) {
50 d->logger = new SessionLogger;
51 }
52
53 d->isSocketConnected = false;
54 d->state = Disconnected;
55 d->jobRunning = false;
56
57 d->thread = new SessionThread( hostName, port );
58 connect( d->thread, SIGNAL(encryptionNegotiationResult(bool,KTcpSocket::SslVersion)),
59 d, SLOT(onEncryptionNegotiationResult(bool,KTcpSocket::SslVersion)) );
60 connect( d->thread, SIGNAL(sslError(KSslErrorUiData)),
61 d, SLOT(handleSslError(KSslErrorUiData)) );
62 connect( d->thread, SIGNAL(socketDisconnected()),
63 d, SLOT(socketDisconnected()) );
64 connect( d->thread, SIGNAL(responseReceived(KIMAP::Message)),
65 d, SLOT(responseReceived(KIMAP::Message)) );
66 connect( d->thread, SIGNAL(socketConnected()),
67 d, SLOT(socketConnected()) );
68 connect( d->thread, SIGNAL(socketActivity()),
69 d, SLOT(socketActivity()) );
70 connect( d->thread, SIGNAL(socketError(KTcpSocket::Error)),
71 d, SLOT(socketError(KTcpSocket::Error)) );
72
73 d->socketTimer.setSingleShot( true );
74 connect( &d->socketTimer, SIGNAL(timeout()),
75 d, SLOT(onSocketTimeout()) );
76
77 d->startSocketTimer();
78}
79
80Session::~Session()
81{
82 delete d->thread;
83 d->thread = 0;
84}
85
86void Session::setUiProxy(SessionUiProxy::Ptr proxy)
87{
88 d->uiProxy = proxy;
89}
90
91void Session::setUiProxy(SessionUiProxy *proxy)
92{
93 setUiProxy( SessionUiProxy::Ptr( proxy ) );
94}
95
96QString Session::hostName() const
97{
98 return d->thread->hostName();
99}
100
101quint16 Session::port() const
102{
103 return d->thread->port();
104}
105
106Session::State Session::state() const
107{
108 return d->state;
109}
110
111QString Session::userName() const
112{
113 return d->userName;
114}
115
116QByteArray Session::serverGreeting() const
117{
118 return d->greeting;
119}
120
121int Session::jobQueueSize() const
122{
123 return d->queue.size() + ( d->jobRunning ? 1 : 0 );
124}
125
126void KIMAP::Session::close()
127{
128 d->thread->closeSocket();
129}
130
131void SessionPrivate::handleSslError(const KSslErrorUiData& errorData)
132{
133 const bool ignoreSslError = uiProxy && uiProxy->ignoreSslError( errorData );
134 //ignoreSslError is async, so the thread might already be gone when it returns
135 if ( thread ) {
136 thread->sslErrorHandlerResponse(ignoreSslError);
137 }
138}
139
140SessionPrivate::SessionPrivate( Session *session )
141 : QObject( session ),
142 q( session ),
143 state( Session::Disconnected ),
144 logger( 0 ),
145 currentJob( 0 ),
146 tagCount( 0 ),
147 sslVersion( KTcpSocket::UnknownSslVersion ),
148 socketTimerInterval( 30000 ) // By default timeouts on 30s
149{
150}
151
152SessionPrivate::~SessionPrivate()
153{
154 delete logger;
155}
156
157void SessionPrivate::addJob(Job *job)
158{
159 queue.append( job );
160 emit q->jobQueueSizeChanged( q->jobQueueSize() );
161
162 QObject::connect( job, SIGNAL(result(KJob*)), this, SLOT(jobDone(KJob*)) );
163 QObject::connect( job, SIGNAL(destroyed(QObject*)), this, SLOT(jobDestroyed(QObject*)) );
164
165 if ( state != Session::Disconnected ) {
166 startNext();
167 }
168}
169
170void SessionPrivate::startNext()
171{
172 QMetaObject::invokeMethod( this, "doStartNext" );
173}
174
175void SessionPrivate::doStartNext()
176{
177 if ( queue.isEmpty() || jobRunning || !isSocketConnected ) {
178 return;
179 }
180
181 restartSocketTimer();
182 jobRunning = true;
183
184 currentJob = queue.dequeue();
185 currentJob->doStart();
186}
187
188void SessionPrivate::jobDone( KJob *job )
189{
190 Q_UNUSED( job );
191 Q_ASSERT( job == currentJob );
192
193 stopSocketTimer();
194
195 jobRunning = false;
196 currentJob = 0;
197 emit q->jobQueueSizeChanged( q->jobQueueSize() );
198 startNext();
199}
200
201void SessionPrivate::jobDestroyed( QObject *job )
202{
203 queue.removeAll( static_cast<KIMAP::Job*>( job ) );
204 if ( currentJob == job ) {
205 currentJob = 0;
206 }
207}
208
209void SessionPrivate::responseReceived( const Message &response )
210{
211 if ( logger && ( state == Session::Authenticated || state == Session::Selected ) ) {
212 logger->dataReceived( response.toString() );
213 }
214
215 QByteArray tag;
216 QByteArray code;
217
218 if ( response.content.size()>=1 ) {
219 tag = response.content[0].toString();
220 }
221
222 if ( response.content.size()>=2 ) {
223 code = response.content[1].toString();
224 }
225
226 // BYE may arrive as part of a LOGOUT sequence or before the server closes the connection after an error.
227 // In any case we should wait until the server closes the connection, so we don't have to do anything.
228 if ( code == "BYE" ) {
229 Message simplified = response;
230 if ( simplified.content.size() >= 2 ) {
231 simplified.content.removeFirst(); // Strip the tag
232 simplified.content.removeFirst(); // Strip the code
233 }
234 kDebug() << "Received BYE: " << simplified.toString();
235 return;
236 }
237
238 switch ( state ) {
239 case Session::Disconnected:
240 if ( socketTimer.isActive() ) {
241 stopSocketTimer();
242 }
243 if ( code == "OK" ) {
244 setState( Session::NotAuthenticated );
245
246 Message simplified = response;
247 simplified.content.removeFirst(); // Strip the tag
248 simplified.content.removeFirst(); // Strip the code
249 greeting = simplified.toString().trimmed(); // Save the server greeting
250
251 startNext();
252 } else if ( code == "PREAUTH" ) {
253 setState( Session::Authenticated );
254
255 Message simplified = response;
256 simplified.content.removeFirst(); // Strip the tag
257 simplified.content.removeFirst(); // Strip the code
258 greeting = simplified.toString().trimmed(); // Save the server greeting
259
260 startNext();
261 } else {
262 thread->closeSocket();
263 }
264 return;
265 case Session::NotAuthenticated:
266 if ( code == "OK" && tag == authTag ) {
267 setState( Session::Authenticated );
268 }
269 break;
270 case Session::Authenticated:
271 if ( code == "OK" && tag == selectTag ) {
272 setState( Session::Selected );
273 currentMailBox = upcomingMailBox;
274 }
275 break;
276 case Session::Selected:
277 if ( ( code == "OK" && tag == closeTag ) ||
278 ( code != "OK" && tag == selectTag ) ) {
279 setState( Session::Authenticated );
280 currentMailBox = QByteArray();
281 } else if ( code == "OK" && tag == selectTag ) {
282 currentMailBox = upcomingMailBox;
283 }
284 break;
285 }
286
287 if ( tag == authTag ) {
288 authTag.clear();
289 }
290 if ( tag == selectTag ) {
291 selectTag.clear();
292 }
293 if ( tag == closeTag ) {
294 closeTag.clear();
295 }
296
297 // If a job is running forward it the response
298 if ( currentJob != 0 ) {
299 restartSocketTimer();
300 currentJob->handleResponse( response );
301 } else {
302 qWarning() << "A message was received from the server with no job to handle it:"
303 << response.toString()
304 << '(' + response.toString().toHex() + ')';
305 }
306}
307
308void SessionPrivate::setState(Session::State s)
309{
310 if ( s != state ) {
311 Session::State oldState = state;
312 state = s;
313 emit q->stateChanged( state, oldState );
314 }
315}
316
317QByteArray SessionPrivate::sendCommand( const QByteArray &command, const QByteArray &args )
318{
319 QByteArray tag = 'A' + QByteArray::number( ++tagCount ).rightJustified( 6, '0' );
320
321 QByteArray payload = tag + ' ' + command;
322 if ( !args.isEmpty() ) {
323 payload += ' ' + args;
324 }
325
326 sendData( payload );
327
328 if ( command == "LOGIN" || command == "AUTHENTICATE" ) {
329 authTag = tag;
330 } else if ( command == "SELECT" || command == "EXAMINE" ) {
331 selectTag = tag;
332 upcomingMailBox = args;
333 upcomingMailBox.remove( 0, 1 );
334 upcomingMailBox = upcomingMailBox.left( upcomingMailBox.indexOf( '\"') );
335 upcomingMailBox = KIMAP::decodeImapFolderName( upcomingMailBox );
336 } else if ( command == "CLOSE" ) {
337 closeTag = tag;
338 }
339 return tag;
340}
341
342void SessionPrivate::sendData( const QByteArray &data )
343{
344 restartSocketTimer();
345
346 if ( logger && ( state == Session::Authenticated || state == Session::Selected ) ) {
347 logger->dataSent( data );
348 }
349
350 thread->sendData( data + "\r\n" );
351}
352
353void SessionPrivate::socketConnected()
354{
355 stopSocketTimer();
356 isSocketConnected = true;
357
358 bool willUseSsl = false;
359 if ( !queue.isEmpty() ) {
360 KIMAP::LoginJob *login = qobject_cast<KIMAP::LoginJob*>( queue.first() );
361 if ( login ) {
362 willUseSsl = ( login->encryptionMode() == KIMAP::LoginJob::SslV2 ) ||
363 ( login->encryptionMode() == KIMAP::LoginJob::SslV3 ) ||
364 ( login->encryptionMode() == KIMAP::LoginJob::SslV3_1 ) ||
365 ( login->encryptionMode() == KIMAP::LoginJob::AnySslVersion );
366
367 userName = login->userName();
368 }
369 }
370
371 if ( state == Session::Disconnected && willUseSsl ) {
372 startNext();
373 } else {
374 startSocketTimer();
375 }
376}
377
378void SessionPrivate::socketDisconnected()
379{
380 if ( socketTimer.isActive() ) {
381 stopSocketTimer();
382 }
383
384 if ( logger && ( state == Session::Authenticated || state == Session::Selected ) ) {
385 logger->disconnectionOccured();
386 }
387
388 if ( state != Session::Disconnected ) {
389 setState( Session::Disconnected );
390 emit q->connectionLost();
391 } else {
392 emit q->connectionFailed();
393 }
394
395 isSocketConnected = false;
396
397 clearJobQueue();
398}
399
400void SessionPrivate::socketActivity()
401{
402 restartSocketTimer();
403}
404
405void SessionPrivate::socketError(KTcpSocket::Error error)
406{
407 if ( socketTimer.isActive() ) {
408 stopSocketTimer();
409 }
410
411 if ( currentJob ) {
412 currentJob->setSocketError(error);
413 } else if ( !queue.isEmpty() ) {
414 currentJob = queue.takeFirst();
415 currentJob->setSocketError(error);
416 }
417
418 if ( isSocketConnected ) {
419 thread->closeSocket();
420 } else {
421 emit q->connectionFailed();
422 emit q->connectionLost(); // KDE5: Remove this. We shouldn't emit connectionLost() if we weren't connected in the first place
423 clearJobQueue();
424 }
425}
426
427void SessionPrivate::clearJobQueue()
428{
429 if ( currentJob ) {
430 currentJob->connectionLost();
431 } else if ( !queue.isEmpty() ) {
432 currentJob = queue.takeFirst();
433 currentJob->connectionLost();
434 }
435
436 QQueue<Job*> queueCopy = queue; // copy because jobDestroyed calls removeAll
437 qDeleteAll(queueCopy);
438 queue.clear();
439 emit q->jobQueueSizeChanged( 0 );
440}
441
442void SessionPrivate::startSsl(const KTcpSocket::SslVersion &version)
443{
444 thread->startSsl( version );
445}
446
447QString Session::selectedMailBox() const
448{
449 return QString::fromUtf8( d->currentMailBox );
450}
451
452void SessionPrivate::onEncryptionNegotiationResult(bool isEncrypted, KTcpSocket::SslVersion version)
453{
454 if ( isEncrypted ) {
455 sslVersion = version;
456 } else {
457 sslVersion = KTcpSocket::UnknownSslVersion;
458 }
459 emit encryptionNegotiationResult( isEncrypted );
460}
461
462KTcpSocket::SslVersion SessionPrivate::negotiatedEncryption() const
463{
464 return sslVersion;
465}
466
467void SessionPrivate::setSocketTimeout( int ms )
468{
469 bool timerActive = socketTimer.isActive();
470
471 if ( timerActive ) {
472 stopSocketTimer();
473 }
474
475 socketTimerInterval = ms;
476
477 if ( timerActive ) {
478 startSocketTimer();
479 }
480}
481
482int SessionPrivate::socketTimeout() const
483{
484 return socketTimerInterval;
485}
486
487void SessionPrivate::startSocketTimer()
488{
489 if ( socketTimerInterval < 0 ) {
490 return;
491 }
492 Q_ASSERT( !socketTimer.isActive() );
493
494 socketTimer.start( socketTimerInterval );
495}
496
497void SessionPrivate::stopSocketTimer()
498{
499 if ( socketTimerInterval < 0 ) {
500 return;
501 }
502
503 socketTimer.stop();
504}
505
506void SessionPrivate::restartSocketTimer()
507{
508 if ( socketTimer.isActive() ) {
509 stopSocketTimer();
510 }
511 startSocketTimer();
512}
513
514void SessionPrivate::onSocketTimeout()
515{
516 kDebug() << "Socket timeout!";
517 thread->closeSocket();
518}
519
520void Session::setTimeout( int timeout )
521{
522 d->setSocketTimeout( timeout * 1000 );
523}
524
525int Session::timeout() const
526{
527 return d->socketTimeout() / 1000;
528}
529
530#include "moc_session.cpp"
531#include "moc_session_p.cpp"
532