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 | |
40 | Q_DECLARE_METATYPE( KTcpSocket::SslVersion ) |
41 | Q_DECLARE_METATYPE( QSslSocket::SslMode ) |
42 | static const int _kimap_sslVersionId = qRegisterMetaType<KTcpSocket::SslVersion>(); |
43 | |
44 | using namespace KIMAP; |
45 | |
46 | Session::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 | |
80 | Session::~Session() |
81 | { |
82 | delete d->thread; |
83 | d->thread = 0; |
84 | } |
85 | |
86 | void Session::setUiProxy(SessionUiProxy::Ptr proxy) |
87 | { |
88 | d->uiProxy = proxy; |
89 | } |
90 | |
91 | void Session::setUiProxy(SessionUiProxy *proxy) |
92 | { |
93 | setUiProxy( SessionUiProxy::Ptr( proxy ) ); |
94 | } |
95 | |
96 | QString Session::hostName() const |
97 | { |
98 | return d->thread->hostName(); |
99 | } |
100 | |
101 | quint16 Session::port() const |
102 | { |
103 | return d->thread->port(); |
104 | } |
105 | |
106 | Session::State Session::state() const |
107 | { |
108 | return d->state; |
109 | } |
110 | |
111 | QString Session::userName() const |
112 | { |
113 | return d->userName; |
114 | } |
115 | |
116 | QByteArray Session::serverGreeting() const |
117 | { |
118 | return d->greeting; |
119 | } |
120 | |
121 | int Session::jobQueueSize() const |
122 | { |
123 | return d->queue.size() + ( d->jobRunning ? 1 : 0 ); |
124 | } |
125 | |
126 | void KIMAP::Session::close() |
127 | { |
128 | d->thread->closeSocket(); |
129 | } |
130 | |
131 | void 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 | |
140 | SessionPrivate::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 | |
152 | SessionPrivate::~SessionPrivate() |
153 | { |
154 | delete logger; |
155 | } |
156 | |
157 | void 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 | |
170 | void SessionPrivate::startNext() |
171 | { |
172 | QMetaObject::invokeMethod( this, "doStartNext" ); |
173 | } |
174 | |
175 | void 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 | |
188 | void 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 | |
201 | void SessionPrivate::jobDestroyed( QObject *job ) |
202 | { |
203 | queue.removeAll( static_cast<KIMAP::Job*>( job ) ); |
204 | if ( currentJob == job ) { |
205 | currentJob = 0; |
206 | } |
207 | } |
208 | |
209 | void 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 | |
308 | void 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 | |
317 | QByteArray 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 | |
342 | void 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 | |
353 | void 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 | |
378 | void 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 | |
400 | void SessionPrivate::socketActivity() |
401 | { |
402 | restartSocketTimer(); |
403 | } |
404 | |
405 | void 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 | |
427 | void 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 | |
442 | void SessionPrivate::startSsl(const KTcpSocket::SslVersion &version) |
443 | { |
444 | thread->startSsl( version ); |
445 | } |
446 | |
447 | QString Session::selectedMailBox() const |
448 | { |
449 | return QString::fromUtf8( d->currentMailBox ); |
450 | } |
451 | |
452 | void 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 | |
462 | KTcpSocket::SslVersion SessionPrivate::negotiatedEncryption() const |
463 | { |
464 | return sslVersion; |
465 | } |
466 | |
467 | void 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 | |
482 | int SessionPrivate::socketTimeout() const |
483 | { |
484 | return socketTimerInterval; |
485 | } |
486 | |
487 | void SessionPrivate::startSocketTimer() |
488 | { |
489 | if ( socketTimerInterval < 0 ) { |
490 | return; |
491 | } |
492 | Q_ASSERT( !socketTimer.isActive() ); |
493 | |
494 | socketTimer.start( socketTimerInterval ); |
495 | } |
496 | |
497 | void SessionPrivate::stopSocketTimer() |
498 | { |
499 | if ( socketTimerInterval < 0 ) { |
500 | return; |
501 | } |
502 | |
503 | socketTimer.stop(); |
504 | } |
505 | |
506 | void SessionPrivate::restartSocketTimer() |
507 | { |
508 | if ( socketTimer.isActive() ) { |
509 | stopSocketTimer(); |
510 | } |
511 | startSocketTimer(); |
512 | } |
513 | |
514 | void SessionPrivate::onSocketTimeout() |
515 | { |
516 | kDebug() << "Socket timeout!" ; |
517 | thread->closeSocket(); |
518 | } |
519 | |
520 | void Session::setTimeout( int timeout ) |
521 | { |
522 | d->setSocketTimeout( timeout * 1000 ); |
523 | } |
524 | |
525 | int Session::timeout() const |
526 | { |
527 | return d->socketTimeout() / 1000; |
528 | } |
529 | |
530 | #include "moc_session.cpp" |
531 | #include "moc_session_p.cpp" |
532 | |