1/*
2 Copyright (c) 2009 Kevin Ottens <ervin@kde.org>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#include "sessionthread_p.h"
21
22#include <QtCore/QDebug>
23#include <QtCore/QThread>
24
25#include <KDE/KDebug>
26
27#include "imapstreamparser.h"
28#include "message_p.h"
29
30using namespace KIMAP;
31
32Q_DECLARE_METATYPE( KTcpSocket::Error )
33Q_DECLARE_METATYPE( KSslErrorUiData )
34static const int _kimap_socketErrorTypeId = qRegisterMetaType<KTcpSocket::Error>();
35static const int _kimap_sslErrorUiData = qRegisterMetaType<KSslErrorUiData>();
36
37SessionThread::SessionThread( const QString &hostName, quint16 port )
38 : QObject(), m_hostName( hostName ), m_port( port ),
39 m_socket( 0 ), m_stream( 0 ), m_mutex(),
40 m_encryptedMode( false )
41{
42 // Just like the Qt docs now recommend, for event-driven threads:
43 // don't derive from QThread, create one directly and move the object to it.
44 QThread* thread = new QThread();
45 moveToThread( thread );
46 thread->start();
47 QMetaObject::invokeMethod( this, "threadInit" );
48}
49
50SessionThread::~SessionThread()
51{
52 QMetaObject::invokeMethod( this, "threadQuit" );
53 if ( !thread()->wait( 10 * 1000 ) ) {
54 kWarning() << "Session thread refuses to die, killing harder...";
55 thread()->terminate();
56 // Make sure to wait until it's done, otherwise it can crash when the pthread callback is called
57 thread()->wait();
58 }
59 delete thread();
60}
61
62// Called in primary thread
63void SessionThread::sendData( const QByteArray &payload )
64{
65 QMutexLocker locker( &m_mutex );
66
67 m_dataQueue.enqueue( payload );
68 QMetaObject::invokeMethod( this, "writeDataQueue" );
69}
70
71// Called in secondary thread
72void SessionThread::writeDataQueue()
73{
74 Q_ASSERT( QThread::currentThread() == thread() );
75 if ( !m_socket )
76 return;
77 QMutexLocker locker( &m_mutex );
78
79 while ( !m_dataQueue.isEmpty() ) {
80 m_socket->write( m_dataQueue.dequeue() );
81 }
82}
83
84// Called in secondary thread
85void SessionThread::readMessage()
86{
87 Q_ASSERT( QThread::currentThread() == thread() );
88 if ( !m_stream || m_stream->availableDataSize() == 0 ) {
89 return;
90 }
91
92 Message message;
93 QList<Message::Part> *payload = &message.content;
94
95 try {
96 while ( !m_stream->atCommandEnd() ) {
97 if ( m_stream->hasString() ) {
98 QByteArray string = m_stream->readString();
99 if ( string == "NIL" ) {
100 *payload << Message::Part( QList<QByteArray>() );
101 } else {
102 *payload << Message::Part( string );
103 }
104 } else if ( m_stream->hasList() ) {
105 *payload << Message::Part( m_stream->readParenthesizedList() );
106 } else if ( m_stream->hasResponseCode() ) {
107 payload = &message.responseCode;
108 } else if ( m_stream->atResponseCodeEnd() ) {
109 payload = &message.content;
110 } else if ( m_stream->hasLiteral() ) {
111 QByteArray literal;
112 while ( !m_stream->atLiteralEnd() ) {
113 literal += m_stream->readLiteralPart();
114 }
115 *payload << Message::Part( literal );
116 } else {
117 // Oops! Something really bad happened, we won't be able to recover
118 // so close the socket immediately
119 qWarning( "Inconsistent state, probably due to some packet loss" );
120 doCloseSocket();
121 return;
122 }
123 }
124
125 emit responseReceived( message );
126
127 } catch ( KIMAP::ImapParserException e ) {
128 qWarning() << "The stream parser raised an exception:" << e.what();
129 }
130
131 if ( m_stream->availableDataSize() > 1 ) {
132 QMetaObject::invokeMethod( this, "readMessage", Qt::QueuedConnection );
133 }
134
135}
136
137// Called in main thread
138void SessionThread::closeSocket()
139{
140 QMetaObject::invokeMethod( this, "doCloseSocket", Qt::QueuedConnection );
141}
142
143// Called in secondary thread
144void SessionThread::doCloseSocket()
145{
146 Q_ASSERT( QThread::currentThread() == thread() );
147 if ( !m_socket )
148 return;
149 m_encryptedMode = false;
150 kDebug() << "close";
151 m_socket->close();
152}
153
154// Called in secondary thread
155void SessionThread::reconnect()
156{
157 Q_ASSERT( QThread::currentThread() == thread() );
158 if ( m_socket == 0 ) // threadQuit already called
159 return;
160 if ( m_socket->state() != SessionSocket::ConnectedState &&
161 m_socket->state() != SessionSocket::ConnectingState ) {
162 if ( m_encryptedMode ) {
163 kDebug() << "connectToHostEncrypted" << m_hostName << m_port;
164 m_socket->connectToHostEncrypted( m_hostName, m_port );
165 } else {
166 kDebug() << "connectToHost" << m_hostName << m_port;
167 m_socket->connectToHost( m_hostName, m_port );
168 }
169 }
170}
171
172// Called in secondary thread
173void SessionThread::threadInit()
174{
175 Q_ASSERT( QThread::currentThread() == thread() );
176 m_socket = new SessionSocket;
177 m_stream = new ImapStreamParser( m_socket );
178 connect( m_socket, SIGNAL(readyRead()),
179 this, SLOT(readMessage()), Qt::QueuedConnection );
180
181 // Delay the call to slotSocketDisconnected so that it finishes disconnecting before we call reconnect()
182 connect( m_socket, SIGNAL(disconnected()),
183 this, SLOT(slotSocketDisconnected()), Qt::QueuedConnection );
184 connect( m_socket, SIGNAL(connected()),
185 this, SIGNAL(socketConnected()) );
186 connect( m_socket, SIGNAL(error(KTcpSocket::Error)),
187 this, SLOT(slotSocketError(KTcpSocket::Error)) );
188 connect( m_socket, SIGNAL(bytesWritten(qint64)),
189 this, SIGNAL(socketActivity()) );
190 if ( m_socket->metaObject()->indexOfSignal( "encryptedBytesWritten(qint64)" ) > -1 ) {
191 connect( m_socket, SIGNAL(encryptedBytesWritten(qint64)), // needs kdelibs > 4.8
192 this, SIGNAL(socketActivity()) );
193 }
194 connect( m_socket, SIGNAL(readyRead()),
195 this, SIGNAL(socketActivity()) );
196
197 QMetaObject::invokeMethod(this, "reconnect", Qt::QueuedConnection);
198}
199
200// Called in secondary thread
201void SessionThread::threadQuit()
202{
203 Q_ASSERT( QThread::currentThread() == thread() );
204 delete m_stream;
205 m_stream = 0;
206 delete m_socket;
207 m_socket = 0;
208 thread()->quit();
209}
210
211// Called in primary thread
212void SessionThread::startSsl( KTcpSocket::SslVersion version )
213{
214 QMetaObject::invokeMethod( this, "doStartSsl", Q_ARG(KTcpSocket::SslVersion, version) );
215}
216
217// Called in secondary thread (via invokeMethod)
218void SessionThread::doStartSsl( KTcpSocket::SslVersion version )
219{
220 Q_ASSERT( QThread::currentThread() == thread() );
221 if ( !m_socket )
222 return;
223
224 m_socket->setAdvertisedSslVersion( version );
225 m_socket->ignoreSslErrors();
226 connect( m_socket, SIGNAL(encrypted()), this, SLOT(sslConnected()) );
227 m_socket->startClientEncryption();
228}
229
230// Called in secondary thread
231void SessionThread::slotSocketDisconnected()
232{
233 Q_ASSERT( QThread::currentThread() == thread() );
234 emit socketDisconnected();
235}
236
237// Called in secondary thread
238void SessionThread::slotSocketError(KTcpSocket::Error error)
239{
240 Q_ASSERT( QThread::currentThread() == thread() );
241 if ( !m_socket )
242 return;
243 Q_UNUSED( error ); // can be used for debugging
244 emit socketError(error);
245}
246
247// Called in secondary thread
248void SessionThread::sslConnected()
249{
250 Q_ASSERT( QThread::currentThread() == thread() );
251 if ( !m_socket )
252 return;
253 KSslCipher cipher = m_socket->sessionCipher();
254
255 if ( m_socket->sslErrors().count() > 0 ||
256 m_socket->encryptionMode() != KTcpSocket::SslClientMode ||
257 cipher.isNull() || cipher.usedBits() == 0 ) {
258 kDebug() << "Initial SSL handshake failed. cipher.isNull() is" << cipher.isNull()
259 << ", cipher.usedBits() is" << cipher.usedBits()
260 << ", the socket says:" << m_socket->errorString()
261 << "and the list of SSL errors contains"
262 << m_socket->sslErrors().count() << "items.";
263 KSslErrorUiData errorData( m_socket );
264 emit sslError( errorData );
265 } else {
266 kDebug() << "TLS negotiation done.";
267 m_encryptedMode = true;
268 emit encryptionNegotiationResult( true, m_socket->negotiatedSslVersion() );
269 }
270}
271
272void SessionThread::sslErrorHandlerResponse(bool response)
273{
274 QMetaObject::invokeMethod(this, "doSslErrorHandlerResponse", Q_ARG(bool, response));
275}
276
277// Called in secondary thread (via invokeMethod)
278void SessionThread::doSslErrorHandlerResponse(bool response)
279{
280 Q_ASSERT( QThread::currentThread() == thread() );
281 if ( !m_socket )
282 return;
283 if ( response ) {
284 m_encryptedMode = true;
285 emit encryptionNegotiationResult( true, m_socket->negotiatedSslVersion() );
286 } else {
287 m_encryptedMode = false;
288 //reconnect in unencrypted mode, so new commands can be issued
289 m_socket->disconnectFromHost();
290 m_socket->waitForDisconnected();
291 m_socket->connectToHost( m_hostName, m_port );
292 emit encryptionNegotiationResult( false, KTcpSocket::UnknownSslVersion );
293 }
294}
295
296#include "moc_sessionthread_p.cpp"
297