diff --git a/CMakeLists.txt b/CMakeLists.txt index 17762b6..385c327 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,61 +1,61 @@ cmake_minimum_required(VERSION 3.0) set(PIM_VERSION "5.8.40") project(MailTransport VERSION ${PIM_VERSION}) # ECM setup set(KF5_VERSION "5.46.0") find_package(ECM ${KF5_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) include(GenerateExportHeader) include(ECMGenerateHeaders) include(ECMGeneratePriFile) include(CMakePackageConfigHelpers) include(ECMSetupVersion) include(FeatureSummary) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMQtDeclareLoggingCategory) include(ECMCoverageOption) add_definitions(-DTRANSLATION_DOMAIN=\"libmailtransport5\") set(KMAILTRANSPORT_LIB_VERSION ${PIM_VERSION}) set(KMIME_LIB_VERSION "5.8.40") set(AKONADI_LIB_VERSION "5.8.40") set(AKONADIMIME_LIB_VERSION "5.8.40") set(KSMTP_LIB_VERSION "5.8.43") -set(KGAPI_LIB_VERSION "5.8.41") +set(KGAPI_LIB_VERSION "5.8.42") set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF5MailTransport") ########### Find packages ########### find_package(KF5KCMUtils ${KF5_VERSION} CONFIG REQUIRED) find_package(KF5ConfigWidgets ${KF5_VERSION} CONFIG REQUIRED) find_package(KF5Wallet ${KF5_VERSION} CONFIG REQUIRED) find_package(KF5I18n ${KF5_VERSION} CONFIG REQUIRED) find_package(KF5KIO ${KF5_VERSION} CONFIG REQUIRED) find_package(KF5Mime ${KMIME_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Akonadi ${AKONADI_LIB_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiMime ${AKONADIMIME_LIB_VERSION} CONFIG REQUIRED) find_package(KPimSMTP ${KSMTP_LIB_VERSION} CONFIG REQUIRED) find_package(KPimGAPI ${KGAPI_LIB_VERSION} CONFIG REQUIRED) add_definitions("-DQT_NO_CAST_FROM_ASCII -DQT_NO_CAST_TO_ASCII") add_definitions(-DQT_NO_NARROWING_CONVERSIONS_IN_CONNECT) add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x060000) add_definitions(-DQT_NO_URL_CAST_FROM_STRING) if(BUILD_TESTING) add_definitions(-DBUILD_TESTING) endif(BUILD_TESTING) ########### Targets ########### add_subdirectory(src) install( FILES kmailtransport.renamecategories kmailtransport.categories DESTINATION ${KDE_INSTALL_CONFDIR} ) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/kmailtransport/plugins/smtp/smtpjob.cpp b/src/kmailtransport/plugins/smtp/smtpjob.cpp index f1dabde..771ca69 100644 --- a/src/kmailtransport/plugins/smtp/smtpjob.cpp +++ b/src/kmailtransport/plugins/smtp/smtpjob.cpp @@ -1,441 +1,429 @@ /* Copyright (c) 2007 Volker Krause Based on KMail code by: Copyright (c) 1996-1998 Stefan Taferner This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "smtpjob.h" #include "transport.h" #include "mailtransport_defs.h" #include "precommandjob.h" #include "sessionuiproxy.h" #include "mailtransportplugin_smtp_debug.h" #include #include #include #include #include "mailtransport_debug.h" #include #include #include #include #include #include +#include #define GOOGLE_API_KEY QStringLiteral("554041944266.apps.googleusercontent.com") #define GOOGLE_API_SECRET QStringLiteral("mdT1DjzohxN3npUUzkENT0gO") using namespace MailTransport; class SessionPool { public: SessionPool() : ref(0) { } int ref; QHash sessions; void removeSession(KSmtp::Session *session) { qCDebug(MAILTRANSPORT_SMTP_LOG) << "Removing session" << session << "from the pool"; int key = sessions.key(session); if (key > 0) { QObject::connect(session, &KSmtp::Session::stateChanged, [session](KSmtp::Session::State state) { if (state == KSmtp::Session::Disconnected) { session->deleteLater(); } }); session->quit(); sessions.remove(key); } } }; Q_GLOBAL_STATIC(SessionPool, s_sessionPool) /** * Private class that helps to provide binary compatibility between releases. * @internal */ class SmtpJobPrivate { public: SmtpJobPrivate(SmtpJob *parent) : q(parent) { } SmtpJob *q; KSmtp::Session *session = nullptr; KSmtp::SessionUiProxy::Ptr uiProxy; enum State { Idle, Precommand, Smtp } currentState; bool finished; }; SmtpJob::SmtpJob(Transport *transport, QObject *parent) : TransportJob(transport, parent) , d(new SmtpJobPrivate(this)) { d->currentState = SmtpJobPrivate::Idle; d->session = nullptr; d->finished = false; d->uiProxy = KSmtp::SessionUiProxy::Ptr(new SmtpSessionUiProxy); if (!s_sessionPool.isDestroyed()) { s_sessionPool->ref++; } } SmtpJob::~SmtpJob() { if (!s_sessionPool.isDestroyed()) { s_sessionPool->ref--; if (s_sessionPool->ref == 0) { qCDebug(MAILTRANSPORT_SMTP_LOG) << "clearing SMTP session pool" << s_sessionPool->sessions.count(); while (!s_sessionPool->sessions.isEmpty()) { s_sessionPool->removeSession(*(s_sessionPool->sessions.begin())); } } } delete d; } void SmtpJob::doStart() { if (s_sessionPool.isDestroyed()) { return; } if ((!s_sessionPool->sessions.isEmpty() && s_sessionPool->sessions.contains(transport()->id())) || transport()->precommand().isEmpty()) { d->currentState = SmtpJobPrivate::Smtp; startSmtpJob(); } else { d->currentState = SmtpJobPrivate::Precommand; PrecommandJob *job = new PrecommandJob(transport()->precommand(), this); addSubjob(job); job->start(); } } void SmtpJob::startSmtpJob() { if (s_sessionPool.isDestroyed()) { return; } d->session = s_sessionPool->sessions.value(transport()->id()); if (!d->session) { d->session = new KSmtp::Session(transport()->host(), transport()->port()); d->session->setUseNetworkProxy(transport()->useProxy()); d->session->setUiProxy(d->uiProxy); if (transport()->specifyHostname()) { d->session->setCustomHostname(transport()->localHostname()); } s_sessionPool->sessions.insert(transport()->id(), d->session); } connect(d->session, &KSmtp::Session::stateChanged, this, &SmtpJob::sessionStateChanged, Qt::UniqueConnection); connect(d->session, &KSmtp::Session::connectionError, this, [this](const QString &err) { setError(KJob::UserDefinedError); setErrorText(err); s_sessionPool->removeSession(d->session); emitResult(); }); if (d->session->state() == KSmtp::Session::Disconnected) { d->session->open(); } else { if (d->session->state() != KSmtp::Session::Authenticated) { startPasswordRetrieval(); } startSendJob(); } } void SmtpJob::sessionStateChanged(KSmtp::Session::State state) { if (state == KSmtp::Session::Ready) { startPasswordRetrieval(); } else if (state == KSmtp::Session::Authenticated) { startSendJob(); } } void SmtpJob::startPasswordRetrieval(bool forceRefresh) { if (!transport()->requiresAuthentication() && !forceRefresh) { startSendJob(); return; } if (transport()->authenticationType() == TransportBase::EnumAuthenticationType::XOAUTH2) { - const auto tokens = transport()->password(); - if (tokens.isEmpty()) { - requestToken(); - } else { - const QString token = tokens.mid(tokens.indexOf(QLatin1Char('\001')) + 1); - if (forceRefresh || token.isEmpty() || token == tokens) { - requestToken(token); // if token == tokens, assume it's account password - } else { - startLoginJob(); - } - } + auto promise = KGAPI2::AccountManager::instance()->findAccount( + GOOGLE_API_KEY, transport()->userName(), { KGAPI2::Account::mailScopeUrl() }); + connect(promise, &KGAPI2::AccountPromise::finished, + this, [forceRefresh, this](KGAPI2::AccountPromise *promise) { + if (promise->account()) { + if (forceRefresh) { + promise = KGAPI2::AccountManager::instance()->refreshTokens( + GOOGLE_API_KEY, GOOGLE_API_SECRET, transport()->userName()); + } else { + onTokenRequestFinished(promise); + return; + } + } else { + promise = KGAPI2::AccountManager::instance()->getAccount( + GOOGLE_API_KEY, GOOGLE_API_SECRET, transport()->userName(), + { KGAPI2::Account::mailScopeUrl() }); + } + connect(promise, &KGAPI2::AccountPromise::finished, + this, &SmtpJob::onTokenRequestFinished); + }); } else { startLoginJob(); } } -void SmtpJob::requestToken(const QString &password) -{ - auto acc = KGAPI2::AccountPtr::create(transport()->userName(), - QString(), QString(), - QList() << QUrl(QStringLiteral("https://mail.google.com/"))); - - auto authJob = new KGAPI2::AuthJob(acc, GOOGLE_API_KEY, GOOGLE_API_SECRET, this); - authJob->setUsername(transport()->userName()); - authJob->setPassword(password); - connect(authJob, &KGAPI2::Job::finished, this, &SmtpJob::onTokenRequestFinished); -} - -void SmtpJob::refreshToken(const QString &refreshToken) +void SmtpJob::onTokenRequestFinished(KGAPI2::AccountPromise *promise) { - auto acc = KGAPI2::AccountPtr::create(transport()->userName(), - QString(), refreshToken, - QList() << QUrl(QStringLiteral("https://mail.google.com/"))); - auto authJob = new KGAPI2::AuthJob(acc, GOOGLE_API_KEY, GOOGLE_API_SECRET, this); - authJob->setUsername(transport()->userName()); - connect(authJob, &KGAPI2::Job::finished, this, &SmtpJob::onTokenRequestFinished); -} - -void SmtpJob::onTokenRequestFinished(KGAPI2::Job *job) -{ - auto authJob = qobject_cast(job); - if (authJob->error()) { - qCWarning(MAILTRANSPORT_SMTP_LOG) << "Error obtaining XOAUTH2 token:" << authJob->errorString(); - // TODO: CANCEL + if (promise->hasError()) { + qCWarning(MAILTRANSPORT_SMTP_LOG) << "Error obtaining XOAUTH2 token:" << promise->errorText(); + setError(KJob::UserDefinedError); + setErrorText(promise->errorText()); + emitResult(); return; } - const auto account = authJob->account(); + const auto account = promise->account(); const QString tokens = QStringLiteral("%1\001%2").arg(account->accessToken(), account->refreshToken()); transport()->setPassword(tokens); - transport()->save(); startLoginJob(); } void SmtpJob::startLoginJob() { if (!transport()->requiresAuthentication()) { startSendJob(); return; } auto login = new KSmtp::LoginJob(d->session); auto user = transport()->userName(); auto passwd = transport()->password(); if ((user.isEmpty() || passwd.isEmpty()) && transport()->authenticationType() != Transport::EnumAuthenticationType::GSSAPI) { QPointer dlg = new KPasswordDialog( nullptr, KPasswordDialog::ShowUsernameLine |KPasswordDialog::ShowKeepPassword); dlg->setPrompt(i18n("You need to supply a username and a password " "to use this SMTP server.")); dlg->setKeepPassword(transport()->storePassword()); dlg->addCommentLine(QString(), transport()->name()); dlg->setUsername(user); dlg->setPassword(passwd); bool gotIt = false; if (dlg->exec()) { transport()->setUserName(dlg->username()); transport()->setPassword(dlg->password()); transport()->setStorePassword(dlg->keepPassword()); transport()->save(); gotIt = true; } delete dlg; if (!gotIt) { setError(KilledJobError); emitResult(); return; } } if (transport()->authenticationType() == Transport::EnumAuthenticationType::XOAUTH2) { passwd = passwd.left(passwd.indexOf(QLatin1Char('\001'))); } login->setUserName(transport()->userName()); login->setPassword(passwd); switch (transport()->authenticationType()) { case TransportBase::EnumAuthenticationType::PLAIN: login->setPreferedAuthMode(KSmtp::LoginJob::Plain); break; case TransportBase::EnumAuthenticationType::LOGIN: login->setPreferedAuthMode(KSmtp::LoginJob::Login); break; case TransportBase::EnumAuthenticationType::CRAM_MD5: login->setPreferedAuthMode(KSmtp::LoginJob::CramMD5); break; case TransportBase::EnumAuthenticationType::XOAUTH2: login->setPreferedAuthMode(KSmtp::LoginJob::XOAuth2); break; case TransportBase::EnumAuthenticationType::DIGEST_MD5: login->setPreferedAuthMode(KSmtp::LoginJob::DigestMD5); break; case TransportBase::EnumAuthenticationType::NTLM: login->setPreferedAuthMode(KSmtp::LoginJob::NTLM); break; case TransportBase::EnumAuthenticationType::GSSAPI: login->setPreferedAuthMode(KSmtp::LoginJob::GSSAPI); break; default: qCWarning(MAILTRANSPORT_SMTP_LOG) << "Unknown authentication mode" << transport()->authenticationTypeString(); break; } switch (transport()->encryption()) { case Transport::EnumEncryption::None: login->setEncryptionMode(KSmtp::LoginJob::Unencrypted); break; case Transport::EnumEncryption::TLS: login->setEncryptionMode(KSmtp::LoginJob::STARTTLS); break; case Transport::EnumEncryption::SSL: login->setEncryptionMode(KSmtp::LoginJob::SSLorTLS); break; default: qCWarning(MAILTRANSPORT_SMTP_LOG) << "Unknown encryption mode" << transport()->encryption(); break; } connect(login, &KJob::result, this, &SmtpJob::slotResult); addSubjob(login); login->start(); qCDebug(MAILTRANSPORT_SMTP_LOG) << "Login started"; } void SmtpJob::startSendJob() { auto send = new KSmtp::SendJob(d->session); send->setFrom(sender()); send->setTo(to()); send->setCc(cc()); send->setBcc(bcc()); send->setData(data()); addSubjob(send); send->start(); qCDebug(MAILTRANSPORT_SMTP_LOG) << "Send started"; } bool SmtpJob::doKill() { if (s_sessionPool.isDestroyed()) { return false; } if (!hasSubjobs()) { return true; } if (d->currentState == SmtpJobPrivate::Precommand) { return subjobs().first()->kill(); } else if (d->currentState == SmtpJobPrivate::Smtp) { clearSubjobs(); s_sessionPool->removeSession(d->session); return true; } return false; } void SmtpJob::slotResult(KJob *job) { if (s_sessionPool.isDestroyed()) { return; } if (qobject_cast(job)) { if (job->error() == KSmtp::LoginJob::TokenExpired) { startPasswordRetrieval(/*force refresh */ true); return; } } // The job has finished, so we don't care about any further errors. Set // d->finished to true, so slaveError() knows about this and doesn't call // emitResult() anymore. // Sometimes, the SMTP slave emits more than one error // // The first error causes slotResult() to be called, but not slaveError(), since // the scheduler doesn't emit errors for connected slaves. // // The second error then causes slaveError() to be called (as the slave is no // longer connected), which does emitResult() a second time, which is invalid // (and triggers an assert in KMail). d->finished = true; // Normally, calling TransportJob::slotResult() whould set the proper error code // for error() via KComposite::slotResult(). However, we can't call that here, // since that also emits the result signal. // In KMail, when there are multiple mails in the outbox, KMail tries to send // the next mail when it gets the result signal, which then would reuse the // old broken slave from the slave pool if there was an error. // To prevent that, we call TransportJob::slotResult() only after removing the // slave from the pool and calculate the error code ourselves. int errorCode = error(); if (!errorCode) { errorCode = job->error(); } if (errorCode && d->currentState == SmtpJobPrivate::Smtp) { s_sessionPool->removeSession(d->session); TransportJob::slotResult(job); return; } TransportJob::slotResult(job); if (!error() && d->currentState == SmtpJobPrivate::Precommand) { d->currentState = SmtpJobPrivate::Smtp; startSmtpJob(); return; } if (!error() && !hasSubjobs()) { emitResult(); } } #include "moc_smtpjob.cpp" diff --git a/src/kmailtransport/plugins/smtp/smtpjob.h b/src/kmailtransport/plugins/smtp/smtpjob.h index b5acd1c..72458a8 100644 --- a/src/kmailtransport/plugins/smtp/smtpjob.h +++ b/src/kmailtransport/plugins/smtp/smtpjob.h @@ -1,90 +1,88 @@ /* Copyright (c) 2007 Volker Krause Based on KMail code by: Copyright (c) 1996-1998 Stefan Taferner This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MAILTRANSPORT_SMTPJOB_H #define MAILTRANSPORT_SMTPJOB_H #include #include namespace KIO { class Job; class Slave; } namespace KGAPI2 { -class Job; +class AccountPromise; } class SmtpJobPrivate; namespace MailTransport { /** Mail transport job for SMTP. Internally, all jobs for a specific transport are queued to use the same KIO::Slave. This avoids multiple simultaneous connections to the server, which is not always allowed. Also, re-using an already existing connection avoids the login overhead and can improve performance. Precommands are automatically executed, once per opening a connection to the server (not necessarily once per message). */ class SmtpJob : public TransportJob { Q_OBJECT public: /** Creates a SmtpJob. @param transport The transport settings. @param parent The parent object. */ explicit SmtpJob(Transport *transport, QObject *parent = nullptr); /** Deletes this job. */ ~SmtpJob() override; protected: void doStart() override; bool doKill() override; protected Q_SLOTS: void slotResult(KJob *job) override; void sessionStateChanged(KSmtp::Session::State state); private: void startPasswordRetrieval(bool forceRefresh = false); - void requestToken(const QString &password = {}); - void refreshToken(const QString &token); - void onTokenRequestFinished(KGAPI2::Job *result); + void onTokenRequestFinished(KGAPI2::AccountPromise *result); void startSmtpJob(); void startLoginJob(); void startSendJob(); private: friend class ::SmtpJobPrivate; SmtpJobPrivate *const d; }; } // namespace MailTransport #endif // MAILTRANSPORT_SMTPJOB_H