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