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"