1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
2
3 This file is part of the Trojita Qt IMAP e-mail client,
4 http://trojita.flaska.net/
5
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License as
8 published by the Free Software Foundation; either version 2 of
9 the License or (at your option) version 3 or any later version
10 accepted by the membership of KDE e.V. (or its successor approved
11 by the membership of KDE e.V.), which shall act as a proxy
12 defined in Section 14 of version 3 of the license.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23 #include "OpenConnectionTask.h"
24 #include <QTimer>
25 #include "Common/ConnectionId.h"
26 #include "Common/InvokeMethod.h"
27 #include "Imap/Model/ItemRoles.h"
28 #include "Imap/Model/TaskPresentationModel.h"
29 #include "Imap/Tasks/EnableTask.h"
30 #include "Imap/Tasks/IdTask.h"
31 #include "Streams/SocketFactory.h"
32 #include "Streams/TrojitaZlibStatus.h"
33
34 namespace Imap
35 {
36 namespace Mailbox
37 {
38
OpenConnectionTask(Model * model)39 OpenConnectionTask::OpenConnectionTask(Model *model) :
40 ImapTask(model)
41 {
42 // Offline mode shall be checked by the caller who decides to create the connection
43 Q_ASSERT(model->networkPolicy() != NETWORK_OFFLINE);
44 parser = new Parser(model, model->m_socketFactory->create(), Common::ConnectionId::next());
45 ParserState parserState(parser);
46 connect(parser, &Parser::responseReceived, model, static_cast<void (Model::*)(Parser*)>(&Model::responseReceived), Qt::QueuedConnection);
47 connect(parser, &Parser::connectionStateChanged, model, &Model::handleSocketStateChanged);
48 connect(parser, &Parser::lineReceived, model, &Model::slotParserLineReceived);
49 connect(parser, &Parser::lineSent, model, &Model::slotParserLineSent);
50 model->m_parsers[ parser ] = parserState;
51 model->m_taskModel->slotParserCreated(parser);
52 markAsActiveTask();
53 }
54
OpenConnectionTask(Model * model,void * dummy)55 OpenConnectionTask::OpenConnectionTask(Model *model, void *dummy):
56 ImapTask(model)
57 {
58 Q_UNUSED(dummy);
59 }
60
debugIdentification() const61 QString OpenConnectionTask::debugIdentification() const
62 {
63 if (parser)
64 return QStringLiteral("OpenConnectionTask: %1").arg(Imap::connectionStateToString(model->accessParser(parser).connState));
65 else
66 return QStringLiteral("OpenConnectionTask: no parser");
67 }
68
perform()69 void OpenConnectionTask::perform()
70 {
71 // nothing should happen here
72 }
73
74 /** @short Decide what to do next based on the received response and the current state of the connection
75
76 CONN_STATE_NONE:
77 CONN_STATE_HOST_LOOKUP:
78 CONN_STATE_CONNECTING:
79 - not allowed
80
81 CONN_STATE_CONNECTED_PRETLS_PRECAPS:
82 -> CONN_STATE_AUTHENTICATED iff "* PREAUTH [CAPABILITIES ...]"
83 - done
84 -> CONN_STATE_POSTAUTH_PRECAPS iff "* PREAUTH"
85 - requesting capabilities
86 -> CONN_STATE_TLS if "* OK [CAPABILITIES ...]"
87 - calling STARTTLS
88 -> CONN_STATE_CONNECTED_PRETLS if caps not known
89 - asking for capabilities
90 -> CONN_STATE_LOGIN iff capabilities are provided and LOGINDISABLED is not there and configuration doesn't want STARTTLS
91 - trying to LOGIN.
92 -> CONN_STATE_LOGOUT if the initial greeting asks us to leave
93 - fail
94
95 CONN_STATE_CONNECTED_PRETLS: checks result of the capability command
96 -> CONN_STATE_STARTTLS
97 - calling STARTTLS
98 -> CONN_STATE_LOGIN
99 - calling login
100 -> fail
101
102 CONN_STATE_STARTTLS: checks result of STARTTLS command
103 -> CONN_STATE_ESTABLISHED_PRECAPS
104 - asking for capabilities
105 -> fail
106
107 CONN_STATE_ESTABLISHED_PRECAPS: checks for the result of capabilities
108 -> CONN_STATE_LOGIN
109 -> fail
110
111 CONN_STATE_POSTAUTH_PRECAPS: checks result of the capability command
112 */
handleStateHelper(const Imap::Responses::State * const resp)113 bool OpenConnectionTask::handleStateHelper(const Imap::Responses::State *const resp)
114 {
115 if (_dead) {
116 _failed(tr("Asked to die"));
117 return true;
118 }
119 using namespace Imap::Responses;
120
121 if (model->accessParser(parser).connState == CONN_STATE_CONNECTED_PRETLS_PRECAPS) {
122 if (!resp->tag.isEmpty()) {
123 throw Imap::UnexpectedResponseReceived("Waiting for initial OK/BYE/PREAUTH, but got tagged response instead", *resp);
124 }
125 } else if (model->accessParser(parser).connState > CONN_STATE_CONNECTED_PRETLS_PRECAPS) {
126 if (resp->tag.isEmpty()) {
127 return false;
128 }
129 }
130
131 switch (model->accessParser(parser).connState) {
132
133 case CONN_STATE_AUTHENTICATED:
134 case CONN_STATE_SELECTING_WAIT_FOR_CLOSE:
135 case CONN_STATE_SELECTING:
136 case CONN_STATE_SYNCING:
137 case CONN_STATE_SELECTED:
138 case CONN_STATE_FETCHING_PART:
139 case CONN_STATE_FETCHING_MSG_METADATA:
140 case CONN_STATE_LOGOUT:
141 {
142 QByteArray message = "No response expected by the OpenConnectionTask in state " +
143 Imap::connectionStateToString(model->accessParser(parser).connState).toUtf8();
144 // These shall not ever be reached by this code
145 throw Imap::UnexpectedResponseReceived(message.constData(), *resp);
146 }
147
148 case CONN_STATE_NONE:
149 case CONN_STATE_HOST_LOOKUP:
150 case CONN_STATE_CONNECTING:
151 // Looks like the corresponding stateChanged() signal could be delayed, at least with QProcess-based sockets
152 case CONN_STATE_CONNECTED_PRETLS_PRECAPS:
153 // We're connected now -- this is our initial state.
154 {
155 switch (resp->kind) {
156 case PREAUTH:
157 if (model->m_startTls) {
158 // Oops, we cannot send STARTTLS when the connection is already authenticated.
159 // This is serious enough to warrant an error; an attacker might be going after a plaintext
160 // of a message we're going to APPEND, etc.
161 // Thanks to Arnt Gulbrandsen on the imap-protocol ML for asking what happens when we're configured
162 // to request STARTTLS and a PREAUTH is received, and to Michael M Slusarz for starting that discussion.
163 abortConnection(tr("Configuration requires sending STARTTLS, but the IMAP server greets us with PREAUTH. "
164 "Encryption cannot be established. If this configuration worked previously, someone "
165 "is after your data and they are pretty smart."));
166 return true;
167 }
168 // Cool, we're already authenticated. Now, let's see if we have to issue CAPABILITY or if we already know that
169 if (model->accessParser(parser).capabilitiesFresh) {
170 // We're alsmost done here, apart from compression
171 if (TROJITA_COMPRESS_DEFLATE && model->accessParser(parser).capabilities.contains(QStringLiteral("COMPRESS=DEFLATE"))) {
172 compressCmd = parser->compressDeflate();
173 model->changeConnectionState(parser, CONN_STATE_COMPRESS_DEFLATE);
174 } else {
175 // really done
176 model->changeConnectionState(parser, CONN_STATE_AUTHENTICATED);
177 onComplete();
178 }
179 } else {
180 model->changeConnectionState(parser, CONN_STATE_POSTAUTH_PRECAPS);
181 capabilityCmd = parser->capability();
182 }
183 return true;
184
185 case OK:
186 if (!model->accessParser(parser).capabilitiesFresh) {
187 model->changeConnectionState(parser, CONN_STATE_CONNECTED_PRETLS);
188 capabilityCmd = parser->capability();
189 } else {
190 startTlsOrLoginNow();
191 }
192 return true;
193
194 case BYE:
195 abortConnection(tr("This server gracefully refuses IMAP connections through a BYE response."));
196 return true;
197
198 case BAD:
199 model->changeConnectionState(parser, CONN_STATE_LOGOUT);
200 // If it was an ALERT, we've already warned the user
201 if (resp->respCode != ALERT) {
202 emit model->alertReceived(tr("The server replied with the following BAD response:\n%1").arg(resp->message));
203 }
204 abortConnection(tr("Server has greeted us with a BAD response"));
205 return true;
206
207 default:
208 throw Imap::UnexpectedResponseReceived("Waiting for initial OK/BYE/BAD/PREAUTH, but got this instead", *resp);
209 }
210 break;
211 }
212
213 case CONN_STATE_CONNECTED_PRETLS:
214 // We've asked for capabilities upon the initial interaction
215 {
216 bool wasCaps = checkCapabilitiesResult(resp);
217 if (wasCaps && !_finished) {
218 startTlsOrLoginNow();
219 }
220 return wasCaps;
221 }
222
223 case CONN_STATE_STARTTLS_ISSUED:
224 {
225 if (resp->tag == startTlsCmd) {
226 if (resp->kind == OK) {
227 model->changeConnectionState(parser, CONN_STATE_STARTTLS_HANDSHAKE);
228 if (!model->m_startTls) {
229 // The model was not configured to perform STARTTLS, but we still did that for some reason.
230 // As suggested by Mike Cardwell on the trojita ML (http://article.gmane.org/gmane.mail.trojita.general/299),
231 // it makes sense to make this settings permanent, so that a user is not tricked into revealing their
232 // password when a MITM removes the LOGINDISABLED in future.
233 EMIT_LATER_NOARG(model, requireStartTlsInFuture);
234 }
235 } else {
236 abortConnection(tr("Cannot establish a secure connection.\nThe STARTTLS command failed: %1").arg(resp->message));
237 }
238 return true;
239 }
240 return false;
241 }
242
243 case CONN_STATE_SSL_HANDSHAKE:
244 case CONN_STATE_STARTTLS_HANDSHAKE:
245 // nothing should really arrive at this point; the Parser is expected to wait for encryption and only after that
246 // send the data
247 Q_ASSERT(false);
248 return false;
249
250 case CONN_STATE_STARTTLS_VERIFYING:
251 case CONN_STATE_SSL_VERIFYING:
252 {
253 // We're waiting for a decision based on a policy, so we do not really expect any network IO at this point
254 // FIXME: an assert(false) here?
255 qDebug() << "OpenConnectionTask: ignoring response, we're still waiting for SSL policy decision";
256 return false;
257 }
258
259 case CONN_STATE_ESTABLISHED_PRECAPS:
260 // Connection is established and we're waiting for updated capabilities
261 {
262 bool wasCaps = checkCapabilitiesResult(resp);
263 if (wasCaps && !_finished) {
264 if (model->accessParser(parser).capabilities.contains(QStringLiteral("LOGINDISABLED"))) {
265 abortConnection(tr("Server error: Capabilities contain LOGINDISABLED even after STARTTLS"));
266 } else {
267 model->changeConnectionState(parser, CONN_STATE_LOGIN);
268 askForAuth();
269 }
270 }
271 return wasCaps;
272 }
273
274 case CONN_STATE_LOGIN:
275 // Check the result of the LOGIN command
276 {
277 if (resp->tag == loginCmd) {
278 loginCmd.clear();
279 // The LOGIN command is finished
280 if (resp->kind == OK) {
281 model->setImapAuthError(QString());
282 if (resp->respCode == CAPABILITIES || model->accessParser(parser).capabilitiesFresh) {
283 // Capabilities are already known
284 if (TROJITA_COMPRESS_DEFLATE && model->accessParser(parser).capabilities.contains(QStringLiteral("COMPRESS=DEFLATE"))) {
285 compressCmd = parser->compressDeflate();
286 model->changeConnectionState(parser, CONN_STATE_COMPRESS_DEFLATE);
287 } else {
288 model->changeConnectionState(parser, CONN_STATE_AUTHENTICATED);
289 onComplete();
290 }
291 } else {
292 // Got to ask for the capabilities
293 model->changeConnectionState(parser, CONN_STATE_POSTAUTH_PRECAPS);
294 capabilityCmd = parser->capability();
295 }
296 } else {
297 // Login failed
298 QString message;
299 switch (resp->respCode) {
300 case Responses::UNAVAILABLE:
301 message = tr("Temporary failure because a subsystem is down.");
302 break;
303 case Responses::AUTHENTICATIONFAILED:
304 message = tr("Authentication failed. This often happens due to bad password or wrong user name.");
305 break;
306 case Responses::AUTHORIZATIONFAILED:
307 message = tr("Authentication succeeded in using the authentication identity, "
308 "but the server cannot or will not allow the authentication "
309 "identity to act as the requested authorization identity.");
310 break;
311 case Responses::EXPIRED:
312 message = tr("Either authentication succeeded or the server no longer had the "
313 "necessary data; either way, access is no longer permitted using "
314 "that passphrase. You should get a new passphrase.");
315 break;
316 case Responses::PRIVACYREQUIRED:
317 message = tr("The operation is not permitted due to a lack of privacy.");
318 break;
319 case Responses::CONTACTADMIN:
320 message = tr("You should contact the system administrator or support desk.");
321 break;
322 default:
323 break;
324 }
325
326 if (message.isEmpty()) {
327 message = tr("Login failed: %1").arg(resp->message);
328 } else {
329 message = tr("%1 %2").arg(message, resp->message);
330 }
331
332 model->setImapAuthError(message);
333 EMIT_LATER(model, authAttemptFailed, Q_ARG(QString, message));
334
335 model->m_imapPassword.clear();
336 model->m_hasImapPassword = Model::PasswordAvailability::NOT_REQUESTED;
337 if (model->accessParser(parser).connState == CONN_STATE_LOGOUT) {
338 // The server has closed the conenction
339 _failed(QStringLiteral("Connection closed after a failed login"));
340 return true;
341 }
342 askForAuth();
343 }
344 return true;
345 }
346 return false;
347 }
348
349 case CONN_STATE_POSTAUTH_PRECAPS:
350 {
351 bool wasCaps = checkCapabilitiesResult(resp);
352 if (wasCaps && !_finished) {
353 model->changeConnectionState(parser, CONN_STATE_AUTHENTICATED);
354 onComplete();
355 }
356 return wasCaps;
357 }
358
359 case CONN_STATE_COMPRESS_DEFLATE:
360 if (resp->tag == compressCmd) {
361 model->changeConnectionState(parser, CONN_STATE_AUTHENTICATED);
362 onComplete();
363 return true;
364 } else {
365 return false;
366 }
367 break;
368
369 }
370
371 // Required catch-all for OpenSuSE's build service (Tumbleweed, 2012-04-03)
372 Q_ASSERT(false);
373 return false;
374 }
375
376 /** @short Either call STARTTLS or go ahead and try to LOGIN */
startTlsOrLoginNow()377 void OpenConnectionTask::startTlsOrLoginNow()
378 {
379 if (model->m_startTls || model->accessParser(parser).capabilities.contains(QStringLiteral("LOGINDISABLED"))) {
380 // Should run STARTTLS later and already have the capabilities
381 Q_ASSERT(model->accessParser(parser).capabilitiesFresh);
382 if (!model->accessParser(parser).capabilities.contains(QStringLiteral("STARTTLS"))) {
383 abortConnection(tr("Server error: LOGINDISABLED but no STARTTLS capability. The login is effectively disabled entirely."));
384 } else {
385 startTlsCmd = parser->startTls();
386 model->changeConnectionState(parser, CONN_STATE_STARTTLS_ISSUED);
387 }
388 } else {
389 // We're requested to authenticate even without STARTTLS
390 Q_ASSERT(!model->accessParser(parser).capabilities.contains(QLatin1String("LOGINDISABLED")));
391 model->changeConnectionState(parser, CONN_STATE_LOGIN);
392 askForAuth();
393 }
394 }
395
checkCapabilitiesResult(const Responses::State * const resp)396 bool OpenConnectionTask::checkCapabilitiesResult(const Responses::State *const resp)
397 {
398 if (resp->tag.isEmpty())
399 return false;
400
401 if (resp->tag == capabilityCmd) {
402 if (!model->accessParser(parser).capabilitiesFresh) {
403 abortConnection(tr("Server error: did not get the required CAPABILITY response."));
404 return true;
405 }
406 if (resp->kind != Responses::OK) {
407 abortConnection(tr("Server error: The CAPABILITY request failed."));
408 }
409 return true;
410 }
411
412 return false;
413 }
414
onComplete()415 void OpenConnectionTask::onComplete()
416 {
417 // Optionally issue the ID command
418 if (model->accessParser(parser).capabilities.contains(QStringLiteral("ID"))) {
419 Imap::Mailbox::ImapTask *task = model->m_taskFactory->createIdTask(model, this);
420 task->perform();
421 }
422 // Optionally enable extensions which need enabling
423 if (model->accessParser(parser).capabilities.contains(QStringLiteral("ENABLE"))) {
424 QList<QByteArray> extensions;
425
426 if (model->accessParser(parser).capabilities.contains(QStringLiteral("QRESYNC"))) {
427 extensions << "QRESYNC";
428 }
429
430 if (!extensions.isEmpty()) {
431 model->m_taskFactory->createEnableTask(model, this, extensions)->perform();
432 }
433 }
434
435 // But do terminate this task
436 _completed();
437 }
438
abortConnection(const QString & message)439 void OpenConnectionTask::abortConnection(const QString &message)
440 {
441 _failed(message);
442 EMIT_LATER(model, authAttemptFailed, Q_ARG(QString, message));
443 model->setNetworkPolicy(NETWORK_OFFLINE);
444 }
445
askForAuth()446 void OpenConnectionTask::askForAuth()
447 {
448 switch(model->m_hasImapPassword) {
449 case Model::PasswordAvailability::NOT_REQUESTED:
450 model->m_hasImapPassword = Model::PasswordAvailability::ASKED_WAITING;
451 EMIT_LATER_NOARG(model, authRequested);
452 break;
453 case Model::PasswordAvailability::ASKED_WAITING:
454 // do nothing, it has been already requested by the GUI
455 model->logTrace(parser->parserId(), Common::LOG_OTHER, QLatin1String("imap.password"),
456 QLatin1String("Password already requested, will wait"));
457 break;
458 case Model::PasswordAvailability::AVAILABLE:
459 Q_ASSERT(loginCmd.isEmpty());
460 loginCmd = parser->login(model->m_imapUser, model->m_imapPassword);
461 model->accessParser(parser).capabilitiesFresh = false;
462 break;
463 }
464 }
465
authCredentialsNowAvailable()466 void OpenConnectionTask::authCredentialsNowAvailable()
467 {
468 if (model->accessParser(parser).connState == CONN_STATE_LOGIN && loginCmd.isEmpty()) {
469 switch (model->m_hasImapPassword) {
470 case Model::PasswordAvailability::NOT_REQUESTED:
471 case Model::PasswordAvailability::ASKED_WAITING:
472 abortConnection(tr("Cannot login, you have not provided any credentials yet."));
473 break;
474 case Model::PasswordAvailability::AVAILABLE:
475 loginCmd = parser->login(model->m_imapUser, model->m_imapPassword);
476 model->accessParser(parser).capabilitiesFresh = false;
477 break;
478 }
479 }
480 }
481
taskData(const int role) const482 QVariant OpenConnectionTask::taskData(const int role) const
483 {
484 return role == RoleTaskCompactName ? QVariant(tr("Connecting to mail server")) : QVariant();
485 }
486
sslCertificateChain() const487 QList<QSslCertificate> OpenConnectionTask::sslCertificateChain() const
488 {
489 return m_sslChain;
490 }
491
sslErrors() const492 QList<QSslError> OpenConnectionTask::sslErrors() const
493 {
494 return m_sslErrors;
495 }
496
sslConnectionPolicyDecided(bool ok)497 void OpenConnectionTask::sslConnectionPolicyDecided(bool ok)
498 {
499 switch (model->accessParser(parser).connState) {
500 case CONN_STATE_SSL_VERIFYING:
501 if (ok) {
502 model->changeConnectionState(parser, CONN_STATE_CONNECTED_PRETLS_PRECAPS);
503 } else {
504 abortConnection(tr("The security state of the SSL connection got rejected"));
505 }
506 break;
507 case CONN_STATE_STARTTLS_VERIFYING:
508 if (ok) {
509 model->changeConnectionState(parser, CONN_STATE_ESTABLISHED_PRECAPS);
510 model->accessParser(parser).capabilitiesFresh = false;
511 capabilityCmd = parser->capability();
512 } else {
513 abortConnection(tr("The security state of the connection after a STARTTLS operation got rejected"));
514 }
515 break;
516 default:
517 Q_ASSERT(false);
518 }
519 parser->unfreezeAfterEncryption();
520 }
521
handleSocketEncryptedResponse(const Responses::SocketEncryptedResponse * const resp)522 bool OpenConnectionTask::handleSocketEncryptedResponse(const Responses::SocketEncryptedResponse *const resp)
523 {
524 switch (model->accessParser(parser).connState) {
525 case CONN_STATE_SSL_HANDSHAKE:
526 model->changeConnectionState(parser, CONN_STATE_SSL_VERIFYING);
527 m_sslChain = resp->sslChain;
528 m_sslErrors = resp->sslErrors;
529 model->processSslErrors(this);
530 return true;
531 case CONN_STATE_STARTTLS_HANDSHAKE:
532 model->changeConnectionState(parser, CONN_STATE_STARTTLS_VERIFYING);
533 m_sslChain = resp->sslChain;
534 m_sslErrors = resp->sslErrors;
535 model->processSslErrors(this);
536 return true;
537 case CONN_STATE_LOGOUT:
538 return true;
539 default:
540 return false;
541 }
542 }
543
544 }
545 }
546