diff --git a/CMakeLists.txt b/CMakeLists.txt index 65ec0d3..89388fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,59 +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.44.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.7.80") set(AKONADI_LIB_VERSION "5.7.80") set(AKONADIMIME_LIB_VERSION "5.7.80") -set(KSMTP_LIB_VERSION "5.7.80") +set(KSMTP_LIB_VERSION "5.8.41") +set(KGAPI_LIB_VERSION "5.8.41") 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/CMakeLists.txt b/src/kmailtransport/plugins/smtp/CMakeLists.txt index e384c77..dc62777 100644 --- a/src/kmailtransport/plugins/smtp/CMakeLists.txt +++ b/src/kmailtransport/plugins/smtp/CMakeLists.txt @@ -1,26 +1,27 @@ if (BUILD_TESTING) add_subdirectory(autotests) endif() set(mailtransport_smtpplugin_SRCS smtpmailtransportplugin.cpp smtpconfigdialog.cpp smtpjob.cpp smtpconfigwidget.cpp ) ki18n_wrap_ui(mailtransport_smtpplugin_SRCS smtpsettings.ui ) ecm_qt_declare_logging_category(mailtransport_smtpplugin_SRCS HEADER mailtransportplugin_smtp_debug.h IDENTIFIER MAILTRANSPORT_SMTP_LOG CATEGORY_NAME org.kde.pim.mailtransport.smtpplugin) kcoreaddons_add_plugin(mailtransport_smtpplugin JSON smtpmailtransport.json SOURCES ${mailtransport_smtpplugin_SRCS} INSTALL_NAMESPACE mailtransport) target_link_libraries(mailtransport_smtpplugin KF5::MailTransport KF5::I18n KF5::ConfigWidgets KF5::KIOWidgets KF5::Completion KPim::SMTP + KPim::GAPICore ) diff --git a/src/kmailtransport/plugins/smtp/autotests/CMakeLists.txt b/src/kmailtransport/plugins/smtp/autotests/CMakeLists.txt index f85ae2a..a92ecf4 100644 --- a/src/kmailtransport/plugins/smtp/autotests/CMakeLists.txt +++ b/src/kmailtransport/plugins/smtp/autotests/CMakeLists.txt @@ -1,19 +1,20 @@ include(ECMAddTests) find_package(Qt5Test ${QT_REQUIRED_VERSION} REQUIRED) include_directories(${CMAKE_CURRENT_BINARY_DIR}/..) ecm_add_test(smtpjobtest.cpp ../smtpjob.cpp ${CMAKE_CURRENT_BINARY_DIR}/../mailtransportplugin_smtp_debug.cpp fakeserver.cpp LINK_LIBRARIES Qt5::Network Qt5::Test KF5::MailTransport KF5::I18n KF5::ConfigWidgets KF5::KIOWidgets KPim::SMTP + KPim::GAPICore TEST_NAME smtpjobtest ) diff --git a/src/kmailtransport/plugins/smtp/smtpconfigwidget.cpp b/src/kmailtransport/plugins/smtp/smtpconfigwidget.cpp index b91fe49..10f980c 100644 --- a/src/kmailtransport/plugins/smtp/smtpconfigwidget.cpp +++ b/src/kmailtransport/plugins/smtp/smtpconfigwidget.cpp @@ -1,351 +1,352 @@ /* Copyright (c) 2009 Constantin Berzan Based on MailTransport code by: Copyright (c) 2006 - 2007 Volker Krause Copyright (c) 2007 KovoKs Based on KMail code by: Copyright (c) 2001-2002 Michael Haeckel 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 "smtpconfigwidget.h" #include "ui_smtpsettings.h" #include "widgets/transportconfigwidget_p.h" #include "transport.h" #include "transportmanager.h" #include "servertest.h" #include "mailtransport_defs.h" #include "mailtransportplugin_smtp_debug.h" #include #include #include #include "mailtransport_debug.h" #include using namespace MailTransport; class MailTransport::SMTPConfigWidgetPrivate : public TransportConfigWidgetPrivate { public: ::Ui::SMTPSettings ui; ServerTest *serverTest = nullptr; QButtonGroup *encryptionGroup = nullptr; // detected authentication capabilities QVector noEncCapa, sslCapa, tlsCapa; bool serverTestFailed; static void addAuthenticationItem(QComboBox *combo, int authenticationType) { combo->addItem(Transport::authenticationTypeString(authenticationType), QVariant(authenticationType)); } void resetAuthCapabilities() { noEncCapa.clear(); noEncCapa << Transport::EnumAuthenticationType::LOGIN << Transport::EnumAuthenticationType::PLAIN << Transport::EnumAuthenticationType::CRAM_MD5 << Transport::EnumAuthenticationType::DIGEST_MD5 << Transport::EnumAuthenticationType::NTLM - << Transport::EnumAuthenticationType::GSSAPI; + << Transport::EnumAuthenticationType::GSSAPI + << Transport::EnumAuthenticationType::XOAUTH2; sslCapa = tlsCapa = noEncCapa; updateAuthCapbilities(); } void updateAuthCapbilities() { if (serverTestFailed) { return; } QVector capa = noEncCapa; if (ui.encryptionSsl->isChecked()) { capa = sslCapa; } else if (ui.encryptionTls->isChecked()) { capa = tlsCapa; } ui.authCombo->clear(); for (int authType : qAsConst(capa)) { addAuthenticationItem(ui.authCombo, authType); } if (transport->isValid()) { const int idx = ui.authCombo->findData(transport->authenticationType()); if (idx != -1) { ui.authCombo->setCurrentIndex(idx); } } if (capa.isEmpty()) { ui.noAuthPossible->setVisible(true); ui.kcfg_requiresAuthentication->setChecked(false); ui.kcfg_requiresAuthentication->setEnabled(false); ui.kcfg_requiresAuthentication->setVisible(false); ui.authCombo->setEnabled(false); ui.authLabel->setEnabled(false); } else { ui.noAuthPossible->setVisible(false); ui.kcfg_requiresAuthentication->setEnabled(true); ui.kcfg_requiresAuthentication->setVisible(true); ui.authCombo->setEnabled(true); ui.authLabel->setEnabled(true); } } }; SMTPConfigWidget::SMTPConfigWidget(Transport *transport, QWidget *parent) : TransportConfigWidget(*new SMTPConfigWidgetPrivate, transport, parent) { init(); } static void checkHighestEnabledButton(QButtonGroup *group) { Q_ASSERT(group); for (int i = group->buttons().count() - 1; i >= 0; --i) { QAbstractButton *b = group->buttons().at(i); if (b && b->isEnabled()) { b->animateClick(); return; } } } void SMTPConfigWidget::init() { Q_D(SMTPConfigWidget); d->serverTest = nullptr; connect(TransportManager::self(), &TransportManager::passwordsChanged, this, &SMTPConfigWidget::passwordsLoaded); d->serverTestFailed = false; d->ui.setupUi(this); d->manager->addWidget(this); // otherwise it doesn't find out about these widgets d->manager->updateWidgets(); d->ui.password->setWhatsThis(i18n("The password to send to the server for authorization.")); d->ui.kcfg_userName->setClearButtonShown(true); d->encryptionGroup = new QButtonGroup(this); d->encryptionGroup->addButton(d->ui.encryptionNone, Transport::EnumEncryption::None); d->encryptionGroup->addButton(d->ui.encryptionSsl, Transport::EnumEncryption::SSL); d->encryptionGroup->addButton(d->ui.encryptionTls, Transport::EnumEncryption::TLS); d->ui.encryptionNone->setChecked(d->transport->encryption() == Transport::EnumEncryption::None); d->ui.encryptionSsl->setChecked(d->transport->encryption() == Transport::EnumEncryption::SSL); d->ui.encryptionTls->setChecked(d->transport->encryption() == Transport::EnumEncryption::TLS); d->resetAuthCapabilities(); if (!KProtocolInfo::capabilities(SMTP_PROTOCOL).contains(QLatin1String("SASL"))) { d->ui.authCombo->removeItem(d->ui.authCombo->findData( Transport::EnumAuthenticationType::NTLM)); d->ui.authCombo->removeItem(d->ui.authCombo->findData( Transport::EnumAuthenticationType::GSSAPI)); } connect(d->ui.checkCapabilities, &QPushButton::clicked, this, &SMTPConfigWidget::checkSmtpCapabilities); connect(d->ui.kcfg_host, &QLineEdit::textChanged, this, &SMTPConfigWidget::hostNameChanged); connect(d->encryptionGroup, QOverload::of(&QButtonGroup::buttonClicked), this, &SMTPConfigWidget::encryptionChanged); connect(d->ui.kcfg_requiresAuthentication, &QCheckBox::toggled, this, &SMTPConfigWidget::ensureValidAuthSelection); if (!d->transport->isValid()) { checkHighestEnabledButton(d->encryptionGroup); } // load the password d->transport->updatePasswordState(); if (d->transport->isComplete()) { d->ui.password->setPassword(d->transport->password()); } else { if (d->transport->requiresAuthentication()) { TransportManager::self()->loadPasswordsAsync(); } } hostNameChanged(d->transport->host()); } void SMTPConfigWidget::checkSmtpCapabilities() { Q_D(SMTPConfigWidget); d->serverTest = new ServerTest(this); d->serverTest->setProtocol(SMTP_PROTOCOL); d->serverTest->setServer(d->ui.kcfg_host->text().trimmed()); if (d->ui.kcfg_specifyHostname->isChecked()) { d->serverTest->setFakeHostname(d->ui.kcfg_localHostname->text()); } QAbstractButton *encryptionChecked = d->encryptionGroup->checkedButton(); if (encryptionChecked == d->ui.encryptionNone) { d->serverTest->setPort(Transport::EnumEncryption::None, d->ui.kcfg_port->value()); } else if (encryptionChecked == d->ui.encryptionSsl) { d->serverTest->setPort(Transport::EnumEncryption::SSL, d->ui.kcfg_port->value()); } d->serverTest->setProgressBar(d->ui.checkCapabilitiesProgress); d->ui.checkCapabilitiesStack->setCurrentIndex(1); qApp->setOverrideCursor(Qt::BusyCursor); connect(d->serverTest, &ServerTest::finished, this, &SMTPConfigWidget::slotFinished); connect(d->serverTest, &ServerTest::finished, qApp, [](){ qApp->restoreOverrideCursor(); }); d->ui.checkCapabilities->setEnabled(false); d->serverTest->start(); d->serverTestFailed = false; } void SMTPConfigWidget::apply() { Q_D(SMTPConfigWidget); Q_ASSERT(d->manager); d->manager->updateSettings(); d->transport->setPassword(d->ui.password->password()); KConfigGroup group(d->transport->config(), d->transport->currentGroup()); const int index = d->ui.authCombo->currentIndex(); if (index >= 0) { group.writeEntry("authtype", d->ui.authCombo->itemData(index).toInt()); } if (d->ui.encryptionNone->isChecked()) { d->transport->setEncryption(Transport::EnumEncryption::None); } else if (d->ui.encryptionSsl->isChecked()) { d->transport->setEncryption(Transport::EnumEncryption::SSL); } else if (d->ui.encryptionTls->isChecked()) { d->transport->setEncryption(Transport::EnumEncryption::TLS); } TransportConfigWidget::apply(); } void SMTPConfigWidget::passwordsLoaded() { Q_D(SMTPConfigWidget); // Load the password from the original to our cloned copy d->transport->updatePasswordState(); if (d->ui.password->password().isEmpty()) { d->ui.password->setPassword(d->transport->password()); } } // TODO rename void SMTPConfigWidget::slotFinished(const QVector &results) { Q_D(SMTPConfigWidget); d->ui.checkCapabilitiesStack->setCurrentIndex(0); d->ui.checkCapabilities->setEnabled(true); d->serverTest->deleteLater(); // If the servertest did not find any useable authentication modes, assume the // connection failed and don't disable any of the radioboxes. if (results.isEmpty()) { KMessageBox::error(this, i18n("Failed to check capabilities. Please verify port and authentication mode."), i18n("Check Capabilities Failed")); d->serverTestFailed = true; d->serverTest->deleteLater(); return; } // encryption method d->ui.encryptionNone->setEnabled(results.contains(Transport::EnumEncryption::None)); d->ui.encryptionSsl->setEnabled(results.contains(Transport::EnumEncryption::SSL)); d->ui.encryptionTls->setEnabled(results.contains(Transport::EnumEncryption::TLS)); checkHighestEnabledButton(d->encryptionGroup); d->noEncCapa = d->serverTest->normalProtocols(); if (d->ui.encryptionTls->isEnabled()) { d->tlsCapa = d->serverTest->tlsProtocols(); } else { d->tlsCapa.clear(); } d->sslCapa = d->serverTest->secureProtocols(); d->updateAuthCapbilities(); //Show correct port from capabilities. if (d->ui.encryptionSsl->isEnabled()) { const int portValue = d->serverTest->port(Transport::EnumEncryption::SSL); d->ui.kcfg_port->setValue(portValue == -1 ? SMTPS_PORT : portValue); } else if (d->ui.encryptionNone->isEnabled()) { const int portValue = d->serverTest->port(Transport::EnumEncryption::None); d->ui.kcfg_port->setValue(portValue == -1 ? SMTP_PORT : portValue); } d->serverTest->deleteLater(); } void SMTPConfigWidget::hostNameChanged(const QString &text) { // TODO: really? is this done at every change? wtf Q_D(SMTPConfigWidget); // sanitize hostname... const int pos = d->ui.kcfg_host->cursorPosition(); d->ui.kcfg_host->blockSignals(true); d->ui.kcfg_host->setText(text.trimmed()); d->ui.kcfg_host->blockSignals(false); d->ui.kcfg_host->setCursorPosition(pos); d->resetAuthCapabilities(); if (d->encryptionGroup) { for (int i = 0; i < d->encryptionGroup->buttons().count(); ++i) { d->encryptionGroup->buttons().at(i)->setEnabled(true); } } } void SMTPConfigWidget::ensureValidAuthSelection() { Q_D(SMTPConfigWidget); // adjust available authentication methods d->updateAuthCapbilities(); } void SMTPConfigWidget::encryptionChanged(int enc) { Q_D(SMTPConfigWidget); qCDebug(MAILTRANSPORT_SMTP_LOG) << enc; // adjust port if (enc == Transport::EnumEncryption::SSL) { if (d->ui.kcfg_port->value() == SMTP_PORT) { d->ui.kcfg_port->setValue(SMTPS_PORT); } } else { if (d->ui.kcfg_port->value() == SMTPS_PORT) { d->ui.kcfg_port->setValue(SMTP_PORT); } } ensureValidAuthSelection(); } diff --git a/src/kmailtransport/plugins/smtp/smtpjob.cpp b/src/kmailtransport/plugins/smtp/smtpjob.cpp index 2f7f1da..fbb7472 100644 --- a/src/kmailtransport/plugins/smtp/smtpjob.cpp +++ b/src/kmailtransport/plugins/smtp/smtpjob.cpp @@ -1,360 +1,434 @@ /* 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 + +#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) { - startLoginJob(); + startPasswordRetrieval(); } startSendJob(); } } void SmtpJob::sessionStateChanged(KSmtp::Session::State state) { if (state == KSmtp::Session::Ready) { - startLoginJob(); + startPasswordRetrieval(); } else if (state == KSmtp::Session::Authenticated) { startSendJob(); } } +void SmtpJob::startPasswordRetrieval() +{ + if (!transport()->requiresAuthentication()) { + 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 (token.isEmpty() || token == tokens) { + requestToken(token); // if token == tokens, assume it's account password + } else { + startLoginJob(); + } + } + } 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) +{ + 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 + return; + } + + const auto account = authJob->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(transport()->password()); + 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::XOAuth); + 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::TlsV1); break; case Transport::EnumEncryption::SSL: login->setEncryptionMode(KSmtp::LoginJob::AnySslVersion); 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; } // 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 dc715c8..8e75302 100644 --- a/src/kmailtransport/plugins/smtp/smtpjob.h +++ b/src/kmailtransport/plugins/smtp/smtpjob.h @@ -1,82 +1,90 @@ /* 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 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(); + void requestToken(const QString &password = {}); + void refreshToken(const QString &token); + void onTokenRequestFinished(KGAPI2::Job *result); void startSmtpJob(); void startLoginJob(); void startSendJob(); private: friend class ::SmtpJobPrivate; SmtpJobPrivate *const d; }; } // namespace MailTransport #endif // MAILTRANSPORT_SMTPJOB_H diff --git a/src/kmailtransport/transport.cpp b/src/kmailtransport/transport.cpp index 52ce8d5..3c1a517 100644 --- a/src/kmailtransport/transport.cpp +++ b/src/kmailtransport/transport.cpp @@ -1,342 +1,344 @@ /* Copyright (c) 2006 - 2007 Volker Krause 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 "transport.h" #include "transport_p.h" #include "mailtransport_defs.h" #include "transportmanager.h" #include "transporttype_p.h" #include #include #include "mailtransport_debug.h" #include #include #include #include using namespace MailTransport; using namespace KWallet; Transport::Transport(const QString &cfgGroup) : TransportBase(cfgGroup) , d(new TransportPrivate) { qCDebug(MAILTRANSPORT_LOG) << cfgGroup; d->passwordLoaded = false; d->passwordDirty = false; d->storePasswordInFile = false; d->needsWalletMigration = false; d->passwordNeedsUpdateFromWallet = false; load(); } Transport::~Transport() { delete d; } bool Transport::isValid() const { return (id() > 0) && !host().isEmpty() && port() <= 65536; } QString Transport::password() { if (!d->passwordLoaded && requiresAuthentication() && storePassword() && d->password.isEmpty()) { readPassword(); } return d->password; } void Transport::setPassword(const QString &passwd) { d->passwordLoaded = true; if (d->password == passwd) { return; } d->passwordDirty = true; d->password = passwd; } void Transport::forceUniqueName() { QStringList existingNames; foreach (Transport *t, TransportManager::self()->transports()) { if (t->id() != id()) { existingNames << t->name(); } } int suffix = 1; QString origName = name(); while (existingNames.contains(name())) { setName(i18nc("%1: name; %2: number appended to it to make " "it unique among a list of names", "%1 #%2", origName, suffix)); ++suffix; } } void Transport::updatePasswordState() { Transport *original = TransportManager::self()->transportById(id(), false); if (original == this) { qCWarning(MAILTRANSPORT_LOG) << "Tried to update password state of non-cloned transport."; return; } if (original) { d->password = original->d->password; d->passwordLoaded = original->d->passwordLoaded; d->passwordDirty = original->d->passwordDirty; } else { qCWarning(MAILTRANSPORT_LOG) << "Transport with this ID not managed by transport manager."; } } bool Transport::isComplete() const { return !requiresAuthentication() || !storePassword() || d->passwordLoaded; } QString Transport::authenticationTypeString() const { return Transport::authenticationTypeString(authenticationType()); } QString Transport::authenticationTypeString(int type) { switch (type) { case EnumAuthenticationType::LOGIN: return QStringLiteral("LOGIN"); case EnumAuthenticationType::PLAIN: return QStringLiteral("PLAIN"); case EnumAuthenticationType::CRAM_MD5: return QStringLiteral("CRAM-MD5"); case EnumAuthenticationType::DIGEST_MD5: return QStringLiteral("DIGEST-MD5"); case EnumAuthenticationType::NTLM: return QStringLiteral("NTLM"); case EnumAuthenticationType::GSSAPI: return QStringLiteral("GSSAPI"); case EnumAuthenticationType::CLEAR: return i18nc("Authentication method", "Clear text"); case EnumAuthenticationType::APOP: return QStringLiteral("APOP"); case EnumAuthenticationType::ANONYMOUS: return i18nc("Authentication method", "Anonymous"); + case EnumAuthenticationType::XOAUTH2: + return QStringLiteral("XOAUTH2"); } Q_ASSERT(false); return QString(); } void Transport::usrRead() { TransportBase::usrRead(); setHost(host().trimmed()); if (d->oldName.isEmpty()) { d->oldName = name(); } // Set TransportType. { d->transportType = TransportType(); d->transportType.d->mIdentifier = identifier(); //qCDebug(MAILTRANSPORT_LOG) << "type" << identifier(); // Now we have the type and possibly agentType. Get the name, description // etc. from TransportManager. const TransportType::List &types = TransportManager::self()->types(); int index = types.indexOf(d->transportType); if (index != -1) { d->transportType = types[ index ]; } else { qCWarning(MAILTRANSPORT_LOG) << "Type unknown to manager."; d->transportType.d->mName = i18nc("An unknown transport type", "Unknown"); } } // we have everything we need if (!storePassword()) { return; } if (d->passwordLoaded) { if (d->passwordNeedsUpdateFromWallet) { d->passwordNeedsUpdateFromWallet = false; // read password if wallet is open, defer otherwise if (Wallet::isOpen(Wallet::NetworkWallet())) { // Don't read the password right away because this can lead // to reentrancy problems in KDBusServiceStarter when an application // run in Kontact creates the transports (due to a QEventLoop in the // synchronous KWallet openWallet call). QTimer::singleShot(0, this, &Transport::readPassword); } else { d->passwordLoaded = false; } } return; } // try to find a password in the config file otherwise KConfigGroup group(config(), currentGroup()); if (group.hasKey("password")) { d->password = KStringHandler::obscure(group.readEntry("password")); } else if (group.hasKey("password-kmail")) { //Legacy d->password = KStringHandler::obscure(group.readEntry("password-kmail")); } if (!d->password.isEmpty()) { d->passwordLoaded = true; if (Wallet::isEnabled()) { d->needsWalletMigration = true; } else { d->storePasswordInFile = true; } } } bool Transport::usrSave() { if (requiresAuthentication() && storePassword() && d->passwordDirty) { const QString storePassword = d->password; Wallet *wallet = TransportManager::self()->wallet(); if (!wallet || wallet->writePassword(QString::number(id()), d->password) != 0) { // wallet saving failed, ask if we should store in the config file instead if (d->storePasswordInFile || KMessageBox::warningYesNo( nullptr, i18n("KWallet is not available. It is strongly recommended to use " "KWallet for managing your passwords.\n" "However, the password can be stored in the configuration " "file instead. The password is stored in an obfuscated format, " "but should not be considered secure from decryption efforts " "if access to the configuration file is obtained.\n" "Do you want to store the password for server '%1' in the " "configuration file?", name()), i18n("KWallet Not Available"), KGuiItem(i18n("Store Password")), KGuiItem(i18n("Do Not Store Password"))) == KMessageBox::Yes) { // write to config file KConfigGroup group(config(), currentGroup()); group.writeEntry("password", KStringHandler::obscure(storePassword)); d->storePasswordInFile = true; } } d->passwordDirty = false; } if (!TransportBase::usrSave()) { return false; } TransportManager::self()->emitChangesCommitted(); if (name() != d->oldName) { emit TransportManager::self()->transportRenamed(id(), d->oldName, name()); d->oldName = name(); } return true; } void Transport::readPassword() { // no need to load a password if the account doesn't require auth if (!requiresAuthentication()) { return; } d->passwordLoaded = true; // check whether there is a chance to find our password at all if (Wallet::folderDoesNotExist(Wallet::NetworkWallet(), WALLET_FOLDER) || Wallet::keyDoesNotExist(Wallet::NetworkWallet(), WALLET_FOLDER, QString::number(id()))) { // try migrating password from kmail if (Wallet::folderDoesNotExist(Wallet::NetworkWallet(), KMAIL_WALLET_FOLDER) || Wallet::keyDoesNotExist(Wallet::NetworkWallet(), KMAIL_WALLET_FOLDER, QStringLiteral("transport-%1").arg(id()))) { return; } qCDebug(MAILTRANSPORT_LOG) << "migrating password from kmail wallet"; KWallet::Wallet *wallet = TransportManager::self()->wallet(); if (wallet) { QString pwd; wallet->setFolder(KMAIL_WALLET_FOLDER); if (wallet->readPassword(QStringLiteral("transport-%1").arg(id()), pwd) == 0) { setPassword(pwd); save(); } else { d->password.clear(); d->passwordLoaded = false; } wallet->removeEntry(QStringLiteral("transport-%1").arg(id())); wallet->setFolder(WALLET_FOLDER); } return; } // finally try to open the wallet and read the password KWallet::Wallet *wallet = TransportManager::self()->wallet(); if (wallet) { QString pwd; if (wallet->readPassword(QString::number(id()), pwd) == 0) { setPassword(pwd); } else { d->password.clear(); d->passwordLoaded = false; } } } bool Transport::needsWalletMigration() const { return d->needsWalletMigration; } void Transport::migrateToWallet() { qCDebug(MAILTRANSPORT_LOG) << "migrating" << id() << "to wallet"; d->needsWalletMigration = false; KConfigGroup group(config(), currentGroup()); group.deleteEntry("password"); group.deleteEntry("password-kmail"); d->passwordDirty = true; d->storePasswordInFile = false; save(); } Transport *Transport::clone() const { QString id = currentGroup().mid(10); return new Transport(id); } TransportType Transport::transportType() const { if (!d->transportType.isValid()) { qCWarning(MAILTRANSPORT_LOG) << "Invalid transport type."; } return d->transportType; } QByteArray Transport::toJson() const { //TODO create json info return {}; }