File indexing completed on 2024-09-22 04:47:56
0001 /* 0002 SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org> 0003 0004 Based on KMail code by: 0005 SPDX-FileCopyrightText: 1996-1998 Stefan Taferner <taferner@kde.org> 0006 0007 SPDX-License-Identifier: LGPL-2.0-or-later 0008 */ 0009 0010 #include "smtpjob.h" 0011 #include "mailtransport_defs.h" 0012 #include "mailtransportplugin_smtp_debug.h" 0013 #include "precommandjob.h" 0014 #include "sessionuiproxy.h" 0015 #include "transport.h" 0016 #include <KAuthorized> 0017 #include <QHash> 0018 #include <QPointer> 0019 0020 #include "mailtransport_debug.h" 0021 #include <KLocalizedString> 0022 #include <KPasswordDialog> 0023 0024 #include <KSMTP/LoginJob> 0025 #include <KSMTP/SendJob> 0026 0027 #include <KGAPI/Account> 0028 #include <KGAPI/AccountManager> 0029 #include <KGAPI/AuthJob> 0030 0031 #define GOOGLE_API_KEY QStringLiteral("554041944266.apps.googleusercontent.com") 0032 #define GOOGLE_API_SECRET QStringLiteral("mdT1DjzohxN3npUUzkENT0gO") 0033 0034 using namespace MailTransport; 0035 0036 class SessionPool 0037 { 0038 public: 0039 int ref = 0; 0040 QHash<int, KSmtp::Session *> sessions; 0041 0042 void removeSession(KSmtp::Session *session) 0043 { 0044 qCDebug(MAILTRANSPORT_SMTP_LOG) << "Removing session" << session << "from the pool"; 0045 int key = sessions.key(session); 0046 if (key > 0) { 0047 QObject::connect(session, &KSmtp::Session::stateChanged, session, [session](KSmtp::Session::State state) { 0048 if (state == KSmtp::Session::Disconnected) { 0049 session->deleteLater(); 0050 } 0051 }); 0052 session->quit(); 0053 sessions.remove(key); 0054 } 0055 } 0056 }; 0057 0058 Q_GLOBAL_STATIC(SessionPool, s_sessionPool) 0059 0060 /** 0061 * Private class that helps to provide binary compatibility between releases. 0062 * @internal 0063 */ 0064 class SmtpJobPrivate 0065 { 0066 public: 0067 explicit SmtpJobPrivate(SmtpJob *parent) 0068 : q(parent) 0069 { 0070 } 0071 0072 void doLogin(); 0073 0074 SmtpJob *const q; 0075 KSmtp::Session *session = nullptr; 0076 KSmtp::SessionUiProxy::Ptr uiProxy; 0077 enum State { Idle, Precommand, Smtp } currentState; 0078 bool finished; 0079 }; 0080 0081 SmtpJob::SmtpJob(Transport *transport, QObject *parent) 0082 : TransportJob(transport, parent) 0083 , d(new SmtpJobPrivate(this)) 0084 { 0085 d->currentState = SmtpJobPrivate::Idle; 0086 d->session = nullptr; 0087 d->finished = false; 0088 d->uiProxy = KSmtp::SessionUiProxy::Ptr(new SmtpSessionUiProxy); 0089 if (!s_sessionPool.isDestroyed()) { 0090 s_sessionPool->ref++; 0091 } 0092 } 0093 0094 SmtpJob::~SmtpJob() 0095 { 0096 if (!s_sessionPool.isDestroyed()) { 0097 s_sessionPool->ref--; 0098 if (s_sessionPool->ref == 0) { 0099 qCDebug(MAILTRANSPORT_SMTP_LOG) << "clearing SMTP session pool" << s_sessionPool->sessions.count(); 0100 while (!s_sessionPool->sessions.isEmpty()) { 0101 s_sessionPool->removeSession(*(s_sessionPool->sessions.begin())); 0102 } 0103 } 0104 } 0105 } 0106 0107 void SmtpJob::doStart() 0108 { 0109 if (s_sessionPool.isDestroyed()) { 0110 return; 0111 } 0112 0113 if ((!s_sessionPool->sessions.isEmpty() && s_sessionPool->sessions.contains(transport()->id())) || transport()->precommand().isEmpty()) { 0114 d->currentState = SmtpJobPrivate::Smtp; 0115 startSmtpJob(); 0116 } else { 0117 d->currentState = SmtpJobPrivate::Precommand; 0118 auto job = new PrecommandJob(transport()->precommand(), this); 0119 addSubjob(job); 0120 job->start(); 0121 } 0122 } 0123 0124 void SmtpJob::startSmtpJob() 0125 { 0126 if (s_sessionPool.isDestroyed()) { 0127 return; 0128 } 0129 0130 d->session = s_sessionPool->sessions.value(transport()->id()); 0131 if (!d->session) { 0132 d->session = new KSmtp::Session(transport()->host(), transport()->port()); 0133 d->session->setUseNetworkProxy(transport()->useProxy()); 0134 d->session->setUiProxy(d->uiProxy); 0135 switch (transport()->encryption()) { 0136 case Transport::EnumEncryption::None: 0137 d->session->setEncryptionMode(KSmtp::Session::Unencrypted); 0138 break; 0139 case Transport::EnumEncryption::TLS: 0140 d->session->setEncryptionMode(KSmtp::Session::STARTTLS); 0141 break; 0142 case Transport::EnumEncryption::SSL: 0143 d->session->setEncryptionMode(KSmtp::Session::TLS); 0144 break; 0145 default: 0146 qCWarning(MAILTRANSPORT_SMTP_LOG) << "Unknown encryption mode" << transport()->encryption(); 0147 break; 0148 } 0149 if (transport()->specifyHostname()) { 0150 d->session->setCustomHostname(transport()->localHostname()); 0151 } 0152 s_sessionPool->sessions.insert(transport()->id(), d->session); 0153 } 0154 0155 connect(d->session, &KSmtp::Session::stateChanged, this, &SmtpJob::sessionStateChanged, Qt::UniqueConnection); 0156 connect(d->session, &KSmtp::Session::connectionError, this, [this](const QString &err) { 0157 setError(KJob::UserDefinedError); 0158 setErrorText(err); 0159 s_sessionPool->removeSession(d->session); 0160 emitResult(); 0161 }); 0162 0163 if (d->session->state() == KSmtp::Session::Disconnected) { 0164 d->session->open(); 0165 } else { 0166 if (d->session->state() != KSmtp::Session::Authenticated) { 0167 startPasswordRetrieval(); 0168 } 0169 0170 startSendJob(); 0171 } 0172 } 0173 0174 void SmtpJob::sessionStateChanged(KSmtp::Session::State state) 0175 { 0176 if (state == KSmtp::Session::Ready) { 0177 startPasswordRetrieval(); 0178 } else if (state == KSmtp::Session::Authenticated) { 0179 startSendJob(); 0180 } 0181 } 0182 0183 void SmtpJob::startPasswordRetrieval(bool forceRefresh) 0184 { 0185 if (!transport()->requiresAuthentication() && !forceRefresh) { 0186 startSendJob(); 0187 return; 0188 } 0189 0190 if (transport()->authenticationType() == TransportBase::EnumAuthenticationType::XOAUTH2) { 0191 auto promise = KGAPI2::AccountManager::instance()->findAccount(GOOGLE_API_KEY, transport()->userName(), {KGAPI2::Account::mailScopeUrl()}); 0192 connect(promise, &KGAPI2::AccountPromise::finished, this, [forceRefresh, this](KGAPI2::AccountPromise *promise) { 0193 if (promise->account()) { 0194 if (forceRefresh) { 0195 promise = KGAPI2::AccountManager::instance()->refreshTokens(GOOGLE_API_KEY, GOOGLE_API_SECRET, transport()->userName()); 0196 } else { 0197 onTokenRequestFinished(promise); 0198 return; 0199 } 0200 } else { 0201 promise = KGAPI2::AccountManager::instance()->getAccount(GOOGLE_API_KEY, 0202 GOOGLE_API_SECRET, 0203 transport()->userName(), 0204 {KGAPI2::Account::mailScopeUrl()}); 0205 } 0206 connect(promise, &KGAPI2::AccountPromise::finished, this, &SmtpJob::onTokenRequestFinished); 0207 }); 0208 } else { 0209 startLoginJob(); 0210 } 0211 } 0212 0213 void SmtpJob::onTokenRequestFinished(KGAPI2::AccountPromise *promise) 0214 { 0215 if (promise->hasError()) { 0216 qCWarning(MAILTRANSPORT_SMTP_LOG) << "Error obtaining XOAUTH2 token:" << promise->errorText(); 0217 setError(KJob::UserDefinedError); 0218 setErrorText(promise->errorText()); 0219 emitResult(); 0220 return; 0221 } 0222 0223 const auto account = promise->account(); 0224 const QString tokens = QStringLiteral("%1\001%2").arg(account->accessToken(), account->refreshToken()); 0225 transport()->setPassword(tokens); 0226 startLoginJob(); 0227 } 0228 0229 void SmtpJob::startLoginJob() 0230 { 0231 if (!transport()->requiresAuthentication()) { 0232 startSendJob(); 0233 return; 0234 } 0235 0236 auto user = transport()->userName(); 0237 auto passwd = transport()->password(); 0238 if ((user.isEmpty() || passwd.isEmpty()) && transport()->authenticationType() != Transport::EnumAuthenticationType::GSSAPI) { 0239 QPointer<KPasswordDialog> dlg = new KPasswordDialog(nullptr, KPasswordDialog::ShowUsernameLine | KPasswordDialog::ShowKeepPassword); 0240 dlg->setAttribute(Qt::WA_DeleteOnClose, true); 0241 dlg->setPrompt( 0242 i18n("You need to supply a username and a password " 0243 "to use this SMTP server.")); 0244 dlg->setKeepPassword(transport()->storePassword()); 0245 dlg->addCommentLine(QString(), transport()->name()); 0246 dlg->setUsername(user); 0247 dlg->setPassword(passwd); 0248 dlg->setRevealPasswordAvailable(KAuthorized::authorize(QStringLiteral("lineedit_reveal_password"))); 0249 0250 connect(this, &KJob::result, dlg, &QDialog::reject); 0251 0252 connect(dlg, &QDialog::finished, this, [this, dlg](const int result) { 0253 if (result == QDialog::Rejected) { 0254 setError(KilledJobError); 0255 emitResult(); 0256 return; 0257 } 0258 0259 transport()->setUserName(dlg->username()); 0260 transport()->setPassword(dlg->password()); 0261 transport()->setStorePassword(dlg->keepPassword()); 0262 transport()->save(); 0263 0264 d->doLogin(); 0265 }); 0266 dlg->open(); 0267 0268 return; 0269 } 0270 0271 d->doLogin(); 0272 } 0273 0274 void SmtpJobPrivate::doLogin() 0275 { 0276 QString passwd = q->transport()->password(); 0277 if (q->transport()->authenticationType() == Transport::EnumAuthenticationType::XOAUTH2) { 0278 passwd = passwd.left(passwd.indexOf(QLatin1Char('\001'))); 0279 } 0280 0281 auto login = new KSmtp::LoginJob(session); 0282 login->setUserName(q->transport()->userName()); 0283 login->setPassword(passwd); 0284 switch (q->transport()->authenticationType()) { 0285 case TransportBase::EnumAuthenticationType::PLAIN: 0286 login->setPreferedAuthMode(KSmtp::LoginJob::Plain); 0287 break; 0288 case TransportBase::EnumAuthenticationType::LOGIN: 0289 login->setPreferedAuthMode(KSmtp::LoginJob::Login); 0290 break; 0291 case TransportBase::EnumAuthenticationType::CRAM_MD5: 0292 login->setPreferedAuthMode(KSmtp::LoginJob::CramMD5); 0293 break; 0294 case TransportBase::EnumAuthenticationType::XOAUTH2: 0295 login->setPreferedAuthMode(KSmtp::LoginJob::XOAuth2); 0296 break; 0297 case TransportBase::EnumAuthenticationType::DIGEST_MD5: 0298 login->setPreferedAuthMode(KSmtp::LoginJob::DigestMD5); 0299 break; 0300 case TransportBase::EnumAuthenticationType::NTLM: 0301 login->setPreferedAuthMode(KSmtp::LoginJob::NTLM); 0302 break; 0303 case TransportBase::EnumAuthenticationType::GSSAPI: 0304 login->setPreferedAuthMode(KSmtp::LoginJob::GSSAPI); 0305 break; 0306 default: 0307 qCWarning(MAILTRANSPORT_SMTP_LOG) << "Unknown authentication mode" << q->transport()->authenticationTypeString(); 0308 break; 0309 } 0310 0311 q->addSubjob(login); 0312 login->start(); 0313 qCDebug(MAILTRANSPORT_SMTP_LOG) << "Login started"; 0314 } 0315 0316 void SmtpJob::startSendJob() 0317 { 0318 auto send = new KSmtp::SendJob(d->session); 0319 send->setFrom(sender()); 0320 send->setTo(to()); 0321 send->setCc(cc()); 0322 send->setBcc(bcc()); 0323 send->setData(data()); 0324 send->setDeliveryStatusNotification(deliveryStatusNotification()); 0325 0326 addSubjob(send); 0327 send->start(); 0328 0329 qCDebug(MAILTRANSPORT_SMTP_LOG) << "Send started"; 0330 } 0331 0332 bool SmtpJob::doKill() 0333 { 0334 if (s_sessionPool.isDestroyed()) { 0335 return false; 0336 } 0337 0338 if (!hasSubjobs()) { 0339 return true; 0340 } 0341 if (d->currentState == SmtpJobPrivate::Precommand) { 0342 return subjobs().first()->kill(); 0343 } else if (d->currentState == SmtpJobPrivate::Smtp) { 0344 clearSubjobs(); 0345 s_sessionPool->removeSession(d->session); 0346 return true; 0347 } 0348 return false; 0349 } 0350 0351 void SmtpJob::slotResult(KJob *job) 0352 { 0353 if (s_sessionPool.isDestroyed()) { 0354 removeSubjob(job); 0355 return; 0356 } 0357 if (qobject_cast<KSmtp::LoginJob *>(job)) { 0358 if (job->error() == KSmtp::LoginJob::TokenExpired) { 0359 removeSubjob(job); 0360 startPasswordRetrieval(/*force refresh */ true); 0361 return; 0362 } 0363 } 0364 0365 // The job has finished, so we don't care about any further errors. Set 0366 // d->finished to true, so slaveError() knows about this and doesn't call 0367 // emitResult() anymore. 0368 // Sometimes, the SMTP slave emits more than one error 0369 // 0370 // The first error causes slotResult() to be called, but not slaveError(), since 0371 // the scheduler doesn't emit errors for connected slaves. 0372 // 0373 // The second error then causes slaveError() to be called (as the slave is no 0374 // longer connected), which does emitResult() a second time, which is invalid 0375 // (and triggers an assert in KMail). 0376 d->finished = true; 0377 0378 // Normally, calling TransportJob::slotResult() would set the proper error code 0379 // for error() via KComposite::slotResult(). However, we can't call that here, 0380 // since that also emits the result signal. 0381 // In KMail, when there are multiple mails in the outbox, KMail tries to send 0382 // the next mail when it gets the result signal, which then would reuse the 0383 // old broken slave from the slave pool if there was an error. 0384 // To prevent that, we call TransportJob::slotResult() only after removing the 0385 // slave from the pool and calculate the error code ourselves. 0386 int errorCode = error(); 0387 if (!errorCode) { 0388 errorCode = job->error(); 0389 } 0390 0391 if (errorCode && d->currentState == SmtpJobPrivate::Smtp) { 0392 s_sessionPool->removeSession(d->session); 0393 TransportJob::slotResult(job); 0394 return; 0395 } 0396 0397 TransportJob::slotResult(job); 0398 if (!error() && d->currentState == SmtpJobPrivate::Precommand) { 0399 d->currentState = SmtpJobPrivate::Smtp; 0400 startSmtpJob(); 0401 return; 0402 } 0403 if (!error() && !hasSubjobs()) { 0404 emitResult(); 0405 } 0406 } 0407 0408 #include "moc_smtpjob.cpp"