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 }