File indexing completed on 2024-06-23 05:21:15

0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
0002 
0003    This file is part of the Trojita Qt IMAP e-mail client,
0004    http://trojita.flaska.net/
0005 
0006    This program is free software; you can redistribute it and/or
0007    modify it under the terms of the GNU General Public License as
0008    published by the Free Software Foundation; either version 2 of
0009    the License or (at your option) version 3 or any later version
0010    accepted by the membership of KDE e.V. (or its successor approved
0011    by the membership of KDE e.V.), which shall act as a proxy
0012    defined in Section 14 of version 3 of the license.
0013 
0014    This program is distributed in the hope that it will be useful,
0015    but WITHOUT ANY WARRANTY; without even the implied warranty of
0016    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0017    GNU General Public License for more details.
0018 
0019    You should have received a copy of the GNU General Public License
0020    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0021 */
0022 
0023 #include "OpenConnectionTask.h"
0024 #include <QTimer>
0025 #include "Common/ConnectionId.h"
0026 #include "Common/InvokeMethod.h"
0027 #include "Imap/Model/ItemRoles.h"
0028 #include "Imap/Model/TaskPresentationModel.h"
0029 #include "Imap/Tasks/EnableTask.h"
0030 #include "Imap/Tasks/IdTask.h"
0031 #include "Streams/SocketFactory.h"
0032 #include "Streams/TrojitaZlibStatus.h"
0033 
0034 namespace Imap
0035 {
0036 namespace Mailbox
0037 {
0038 
0039 OpenConnectionTask::OpenConnectionTask(Model *model) :
0040     ImapTask(model)
0041 {
0042     // Offline mode shall be checked by the caller who decides to create the connection
0043     Q_ASSERT(model->networkPolicy() != NETWORK_OFFLINE);
0044     parser = new Parser(model, model->m_socketFactory->create(), Common::ConnectionId::next());
0045     ParserState parserState(parser);
0046     connect(parser, &Parser::responseReceived, model, static_cast<void (Model::*)(Parser*)>(&Model::responseReceived), Qt::QueuedConnection);
0047     connect(parser, &Parser::connectionStateChanged, model, &Model::handleSocketStateChanged);
0048     connect(parser, &Parser::lineReceived, model, &Model::slotParserLineReceived);
0049     connect(parser, &Parser::lineSent, model, &Model::slotParserLineSent);
0050     model->m_parsers[ parser ] = parserState;
0051     model->m_taskModel->slotParserCreated(parser);
0052     markAsActiveTask();
0053 }
0054 
0055 OpenConnectionTask::OpenConnectionTask(Model *model, void *dummy):
0056     ImapTask(model)
0057 {
0058     Q_UNUSED(dummy);
0059 }
0060 
0061 QString OpenConnectionTask::debugIdentification() const
0062 {
0063     if (parser)
0064         return QStringLiteral("OpenConnectionTask: %1").arg(Imap::connectionStateToString(model->accessParser(parser).connState));
0065     else
0066         return QStringLiteral("OpenConnectionTask: no parser");
0067 }
0068 
0069 void OpenConnectionTask::perform()
0070 {
0071     // nothing should happen here
0072 }
0073 
0074 /** @short Decide what to do next based on the received response and the current state of the connection
0075 
0076 CONN_STATE_NONE:
0077 CONN_STATE_HOST_LOOKUP:
0078 CONN_STATE_CONNECTING:
0079  - not allowed
0080 
0081 CONN_STATE_CONNECTED_PRETLS_PRECAPS:
0082  -> CONN_STATE_AUTHENTICATED iff "* PREAUTH [CAPABILITIES ...]"
0083     - done
0084  -> CONN_STATE_POSTAUTH_PRECAPS iff "* PREAUTH"
0085     - requesting capabilities
0086  -> CONN_STATE_TLS if "* OK [CAPABILITIES ...]"
0087     - calling STARTTLS
0088  -> CONN_STATE_CONNECTED_PRETLS if caps not known
0089     - asking for capabilities
0090  -> CONN_STATE_LOGIN iff capabilities are provided and LOGINDISABLED is not there and configuration doesn't want STARTTLS
0091     - trying to LOGIN.
0092  -> CONN_STATE_LOGOUT if the initial greeting asks us to leave
0093     - fail
0094 
0095 CONN_STATE_CONNECTED_PRETLS: checks result of the capability command
0096  -> CONN_STATE_STARTTLS
0097     - calling STARTTLS
0098  -> CONN_STATE_LOGIN
0099     - calling login
0100  -> fail
0101 
0102 CONN_STATE_STARTTLS: checks result of STARTTLS command
0103  -> CONN_STATE_ESTABLISHED_PRECAPS
0104     - asking for capabilities
0105  -> fail
0106 
0107 CONN_STATE_ESTABLISHED_PRECAPS: checks for the result of capabilities
0108  -> CONN_STATE_LOGIN
0109  -> fail
0110 
0111 CONN_STATE_POSTAUTH_PRECAPS: checks result of the capability command
0112 */
0113 bool OpenConnectionTask::handleStateHelper(const Imap::Responses::State *const resp)
0114 {
0115     if (_dead) {
0116         _failed(tr("Asked to die"));
0117         return true;
0118     }
0119     using namespace Imap::Responses;
0120 
0121     if (model->accessParser(parser).connState == CONN_STATE_CONNECTED_PRETLS_PRECAPS) {
0122         if (!resp->tag.isEmpty()) {
0123             throw Imap::UnexpectedResponseReceived("Waiting for initial OK/BYE/PREAUTH, but got tagged response instead", *resp);
0124         }
0125     } else if (model->accessParser(parser).connState > CONN_STATE_CONNECTED_PRETLS_PRECAPS) {
0126         if (resp->tag.isEmpty()) {
0127             return false;
0128         }
0129     }
0130 
0131     switch (model->accessParser(parser).connState) {
0132 
0133     case CONN_STATE_AUTHENTICATED:
0134     case CONN_STATE_SELECTING_WAIT_FOR_CLOSE:
0135     case CONN_STATE_SELECTING:
0136     case CONN_STATE_SYNCING:
0137     case CONN_STATE_SELECTED:
0138     case CONN_STATE_FETCHING_PART:
0139     case CONN_STATE_FETCHING_MSG_METADATA:
0140     case CONN_STATE_LOGOUT:
0141     {
0142         QByteArray message = "No response expected by the OpenConnectionTask in state " +
0143                 Imap::connectionStateToString(model->accessParser(parser).connState).toUtf8();
0144         // These shall not ever be reached by this code
0145         throw Imap::UnexpectedResponseReceived(message.constData(), *resp);
0146     }
0147 
0148     case CONN_STATE_NONE:
0149     case CONN_STATE_HOST_LOOKUP:
0150     case CONN_STATE_CONNECTING:
0151         // Looks like the corresponding stateChanged() signal could be delayed, at least with QProcess-based sockets
0152     case CONN_STATE_CONNECTED_PRETLS_PRECAPS:
0153         // We're connected now -- this is our initial state.
0154     {
0155         switch (resp->kind) {
0156         case PREAUTH:
0157             if (model->m_startTls) {
0158                 // Oops, we cannot send STARTTLS when the connection is already authenticated.
0159                 // This is serious enough to warrant an error; an attacker might be going after a plaintext
0160                 // of a message we're going to APPEND, etc.
0161                 // Thanks to Arnt Gulbrandsen on the imap-protocol ML for asking what happens when we're configured
0162                 // to request STARTTLS and a PREAUTH is received, and to Michael M Slusarz for starting that discussion.
0163                 abortConnection(tr("Configuration requires sending STARTTLS, but the IMAP server greets us with PREAUTH. "
0164                                    "Encryption cannot be established. If this configuration worked previously, someone "
0165                                    "is after your data and they are pretty smart."));
0166                 return true;
0167             }
0168             // Cool, we're already authenticated. Now, let's see if we have to issue CAPABILITY or if we already know that
0169             if (model->accessParser(parser).capabilitiesFresh) {
0170                 // We're alsmost done here, apart from compression
0171                 if (TROJITA_COMPRESS_DEFLATE && model->accessParser(parser).capabilities.contains(QStringLiteral("COMPRESS=DEFLATE"))) {
0172                     compressCmd = parser->compressDeflate();
0173                     model->changeConnectionState(parser, CONN_STATE_COMPRESS_DEFLATE);
0174                 } else {
0175                     // really done
0176                     model->changeConnectionState(parser, CONN_STATE_AUTHENTICATED);
0177                     onComplete();
0178                 }
0179             } else {
0180                 model->changeConnectionState(parser, CONN_STATE_POSTAUTH_PRECAPS);
0181                 capabilityCmd = parser->capability();
0182             }
0183             return true;
0184 
0185         case OK:
0186             if (!model->accessParser(parser).capabilitiesFresh) {
0187                 model->changeConnectionState(parser, CONN_STATE_CONNECTED_PRETLS);
0188                 capabilityCmd = parser->capability();
0189             } else {
0190                 startTlsOrLoginNow();
0191             }
0192             return true;
0193 
0194         case BYE:
0195             abortConnection(tr("This server gracefully refuses IMAP connections through a BYE response."));
0196             return true;
0197 
0198         case BAD:
0199             model->changeConnectionState(parser, CONN_STATE_LOGOUT);
0200             // If it was an ALERT, we've already warned the user
0201             if (resp->respCode != ALERT) {
0202                 emit model->alertReceived(tr("The server replied with the following BAD response:\n%1").arg(resp->message));
0203             }
0204             abortConnection(tr("Server has greeted us with a BAD response"));
0205             return true;
0206 
0207         default:
0208             throw Imap::UnexpectedResponseReceived("Waiting for initial OK/BYE/BAD/PREAUTH, but got this instead", *resp);
0209         }
0210         break;
0211     }
0212 
0213     case CONN_STATE_CONNECTED_PRETLS:
0214         // We've asked for capabilities upon the initial interaction
0215     {
0216         bool wasCaps = checkCapabilitiesResult(resp);
0217         if (wasCaps && !_finished) {
0218             startTlsOrLoginNow();
0219         }
0220         return wasCaps;
0221     }
0222 
0223     case CONN_STATE_STARTTLS_ISSUED:
0224     {
0225         if (resp->tag == startTlsCmd) {
0226             if (resp->kind == OK) {
0227                 model->changeConnectionState(parser, CONN_STATE_STARTTLS_HANDSHAKE);
0228                 if (!model->m_startTls) {
0229                     // The model was not configured to perform STARTTLS, but we still did that for some reason.
0230                     // As suggested by Mike Cardwell on the trojita ML (http://article.gmane.org/gmane.mail.trojita.general/299),
0231                     // it makes sense to make this settings permanent, so that a user is not tricked into revealing their
0232                     // password when a MITM removes the LOGINDISABLED in future.
0233                     EMIT_LATER_NOARG(model, requireStartTlsInFuture);
0234                 }
0235             } else {
0236                 abortConnection(tr("Cannot establish a secure connection.\nThe STARTTLS command failed: %1").arg(resp->message));
0237             }
0238             return true;
0239         }
0240         return false;
0241     }
0242 
0243     case CONN_STATE_SSL_HANDSHAKE:
0244     case CONN_STATE_STARTTLS_HANDSHAKE:
0245         // nothing should really arrive at this point; the Parser is expected to wait for encryption and only after that
0246         // send the data
0247         Q_ASSERT(false);
0248         return false;
0249 
0250     case CONN_STATE_STARTTLS_VERIFYING:
0251     case CONN_STATE_SSL_VERIFYING:
0252     {
0253         // We're waiting for a decision based on a policy, so we do not really expect any network IO at this point
0254         // FIXME: an assert(false) here?
0255         qDebug() << "OpenConnectionTask: ignoring response, we're still waiting for SSL policy decision";
0256         return false;
0257     }
0258 
0259     case CONN_STATE_ESTABLISHED_PRECAPS:
0260         // Connection is established and we're waiting for updated capabilities
0261     {
0262         bool wasCaps = checkCapabilitiesResult(resp);
0263         if (wasCaps && !_finished) {
0264             if (model->accessParser(parser).capabilities.contains(QStringLiteral("LOGINDISABLED"))) {
0265                 abortConnection(tr("Server error: Capabilities contain LOGINDISABLED even after STARTTLS"));
0266             } else {
0267                 model->changeConnectionState(parser, CONN_STATE_LOGIN);
0268                 askForAuth();
0269             }
0270         }
0271         return wasCaps;
0272     }
0273 
0274     case CONN_STATE_LOGIN:
0275         // Check the result of the LOGIN command
0276     {
0277         if (resp->tag == loginCmd) {
0278             loginCmd.clear();
0279             // The LOGIN command is finished
0280             if (resp->kind == OK) {
0281                 model->setImapAuthError(QString());
0282                 if (resp->respCode == CAPABILITIES || model->accessParser(parser).capabilitiesFresh) {
0283                     // Capabilities are already known
0284                     if (TROJITA_COMPRESS_DEFLATE && model->accessParser(parser).capabilities.contains(QStringLiteral("COMPRESS=DEFLATE"))) {
0285                         compressCmd = parser->compressDeflate();
0286                         model->changeConnectionState(parser, CONN_STATE_COMPRESS_DEFLATE);
0287                     } else {
0288                         model->changeConnectionState(parser, CONN_STATE_AUTHENTICATED);
0289                         onComplete();
0290                     }
0291                 } else {
0292                     // Got to ask for the capabilities
0293                     model->changeConnectionState(parser, CONN_STATE_POSTAUTH_PRECAPS);
0294                     capabilityCmd = parser->capability();
0295                 }
0296             } else {
0297                 // Login failed
0298                 QString message;
0299                 switch (resp->respCode) {
0300                 case Responses::UNAVAILABLE:
0301                     message = tr("Temporary failure because a subsystem is down.");
0302                     break;
0303                 case Responses::AUTHENTICATIONFAILED:
0304                     message = tr("Authentication failed.  This often happens due to bad password or wrong user name.");
0305                     break;
0306                 case Responses::AUTHORIZATIONFAILED:
0307                     message = tr("Authentication succeeded in using the authentication identity, "
0308                                  "but the server cannot or will not allow the authentication "
0309                                  "identity to act as the requested authorization identity.");
0310                     break;
0311                 case Responses::EXPIRED:
0312                     message = tr("Either authentication succeeded or the server no longer had the "
0313                                  "necessary data; either way, access is no longer permitted using "
0314                                  "that passphrase.  You should get a new passphrase.");
0315                     break;
0316                 case Responses::PRIVACYREQUIRED:
0317                     message = tr("The operation is not permitted due to a lack of privacy.");
0318                     break;
0319                 case Responses::CONTACTADMIN:
0320                     message = tr("You should contact the system administrator or support desk.");
0321                     break;
0322                 default:
0323                     break;
0324                 }
0325 
0326                 if (message.isEmpty()) {
0327                     message = tr("Login failed: %1").arg(resp->message);
0328                 } else {
0329                     message = tr("%1 %2").arg(message, resp->message);
0330                 }
0331 
0332                 model->setImapAuthError(message);
0333                 EMIT_LATER(model, authAttemptFailed, Q_ARG(QString, message));
0334 
0335                 model->m_imapPassword.clear();
0336                 model->m_hasImapPassword = Model::PasswordAvailability::NOT_REQUESTED;
0337                 if (model->accessParser(parser).connState == CONN_STATE_LOGOUT) {
0338                     // The server has closed the conenction
0339                     _failed(QStringLiteral("Connection closed after a failed login"));
0340                     return true;
0341                 }
0342                 askForAuth();
0343             }
0344             return true;
0345         }
0346         return false;
0347     }
0348 
0349     case CONN_STATE_POSTAUTH_PRECAPS:
0350     {
0351         bool wasCaps = checkCapabilitiesResult(resp);
0352         if (wasCaps && !_finished) {
0353             model->changeConnectionState(parser, CONN_STATE_AUTHENTICATED);
0354             onComplete();
0355         }
0356         return wasCaps;
0357     }
0358 
0359     case CONN_STATE_COMPRESS_DEFLATE:
0360         if (resp->tag == compressCmd) {
0361             model->changeConnectionState(parser, CONN_STATE_AUTHENTICATED);
0362             onComplete();
0363             return true;
0364         } else {
0365             return false;
0366         }
0367         break;
0368 
0369     }
0370 
0371     // Required catch-all for OpenSuSE's build service (Tumbleweed, 2012-04-03)
0372     Q_ASSERT(false);
0373     return false;
0374 }
0375 
0376 /** @short Either call STARTTLS or go ahead and try to LOGIN */
0377 void OpenConnectionTask::startTlsOrLoginNow()
0378 {
0379     if (model->m_startTls || model->accessParser(parser).capabilities.contains(QStringLiteral("LOGINDISABLED"))) {
0380         // Should run STARTTLS later and already have the capabilities
0381         Q_ASSERT(model->accessParser(parser).capabilitiesFresh);
0382         if (!model->accessParser(parser).capabilities.contains(QStringLiteral("STARTTLS"))) {
0383             abortConnection(tr("Server error: LOGINDISABLED but no STARTTLS capability. The login is effectively disabled entirely."));
0384         } else {
0385             startTlsCmd = parser->startTls();
0386             model->changeConnectionState(parser, CONN_STATE_STARTTLS_ISSUED);
0387         }
0388     } else {
0389         // We're requested to authenticate even without STARTTLS
0390         Q_ASSERT(!model->accessParser(parser).capabilities.contains(QLatin1String("LOGINDISABLED")));
0391         model->changeConnectionState(parser, CONN_STATE_LOGIN);
0392         askForAuth();
0393     }
0394 }
0395 
0396 bool OpenConnectionTask::checkCapabilitiesResult(const Responses::State *const resp)
0397 {
0398     if (resp->tag.isEmpty())
0399         return false;
0400 
0401     if (resp->tag == capabilityCmd) {
0402         if (!model->accessParser(parser).capabilitiesFresh) {
0403             abortConnection(tr("Server error: did not get the required CAPABILITY response."));
0404             return true;
0405         }
0406         if (resp->kind != Responses::OK) {
0407             abortConnection(tr("Server error: The CAPABILITY request failed."));
0408         }
0409         return true;
0410     }
0411 
0412     return false;
0413 }
0414 
0415 void OpenConnectionTask::onComplete()
0416 {
0417     // Optionally issue the ID command
0418     if (model->accessParser(parser).capabilities.contains(QStringLiteral("ID"))) {
0419         Imap::Mailbox::ImapTask *task = model->m_taskFactory->createIdTask(model, this);
0420         task->perform();
0421     }
0422     // Optionally enable extensions which need enabling
0423     if (model->accessParser(parser).capabilities.contains(QStringLiteral("ENABLE"))) {
0424         QList<QByteArray> extensions;
0425 
0426         if (model->accessParser(parser).capabilities.contains(QStringLiteral("QRESYNC"))) {
0427             extensions << "QRESYNC";
0428         }
0429 
0430         if (!extensions.isEmpty()) {
0431             model->m_taskFactory->createEnableTask(model, this, extensions)->perform();
0432         }
0433     }
0434 
0435     // But do terminate this task
0436     _completed();
0437 }
0438 
0439 void OpenConnectionTask::abortConnection(const QString &message)
0440 {
0441     _failed(message);
0442     EMIT_LATER(model, authAttemptFailed, Q_ARG(QString, message));
0443     model->setNetworkPolicy(NETWORK_OFFLINE);
0444 }
0445 
0446 void OpenConnectionTask::askForAuth()
0447 {
0448     switch(model->m_hasImapPassword) {
0449     case Model::PasswordAvailability::NOT_REQUESTED:
0450         model->m_hasImapPassword = Model::PasswordAvailability::ASKED_WAITING;
0451         EMIT_LATER_NOARG(model, authRequested);
0452         break;
0453     case Model::PasswordAvailability::ASKED_WAITING:
0454         // do nothing, it has been already requested by the GUI
0455         model->logTrace(parser->parserId(), Common::LOG_OTHER, QLatin1String("imap.password"),
0456                         QLatin1String("Password already requested, will wait"));
0457         break;
0458     case Model::PasswordAvailability::AVAILABLE:
0459         Q_ASSERT(loginCmd.isEmpty());
0460         loginCmd = parser->login(model->m_imapUser, model->m_imapPassword);
0461         model->accessParser(parser).capabilitiesFresh = false;
0462         break;
0463     }
0464 }
0465 
0466 void OpenConnectionTask::authCredentialsNowAvailable()
0467 {
0468     if (model->accessParser(parser).connState == CONN_STATE_LOGIN && loginCmd.isEmpty()) {
0469         switch (model->m_hasImapPassword) {
0470         case Model::PasswordAvailability::NOT_REQUESTED:
0471         case Model::PasswordAvailability::ASKED_WAITING:
0472             abortConnection(tr("Cannot login, you have not provided any credentials yet."));
0473             break;
0474         case Model::PasswordAvailability::AVAILABLE:
0475             loginCmd = parser->login(model->m_imapUser, model->m_imapPassword);
0476             model->accessParser(parser).capabilitiesFresh = false;
0477             break;
0478         }
0479     }
0480 }
0481 
0482 QVariant OpenConnectionTask::taskData(const int role) const
0483 {
0484     return role == RoleTaskCompactName ? QVariant(tr("Connecting to mail server")) : QVariant();
0485 }
0486 
0487 QList<QSslCertificate> OpenConnectionTask::sslCertificateChain() const
0488 {
0489     return m_sslChain;
0490 }
0491 
0492 QList<QSslError> OpenConnectionTask::sslErrors() const
0493 {
0494     return m_sslErrors;
0495 }
0496 
0497 void OpenConnectionTask::sslConnectionPolicyDecided(bool ok)
0498 {
0499     switch (model->accessParser(parser).connState) {
0500     case CONN_STATE_SSL_VERIFYING:
0501         if (ok) {
0502             model->changeConnectionState(parser, CONN_STATE_CONNECTED_PRETLS_PRECAPS);
0503         } else {
0504             abortConnection(tr("The security state of the SSL connection got rejected"));
0505         }
0506         break;
0507     case CONN_STATE_STARTTLS_VERIFYING:
0508         if (ok) {
0509             model->changeConnectionState(parser, CONN_STATE_ESTABLISHED_PRECAPS);
0510             model->accessParser(parser).capabilitiesFresh = false;
0511             capabilityCmd = parser->capability();
0512         } else {
0513             abortConnection(tr("The security state of the connection after a STARTTLS operation got rejected"));
0514         }
0515         break;
0516     default:
0517         Q_ASSERT(false);
0518     }
0519     parser->unfreezeAfterEncryption();
0520 }
0521 
0522 bool OpenConnectionTask::handleSocketEncryptedResponse(const Responses::SocketEncryptedResponse *const resp)
0523 {
0524     switch (model->accessParser(parser).connState) {
0525     case CONN_STATE_SSL_HANDSHAKE:
0526         model->changeConnectionState(parser, CONN_STATE_SSL_VERIFYING);
0527         m_sslChain = resp->sslChain;
0528         m_sslErrors = resp->sslErrors;
0529         model->processSslErrors(this);
0530         return true;
0531     case CONN_STATE_STARTTLS_HANDSHAKE:
0532         model->changeConnectionState(parser, CONN_STATE_STARTTLS_VERIFYING);
0533         m_sslChain = resp->sslChain;
0534         m_sslErrors = resp->sslErrors;
0535         model->processSslErrors(this);
0536         return true;
0537     case CONN_STATE_LOGOUT:
0538         return true;
0539     default:
0540         return false;
0541     }
0542 }
0543 
0544 }
0545 }