diff --git a/migration/googlegroupware/googleresourcemigrator.cpp b/migration/googlegroupware/googleresourcemigrator.cpp index 33c4b3cc2..ddd6ffad7 100644 --- a/migration/googlegroupware/googleresourcemigrator.cpp +++ b/migration/googlegroupware/googleresourcemigrator.cpp @@ -1,388 +1,388 @@ /* Copyright (c) 2020 Daniel Vrátil 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 "googleresourcemigrator.h" #include "googlesettingsinterface.h" #include "migration_debug.h" #include #include #include #include #include #include #include #include #include #include GoogleResourceMigrator::GoogleResourceMigrator() : MigratorBase(QLatin1String("googleresourcemigrator")) { } QString GoogleResourceMigrator::displayName() const { return i18nc("Name of the Migrator (intended for advanced users).", "Google Resource Migrator"); } QString GoogleResourceMigrator::description() const { return i18nc("Description of the migrator", "Migrates the old Google Calendar and Google Contacts resources to the new unified Google Groupware Resource"); } bool GoogleResourceMigrator::shouldAutostart() const { return true; } namespace { static const QStringView akonadiGoogleCalendarResource = {u"akonadi_googlecalendar_resource"}; static const QStringView akonadiGoogleContactsResource = {u"akonadi_googlecontacts_resource"}; static const QStringView akonadiGoogleGroupwareResource = {u"akonadi_google_resource"}; bool isLegacyGoogleResource(const Akonadi::AgentInstance &instance) { return instance.type().identifier() == akonadiGoogleCalendarResource || instance.type().identifier() == akonadiGoogleContactsResource; } bool isGoogleGroupwareResource(const Akonadi::AgentInstance &instance) { return instance.type().identifier() == akonadiGoogleGroupwareResource; } std::unique_ptr settingsForResource(const Akonadi::AgentInstance &instance) { Q_ASSERT(instance.isValid()); if (!instance.isValid()) { return {}; } const auto configFile = Akonadi::ServerManager::self()->addNamespace(instance.identifier()) + QStringLiteral("rc"); const auto configPath = QStandardPaths::locate(QStandardPaths::ConfigLocation, configFile); return std::unique_ptr{new QSettings{configPath, QSettings::IniFormat}}; } QString getAccountNameFromResourceSettings(const Akonadi::AgentInstance &instance) { Q_ASSERT(instance.isValid()); if (!instance.isValid()) { return {}; } const auto config = settingsForResource(instance); QString account = config->value(QStringLiteral("Account")).toString(); if (account.isEmpty()) { account = config->value(QStringLiteral("AccountName")).toString(); } return account; } static const auto WalletFolder = QStringLiteral("Akonadi Google"); std::unique_ptr getWallet() { std::unique_ptr wallet{KWallet::Wallet::openWallet(KWallet::Wallet::NetworkWallet(), 0, KWallet::Wallet::Synchronous)}; if (!wallet) { qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: failed to open KWallet."; return {}; } if (!wallet->hasFolder(WalletFolder)) { qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: couldn't find wallet folder for Google resources."; return {}; } wallet->setFolder(WalletFolder); return wallet; } QMap backupKWalletData(const QString &account) { qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: backing up KWallet data for" << account; const auto wallet = getWallet(); if (!wallet) { return {}; } if (!wallet->entryList().contains(account)) { qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: couldn't find KWallet data for account" << account; return {}; } QMap map; wallet->readMap(account, map); return map; } void restoreKWalletData(const QString &account, const QMap &data) { qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: restoring KWallet data for" << account; auto wallet = getWallet(); if (!wallet) { return; } wallet->writeMap(account, data); } void removeInstanceAndWait(const Akonadi::AgentInstance &instance) { // Make sure we wait for the resource to actually stop - otherwise we are risking // race when we restore the KWallet secrets from backup before the removed resource // actually tries to remove them from the wallet. const QString serviceName = Akonadi::ServerManager::agentServiceName(Akonadi::ServerManager::Resource, instance.identifier()); if (!QDBusConnection::sessionBus().interface()->isServiceRegistered(serviceName)) { Akonadi::AgentManager::self()->removeInstance(instance); } else { QDBusServiceWatcher watcher(Akonadi::ServerManager::agentServiceName(Akonadi::ServerManager::Resource, instance.identifier()), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForUnregistration); QEventLoop loop; QObject::connect(&watcher, &QDBusServiceWatcher::serviceUnregistered, &loop, [&loop, &instance]() { qCDebug(MIGRATION_LOG) << "GoogleResourceMigrator: resource" << instance.identifier() << "has disappeared from DBus"; loop.quit(); }); QTimer::singleShot(std::chrono::seconds(20), &loop, [&loop, &instance]() { qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: timeout while waiting for resource" << instance.identifier() << "to be removed"; loop.quit(); }); Akonadi::AgentManager::self()->removeInstance(instance); qCDebug(MIGRATION_LOG) << "GoogleResourceMigrator: waiting for" << instance.identifier() << "to disappear from DBus"; loop.exec(); } qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: removed the legacy calendar resource" << instance.identifier(); } } // namespace void GoogleResourceMigrator::startWork() { // Discover all existing Google Contacts and Google Calendar resources const auto allInstances = Akonadi::AgentManager::self()->instances(); for (const auto &instance : allInstances) { if (isLegacyGoogleResource(instance)) { const auto account = getAccountNameFromResourceSettings(instance); if (account.isEmpty()) { qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: resource" << instance.identifier() << "is not configued, removing"; Akonadi::AgentManager::self()->removeInstance(instance); continue; } qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: discovered resource" << instance.identifier() << "for account" << account; if (instance.type().identifier() == akonadiGoogleCalendarResource) { mMigrations[account].calendarResource = instance; } else if (instance.type().identifier() == akonadiGoogleContactsResource) { mMigrations[account].contactResource = instance; } } else if (isGoogleGroupwareResource(instance)) { const auto account = getAccountNameFromResourceSettings(instance); mMigrations[account].alreadyExists = true; } } mMigrationCount = mMigrations.size(); migrateNextAccount(); } void GoogleResourceMigrator::removeLegacyInstances(const QString &account, const Instances &instances) { // Legacy resources wipe KWallet data when removed, so we need to back the data up // before removing them and restore it afterwards const auto kwalletData = backupKWalletData(account); if (instances.calendarResource.isValid()) { removeInstanceAndWait(instances.calendarResource); } if (instances.contactResource.isValid()) { removeInstanceAndWait(instances.contactResource); } restoreKWalletData(account, kwalletData); } void GoogleResourceMigrator::migrateNextAccount() { setProgress((static_cast(mMigrationsDone) / mMigrationCount) * 100); if (mMigrations.empty()) { setMigrationState(MigratorBase::Complete); return; } QString account; Instances instances; std::tie(account, instances) = *mMigrations.constKeyValueBegin(); mMigrations.erase(mMigrations.begin()); if (instances.alreadyExists) { - message(Info, i18n("Google Groupware Resource for account %1 already exists, skipping.", account)); + Q_EMIT message(Info, i18n("Google Groupware Resource for account %1 already exists, skipping.", account)); // Just to be sure, check that there are no left-over legacy instances removeLegacyInstances(account, instances); ++mMigrationsDone; QMetaObject::invokeMethod(this, &GoogleResourceMigrator::migrateNextAccount, Qt::QueuedConnection); return; } qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: starting migration of account" << account; - message(Info, i18n("Starting migration of account %1", account)); + Q_EMIT message(Info, i18n("Starting migration of account %1", account)); qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: creating new" << akonadiGoogleGroupwareResource; - message(Info, i18n("Creating new instance of Google Gropware Resource")); + Q_EMIT message(Info, i18n("Creating new instance of Google Gropware Resource")); auto job = new Akonadi::AgentInstanceCreateJob(akonadiGoogleGroupwareResource.toString(), this); connect(job, &Akonadi::AgentInstanceCreateJob::finished, this, [this, job, account, instances](KJob *) { if (job->error()) { qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: Failed to create new Google Groupware Resource:" << job->errorString(); - message(Error, i18n("Failed to create a new Google Groupware Resource: %1", job->errorString())); + Q_EMIT message(Error, i18n("Failed to create a new Google Groupware Resource: %1", job->errorString())); setMigrationState(MigratorBase::Failed); return; } const auto newInstance = job->instance(); if (!migrateAccount(account, instances, newInstance)) { qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: failed to migrate account" << account; - message(Error, i18n("Failed to migrate account %1", account)); + Q_EMIT message(Error, i18n("Failed to migrate account %1", account)); setMigrationState(MigratorBase::Failed); return; } removeLegacyInstances(account, instances); // Reconfigure and restart the new instance newInstance.reconfigure(); newInstance.restart(); if (instances.calendarResource.isValid() ^ instances.contactResource.isValid()) { const auto res = instances.calendarResource.isValid() ? instances.calendarResource.identifier() : instances.contactResource.identifier(); qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: migrated configuration from" << res << "to" << newInstance.identifier(); } else { qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: migrated configuration from" << instances.calendarResource.identifier() << "and" << instances.contactResource.identifier() << "to" << newInstance.identifier(); } - message(Success, i18n("Migrated account %1 to new Google Groupware Resource", account)); + Q_EMIT message(Success, i18n("Migrated account %1 to new Google Groupware Resource", account)); ++mMigrationsDone; migrateNextAccount(); }); job->start(); } QString GoogleResourceMigrator::mergeAccountNames(const ResourceValues &accountName, const Instances &oldInstances) const { if (!accountName.calendar.isEmpty() && !accountName.contacts.isEmpty()) { if (accountName.calendar == accountName.contacts) { return accountName.calendar; } else { qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: account name mismatch:" << oldInstances.calendarResource.identifier() << "=" << accountName.calendar << "," << oldInstances.contactResource.identifier() << "=" << accountName.contacts << ". Ignoring both."; } } else if (!accountName.calendar.isEmpty()) { return accountName.calendar; } else if (!accountName.contacts.isEmpty()) { return accountName.contacts; } return {}; } int GoogleResourceMigrator::mergeAccountIds(const ResourceValues &accountId, const Instances &oldInstances) const { if (accountId.calendar > 0 && accountId.contacts > 0) { if (accountId.calendar == accountId.contacts) { return accountId.calendar; } else { qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: account id mismatch:" << oldInstances.calendarResource.identifier() << "=" << accountId.calendar << "," << oldInstances.contactResource.identifier() << "=" << accountId.contacts << ". Ignoring both."; } return 0; } // Return the non-zero entry return std::max(accountId.calendar, accountId.contacts); } bool GoogleResourceMigrator::migrateAccount(const QString &account, const Instances &oldInstances, const Akonadi::AgentInstance &newInstance) { org::kde::Akonadi::Google::Settings resourceSettings{ Akonadi::ServerManager::self()->agentServiceName(Akonadi::ServerManager::Resource, newInstance.identifier()), QStringLiteral("/Settings"), QDBusConnection::sessionBus()}; if (!resourceSettings.isValid()) { qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: failed to obtain settings DBus interface of " << newInstance.identifier(); return false; } resourceSettings.setAccount(account); ResourceValues accountName; ResourceValues accountId; ResourceValues enableIntervalCheck; ResourceValues intervalCheck{60, 60}; if (oldInstances.calendarResource.isValid()) { const auto calendarSettings = settingsForResource(oldInstances.calendarResource); // Calendar-specific resourceSettings.setCalendars(calendarSettings->value(QStringLiteral("Calendars")).toStringList()); resourceSettings.setTaskLists(calendarSettings->value(QStringLiteral("TaskLists")).toStringList()); resourceSettings.setEventsSince(calendarSettings->value(QStringLiteral("EventsSince")).toString()); enableIntervalCheck.calendar = calendarSettings->value(QStringLiteral("EnableIntervalCheck"), false).toBool(); intervalCheck.calendar = calendarSettings->value(QStringLiteral("IntervalCheckTime"), 60).toInt(); accountName.calendar = calendarSettings->value(QStringLiteral("AccountName")).toString(); accountId.calendar = calendarSettings->value(QStringLiteral("AccountId"), 0).toInt(); } if (oldInstances.contactResource.isValid()) { const auto contactsSettings = settingsForResource(oldInstances.contactResource); enableIntervalCheck.contacts = contactsSettings->value(QStringLiteral("EnableIntervalCheck"), false).toBool(); intervalCheck.contacts = contactsSettings->value(QStringLiteral("IntervalCheckTime"), 60).toInt(); accountName.contacts = contactsSettings->value(QStringLiteral("AccountName")).toString(); accountId.contacts = contactsSettings->value(QStringLiteral("AccountId"), 0).toInt(); } // And now some merging: resourceSettings.setEnableIntervalCheck(enableIntervalCheck.calendar || enableIntervalCheck.contacts); resourceSettings.setIntervalCheckTime(std::min(intervalCheck.calendar, intervalCheck.contacts)); resourceSettings.setAccountName(mergeAccountNames(accountName, oldInstances)); resourceSettings.setAccountId(mergeAccountIds(accountId, oldInstances)); resourceSettings.save(); return true; } diff --git a/resources/ews/ewsclient/auth/ewsoauth.cpp b/resources/ews/ewsclient/auth/ewsoauth.cpp index c5bf6f754..82ec3d4bb 100644 --- a/resources/ews/ewsclient/auth/ewsoauth.cpp +++ b/resources/ews/ewsclient/auth/ewsoauth.cpp @@ -1,494 +1,494 @@ /* Copyright (C) 2018 Krzysztof Nowicki 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 "ewsoauth.h" #include #ifdef EWSOAUTH_UNITTEST #include "ewsoauth_ut_mock.h" using namespace Mock; #else #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ewspkeyauthjob.h" #endif #include #include "ewsclient_debug.h" static const auto o365AuthorizationUrl = QUrl(QStringLiteral("https://login.microsoftonline.com/common/oauth2/authorize")); static const auto o365AccessTokenUrl = QUrl(QStringLiteral("https://login.microsoftonline.com/common/oauth2/token")); static const auto o365FakeUserAgent = QStringLiteral("Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36"); static const auto o365Resource = QStringLiteral("https%3A%2F%2Foutlook.office365.com%2F"); static const auto pkeyAuthSuffix = QStringLiteral(" PKeyAuth/1.0"); static const auto pkeyRedirectUri = QStringLiteral("urn:http-auth:PKeyAuth"); static const QString pkeyPasswordMapKey = QStringLiteral("pkey-password"); static const QString accessTokenMapKey = QStringLiteral("access-token"); static const QString refreshTokenMapKey = QStringLiteral("refresh-token"); class EwsOAuthUrlSchemeHandler final : public QWebEngineUrlSchemeHandler { Q_OBJECT public: EwsOAuthUrlSchemeHandler(QObject *parent = nullptr) : QWebEngineUrlSchemeHandler(parent) { } ~EwsOAuthUrlSchemeHandler() override = default; void requestStarted(QWebEngineUrlRequestJob *request) override; Q_SIGNALS: void returnUriReceived(const QUrl &url); }; class EwsOAuthReplyHandler final : public QAbstractOAuthReplyHandler { Q_OBJECT public: EwsOAuthReplyHandler(QObject *parent, const QString &returnUri) : QAbstractOAuthReplyHandler(parent) , mReturnUri(returnUri) { } ~EwsOAuthReplyHandler() override = default; QString callback() const override { return mReturnUri; } void networkReplyFinished(QNetworkReply *reply) override; Q_SIGNALS: void replyError(const QString &error); private: const QString mReturnUri; }; class EwsOAuthRequestInterceptor final : public QWebEngineUrlRequestInterceptor { Q_OBJECT public: EwsOAuthRequestInterceptor(QObject *parent, const QString &redirectUri) : QWebEngineUrlRequestInterceptor(parent) , mRedirectUri(redirectUri) { } ~EwsOAuthRequestInterceptor() override = default; void interceptRequest(QWebEngineUrlRequestInfo &info) override; Q_SIGNALS: void redirectUriIntercepted(const QUrl &url); private: const QString mRedirectUri; }; class EwsOAuthPrivate final : public QObject { Q_OBJECT public: EwsOAuthPrivate(EwsOAuth *parent, const QString &email, const QString &appId, const QString &redirectUri); ~EwsOAuthPrivate() override = default; bool authenticate(bool interactive); void modifyParametersFunction(QAbstractOAuth::Stage stage, QVariantMap *parameters); void authorizeWithBrowser(const QUrl &url); void redirectUriIntercepted(const QUrl &url); void granted(); void error(const QString &error, const QString &errorDescription, const QUrl &uri); QVariantMap queryToVarmap(const QUrl &url); void pkeyAuthResult(KJob *job); QWebEngineView mWebView; QWebEngineProfile mWebProfile; QWebEnginePage mWebPage; QOAuth2AuthorizationCodeFlow mOAuth2; EwsOAuthReplyHandler mReplyHandler; EwsOAuthRequestInterceptor mRequestInterceptor; EwsOAuthUrlSchemeHandler mSchemeHandler; QString mToken; const QString mEmail; const QString mRedirectUri; bool mAuthenticated; QPointer mWebDialog; QString mPKeyPassword; EwsOAuth *q_ptr = nullptr; Q_DECLARE_PUBLIC(EwsOAuth) }; void EwsOAuthUrlSchemeHandler::requestStarted(QWebEngineUrlRequestJob *request) { Q_EMIT returnUriReceived(request->requestUrl()); } void EwsOAuthReplyHandler::networkReplyFinished(QNetworkReply *reply) { if (reply->error() != QNetworkReply::NoError) { Q_EMIT replyError(reply->errorString()); return; } else if (reply->header(QNetworkRequest::ContentTypeHeader).isNull()) { Q_EMIT replyError(QStringLiteral("Empty or no Content-type header")); return; } const auto cth = reply->header(QNetworkRequest::ContentTypeHeader); const auto ct = cth.isNull() ? QStringLiteral("text/html") : cth.toString(); const auto data = reply->readAll(); if (data.isEmpty()) { Q_EMIT replyError(QStringLiteral("No data received")); return; } Q_EMIT replyDataReceived(data); QVariantMap tokens; if (ct.startsWith(QLatin1String("text/html")) || ct.startsWith(QLatin1String("application/x-www-form-urlencoded"))) { QUrlQuery q(QString::fromUtf8(data)); const auto items = q.queryItems(QUrl::FullyDecoded); for (const auto &it : items) { tokens.insert(it.first, it.second); } } else if (ct.startsWith(QLatin1String("application/json")) || ct.startsWith(QLatin1String("text/javascript"))) { const auto document = QJsonDocument::fromJson(data); if (!document.isObject()) { Q_EMIT replyError(QStringLiteral("Invalid JSON data received")); return; } const auto object = document.object(); if (object.isEmpty()) { Q_EMIT replyError(QStringLiteral("Empty JSON data received")); return; } tokens = object.toVariantMap(); } else { Q_EMIT replyError(QStringLiteral("Unknown content type")); return; } const auto error = tokens.value(QStringLiteral("error")); if (error.isValid()) { Q_EMIT replyError(QStringLiteral("Received error response: ") + error.toString()); return; } const auto accessToken = tokens.value(QStringLiteral("access_token")); if (!accessToken.isValid() || accessToken.toString().isEmpty()) { Q_EMIT replyError(QStringLiteral("Received empty or no access token")); return; } Q_EMIT tokensReceived(tokens); } void EwsOAuthRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo &info) { const auto url = info.requestUrl(); qCDebugNC(EWSCLI_LOG) << QStringLiteral("Intercepted browser navigation to ") << url; if ((url.toString(QUrl::RemoveQuery) == mRedirectUri) || (url.toString(QUrl::RemoveQuery) == pkeyRedirectUri) ) { qCDebug(EWSCLI_LOG) << QStringLiteral("Found redirect URI - blocking request"); Q_EMIT redirectUriIntercepted(url); info.block(true); } } EwsOAuthPrivate::EwsOAuthPrivate(EwsOAuth *parent, const QString &email, const QString &appId, const QString &redirectUri) : QObject(nullptr) , mWebView(nullptr) , mWebProfile() , mWebPage(&mWebProfile) , mReplyHandler(this, redirectUri) , mRequestInterceptor(this, redirectUri) , mEmail(email) , mRedirectUri(redirectUri) , mAuthenticated(false) , q_ptr(parent) { mOAuth2.setReplyHandler(&mReplyHandler); mOAuth2.setAuthorizationUrl(o365AuthorizationUrl); mOAuth2.setAccessTokenUrl(o365AccessTokenUrl); mOAuth2.setClientIdentifier(appId); mWebProfile.setUrlRequestInterceptor(&mRequestInterceptor); mWebProfile.installUrlSchemeHandler("urn", &mSchemeHandler); mWebView.setPage(&mWebPage); mOAuth2.setModifyParametersFunction([&](QAbstractOAuth::Stage stage, QVariantMap *parameters) { modifyParametersFunction(stage, parameters); }); connect(&mOAuth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &EwsOAuthPrivate::authorizeWithBrowser); connect(&mOAuth2, &QOAuth2AuthorizationCodeFlow::granted, this, &EwsOAuthPrivate::granted); connect(&mOAuth2, &QOAuth2AuthorizationCodeFlow::error, this, &EwsOAuthPrivate::error); connect(&mRequestInterceptor, &EwsOAuthRequestInterceptor::redirectUriIntercepted, this, &EwsOAuthPrivate::redirectUriIntercepted, Qt::QueuedConnection); connect(&mReplyHandler, &EwsOAuthReplyHandler::replyError, this, [this](const QString &err) { error(QStringLiteral("Network reply error"), err, QUrl()); }); } bool EwsOAuthPrivate::authenticate(bool interactive) { - Q_Q(EwsOAuth); + //Q_Q(EwsOAuth); qCInfoNC(EWSCLI_LOG) << QStringLiteral("Starting OAuth2 authentication"); if (!mOAuth2.refreshToken().isEmpty()) { mOAuth2.refreshAccessToken(); return true; } else if (interactive) { mOAuth2.grant(); return true; } else { return false; } } void EwsOAuthPrivate::modifyParametersFunction(QAbstractOAuth::Stage stage, QVariantMap *parameters) { switch (stage) { case QAbstractOAuth::Stage::RequestingAccessToken: parameters->insert(QStringLiteral("resource"), o365Resource); break; case QAbstractOAuth::Stage::RequestingAuthorization: parameters->insert(QStringLiteral("prompt"), QStringLiteral("login")); parameters->insert(QStringLiteral("login_hint"), mEmail); parameters->insert(QStringLiteral("resource"), o365Resource); break; default: break; } } void EwsOAuthPrivate::authorizeWithBrowser(const QUrl &url) { Q_Q(EwsOAuth); qCInfoNC(EWSCLI_LOG) << QStringLiteral("Launching browser for authentication"); /* Bad bad Microsoft... * When Conditional Access is enabled on the server the OAuth2 authentication server only supports Windows, * MacOSX, Android and iOS. No option to include Linux. Support (i.e. guarantee that it works) * is one thing, but blocking unsupported browsers completely is just wrong. * Fortunately enough this can be worked around by faking the user agent to something "supported". */ auto userAgent = o365FakeUserAgent; if (!q->mPKeyCertFile.isNull() && !q->mPKeyKeyFile.isNull()) { qCInfoNC(EWSCLI_LOG) << QStringLiteral("Found PKeyAuth certificates"); userAgent += pkeyAuthSuffix; } else { qCInfoNC(EWSCLI_LOG) << QStringLiteral("PKeyAuth certificates not found"); } mWebProfile.setHttpUserAgent(userAgent); mWebDialog = new QDialog(q->mAuthParentWidget); mWebDialog->setObjectName(QStringLiteral("Akonadi EWS Resource - Authentication")); mWebDialog->setWindowIcon(QIcon(QStringLiteral("akonadi-ews"))); mWebDialog->resize(400, 500); auto layout = new QHBoxLayout(mWebDialog); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(&mWebView); mWebView.show(); connect(mWebDialog.data(), &QDialog::rejected, this, [this]() { error(QStringLiteral("User cancellation"), QStringLiteral("The authentication browser was closed"), QUrl()); }); mWebView.load(url); mWebDialog->show(); } QVariantMap EwsOAuthPrivate::queryToVarmap(const QUrl &url) { QUrlQuery query(url); QVariantMap varmap; const auto items = query.queryItems(); for (const auto &item : items) { varmap[item.first] = item.second; } return varmap; } void EwsOAuthPrivate::redirectUriIntercepted(const QUrl &url) { qCDebugNC(EWSCLI_LOG) << QStringLiteral("Intercepted redirect URI from browser: ") << url; mWebView.stop(); mWebDialog->hide(); Q_Q(EwsOAuth); if (url.toString(QUrl::RemoveQuery) == pkeyRedirectUri) { qCDebugNC(EWSCLI_LOG) << QStringLiteral("Found PKeyAuth URI"); auto pkeyAuthJob = new EwsPKeyAuthJob(url, q->mPKeyCertFile, q->mPKeyKeyFile, mPKeyPassword, this); connect(pkeyAuthJob, &KJob::result, this, &EwsOAuthPrivate::pkeyAuthResult); pkeyAuthJob->start(); return; } Q_EMIT mOAuth2.authorizationCallbackReceived(queryToVarmap(url)); } void EwsOAuthPrivate::pkeyAuthResult(KJob *j) { EwsPKeyAuthJob *job = qobject_cast(j); qCDebugNC(EWSCLI_LOG) << QStringLiteral("PKeyAuth result: %1").arg(job->error()); QVariantMap varmap; if (job->error() == 0) { varmap = queryToVarmap(job->resultUri()); } else { varmap[QStringLiteral("error")] = job->errorString(); } Q_EMIT mOAuth2.authorizationCallbackReceived(varmap); } void EwsOAuthPrivate::granted() { Q_Q(EwsOAuth); qCInfoNC(EWSCLI_LOG) << QStringLiteral("Authentication succeeded"); mAuthenticated = true; QMap map; map[accessTokenMapKey] = mOAuth2.token(); map[refreshTokenMapKey] = mOAuth2.refreshToken(); Q_EMIT q->setWalletMap(map); Q_EMIT q->authSucceeded(); } void EwsOAuthPrivate::error(const QString &error, const QString &errorDescription, const QUrl &uri) { Q_Q(EwsOAuth); Q_UNUSED(uri); mAuthenticated = false; mOAuth2.setRefreshToken(QString()); qCInfoNC(EWSCLI_LOG) << QStringLiteral("Authentication failed: ") << error << errorDescription; Q_EMIT q->authFailed(error); } EwsOAuth::EwsOAuth(QObject *parent, const QString &email, const QString &appId, const QString &redirectUri) : EwsAbstractAuth(parent) , d_ptr(new EwsOAuthPrivate(this, email, appId, redirectUri)) { } EwsOAuth::~EwsOAuth() { } void EwsOAuth::init() { Q_EMIT requestWalletMap(); } bool EwsOAuth::getAuthData(QString &username, QString &password, QStringList &customHeaders) { Q_D(const EwsOAuth); Q_UNUSED(username); Q_UNUSED(password); if (d->mAuthenticated) { customHeaders.append(QStringLiteral("Authorization: Bearer ") + d->mOAuth2.token()); return true; } else { return false; } } void EwsOAuth::notifyRequestAuthFailed() { Q_D(EwsOAuth); d->mOAuth2.setToken(QString()); d->mAuthenticated = false; EwsAbstractAuth::notifyRequestAuthFailed(); } bool EwsOAuth::authenticate(bool interactive) { Q_D(EwsOAuth); return d->authenticate(interactive); } const QString &EwsOAuth::reauthPrompt() const { static const QString prompt = i18nc("@info", "Microsoft Exchange credentials for the account %1 are no longer valid. You need to authenticate in order to continue using it."); return prompt; } const QString &EwsOAuth::authFailedPrompt() const { static const QString prompt = i18nc("@info", "Failed to obtain credentials for Microsoft Exchange account %1. Please update it in the account settings page."); return prompt; } void EwsOAuth::walletPasswordRequestFinished(const QString &password) { Q_UNUSED(password); } void EwsOAuth::walletMapRequestFinished(const QMap &map) { Q_D(EwsOAuth); if (map.contains(pkeyPasswordMapKey)) { d->mPKeyPassword = map[pkeyPasswordMapKey]; } if (map.contains(refreshTokenMapKey)) { d->mOAuth2.setRefreshToken(map[refreshTokenMapKey]); } if (map.contains(accessTokenMapKey)) { d->mOAuth2.setToken(map[accessTokenMapKey]); d->mAuthenticated = true; Q_EMIT authSucceeded(); } else { Q_EMIT authFailed(QStringLiteral("Access token request failed")); } } #include "ewsoauth.moc" diff --git a/resources/ews/ewsmtaconfigdialog.cpp b/resources/ews/ewsmtaconfigdialog.cpp index 4e9752615..e1a7932f6 100644 --- a/resources/ews/ewsmtaconfigdialog.cpp +++ b/resources/ews/ewsmtaconfigdialog.cpp @@ -1,93 +1,93 @@ /* Copyright (C) 2015-2016 Krzysztof Nowicki 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 "ewsmtaconfigdialog.h" #include #include #include #include #include #include #include #include #include "ewsmtaresource.h" #include "ewsres_mta_debug.h" #include "ewsmtasettings.h" #include "ui_ewsmtaconfigdialog.h" EwsMtaConfigDialog::EwsMtaConfigDialog(EwsMtaResource *parentResource, WId wId) : QDialog() , mParentResource(parentResource) { if (wId) { setAttribute(Qt::WA_NativeWindow, true); KWindowSystem::setMainWindow(windowHandle(), wId); } QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout(this); mButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); mainLayout->addWidget(mainWidget); QPushButton *okButton = mButtonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(mButtonBox, &QDialogButtonBox::accepted, this, &EwsMtaConfigDialog::accept); connect(mButtonBox, &QDialogButtonBox::rejected, this, &EwsMtaConfigDialog::reject); mainLayout->addWidget(mButtonBox); setWindowTitle(i18nc("@title:window", "Microsoft Exchange Mail Transport Configuration")); mUi = new Ui::SetupServerView; mUi->setupUi(mainWidget); mUi->accountName->setText(parentResource->name()); Akonadi::AgentFilterProxyModel *model = mUi->resourceWidget->agentFilterProxyModel(); model->addCapabilityFilter(QStringLiteral("X-EwsMailTransport")); mUi->resourceWidget->view()->setSelectionMode(QAbstractItemView::SingleSelection); for (int i = 0, total = model->rowCount(); i < total; ++i) { QModelIndex index = model->index(i, 0); QVariant v = model->data(index, Akonadi::AgentInstanceModel::InstanceIdentifierRole); if (v.toString() == EwsMtaSettings::ewsResource()) { mUi->resourceWidget->view()->setCurrentIndex(index); } } connect(okButton, &QPushButton::clicked, this, &EwsMtaConfigDialog::save); } EwsMtaConfigDialog::~EwsMtaConfigDialog() { delete mUi; } void EwsMtaConfigDialog::save() { if (!mUi->resourceWidget->selectedAgentInstances().isEmpty()) { - EwsMtaSettings::setEwsResource(mUi->resourceWidget->selectedAgentInstances().first().identifier()); + EwsMtaSettings::setEwsResource(mUi->resourceWidget->selectedAgentInstances().constFirst().identifier()); mParentResource->setName(mUi->accountName->text()); EwsMtaSettings::self()->save(); } else { qCWarning(EWSRES_MTA_LOG) << "Any agent instance selected"; } } diff --git a/resources/google-groupware/contacthandler.cpp b/resources/google-groupware/contacthandler.cpp index 2c5a4d399..a56b42b57 100644 --- a/resources/google-groupware/contacthandler.cpp +++ b/resources/google-groupware/contacthandler.cpp @@ -1,487 +1,487 @@ /* Copyright (C) 2011-2013 Daniel Vrátil 2020 Igor Poboiko This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "contacthandler.h" #include "googleresource.h" #include "googlesettings.h" #include "googlecontacts_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define OTHERCONTACTS_REMOTEID QStringLiteral("OtherContacts") using namespace KGAPI2; using namespace Akonadi; QString ContactHandler::mimeType() { return KContacts::Addressee::mimeType(); } bool ContactHandler::canPerformTask(const Item &item) { return GenericHandler::canPerformTask(item); } bool ContactHandler::canPerformTask(const Item::List &items) { return GenericHandler::canPerformTask(items); } QString ContactHandler::myContactsRemoteId() const { return QStringLiteral("http://www.google.com/m8/feeds/groups/%1/base/6").arg(QString::fromLatin1(QUrl::toPercentEncoding(m_settings->accountPtr()->accountName()))); } void ContactHandler::setupCollection(Collection &collection, const ContactsGroupPtr &group) { collection.setContentMimeTypes({ mimeType() }); collection.setName(group->id()); collection.setRemoteId(group->id()); QString realName = group->title(); if (group->isSystemGroup()) { if (realName.contains(QLatin1String("Coworkers"))) { realName = i18nc("Name of a group of contacts", "Coworkers"); } else if (realName.contains(QLatin1String("Friends"))) { realName = i18nc("Name of a group of contacts", "Friends"); } else if (realName.contains(QLatin1String("Family"))) { realName = i18nc("Name of a group of contacts", "Family"); } else if (realName.contains(QLatin1String("My Contacts"))) { realName = i18nc("Name of a group of contacts", "My Contacts"); } } // "My Contacts" is the only one not virtual if (group->id() == myContactsRemoteId()) { collection.setRights(Collection::CanCreateItem |Collection::CanChangeItem |Collection::CanDeleteItem); } else { collection.setRights(Collection::CanLinkItem |Collection::CanUnlinkItem |Collection::CanChangeItem); collection.setVirtual(true); if (!group->isSystemGroup()) { collection.setRights(collection.rights() |Collection::CanChangeCollection |Collection::CanDeleteCollection); } } auto attr = collection.attribute(Collection::AddIfMissing); attr->setDisplayName(realName); attr->setIconName(QStringLiteral("view-pim-contacts")); } void ContactHandler::retrieveCollections(const Collection &rootCollection) { m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Retrieving contacts groups")); qCDebug(GOOGLE_CONTACTS_LOG) << "Retrieving contacts groups..."; m_collections.clear(); Collection otherCollection; otherCollection.setContentMimeTypes({ mimeType() }); otherCollection.setName(i18n("Other Contacts")); otherCollection.setParentCollection(rootCollection); otherCollection.setRights(Collection::CanCreateItem |Collection::CanChangeItem |Collection::CanDeleteItem); otherCollection.setRemoteId(OTHERCONTACTS_REMOTEID); auto attr = otherCollection.attribute(Collection::AddIfMissing); attr->setDisplayName(i18n("Other Contacts")); attr->setIconName(QStringLiteral("view-pim-contacts")); m_iface->collectionsRetrieved({ otherCollection }); m_collections[ OTHERCONTACTS_REMOTEID ] = otherCollection; auto job = new ContactsGroupFetchJob(m_settings->accountPtr(), this); connect(job, &ContactFetchJob::finished, this, [this, rootCollection](KGAPI2::Job *job){ if (!m_iface->handleError(job)) { return; } qCDebug(GOOGLE_CONTACTS_LOG) << "Contacts groups retrieved"; const ObjectsList objects = qobject_cast(job)->items(); Collection::List collections; collections.reserve(objects.count()); std::transform(objects.cbegin(), objects.cend(), std::back_inserter(collections), [this, &rootCollection](const ObjectPtr &object){ const ContactsGroupPtr group = object.dynamicCast(); qCDebug(GOOGLE_CONTACTS_LOG) << " -" << group->title() << "(" << group->id() << ")"; Collection collection; setupCollection(collection, group); collection.setParentCollection(rootCollection); m_collections[ collection.remoteId() ] = collection; return collection; }); m_iface->collectionsRetrievedFromHandler(collections); }); } void ContactHandler::retrieveItems(const Collection &collection) { // Contacts are stored inside "My Contacts" and "Other Contacts" only if ((collection.remoteId() != OTHERCONTACTS_REMOTEID) && (collection.remoteId() != myContactsRemoteId())) { m_iface->itemsRetrievalDone(); return; } m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Retrieving contacts for group '%1'", collection.displayName())); qCDebug(GOOGLE_CONTACTS_LOG) << "Retreiving contacts for group" << collection.remoteId() << "..."; auto job = new ContactFetchJob(m_settings->accountPtr(), this); if (!collection.remoteRevision().isEmpty()) { job->setFetchOnlyUpdated(collection.remoteRevision().toLongLong()); job->setFetchDeleted(true); } else { // No need to fetch deleted items for a non-incremental update job->setFetchDeleted(false); } connect(job, &ContactFetchJob::finished, this, &ContactHandler::slotItemsRetrieved); } void ContactHandler::slotItemsRetrieved(KGAPI2::Job *job) { if (!m_iface->handleError(job)) { return; } Collection collection = m_iface->currentCollection(); Item::List changedItems, removedItems; QHash groupsMap; QStringList changedPhotos; auto fetchJob = qobject_cast(job); bool isIncremental = (fetchJob->fetchOnlyUpdated() > 0); const ObjectsList objects = fetchJob->items(); qCDebug(GOOGLE_CONTACTS_LOG) << "Retrieved" << objects.count() << "contacts"; for (const ObjectPtr &object : objects) { const ContactPtr contact = object.dynamicCast(); Item item; item.setMimeType(mimeType()); item.setParentCollection(collection); item.setRemoteId(contact->uid()); item.setRemoteRevision(contact->etag()); item.setPayload(*contact.dynamicCast()); if (contact->deleted() || (collection.remoteId() == OTHERCONTACTS_REMOTEID && !contact->groups().isEmpty()) || (collection.remoteId() == myContactsRemoteId() && contact->groups().isEmpty())) { qCDebug(GOOGLE_CONTACTS_LOG) << " - removed" << contact->uid(); removedItems << item; } else { qCDebug(GOOGLE_CONTACTS_LOG) << " - changed" << contact->uid(); changedItems << item; changedPhotos << contact->uid(); } const QStringList groups = contact->groups(); for (const QString &group : groups) { // We don't link contacts to "My Contacts" if (group != myContactsRemoteId()) { groupsMap[group] << item; } } } if (isIncremental) { m_iface->itemsRetrievedIncremental(changedItems, removedItems); } else { m_iface->itemsRetrieved(changedItems); } for (auto iter = groupsMap.constBegin(), iterEnd = groupsMap.constEnd(); iter != iterEnd; ++iter) { new LinkJob(m_collections[iter.key()], iter.value(), this); } // TODO: unlink if the group was removed! if (!changedPhotos.isEmpty()) { QVariantMap map; map[QStringLiteral("collection")] = QVariant::fromValue(collection); map[QStringLiteral("modified")] = QVariant::fromValue(changedPhotos); m_iface->scheduleCustomTask(this, "retrieveContactsPhotos", map); } const QDateTime local(QDateTime::currentDateTime()); const QDateTime UTC(local.toUTC()); collection.setRemoteRevision(QString::number(UTC.toSecsSinceEpoch())); new CollectionModifyJob(collection, this); emitReadyStatus(); } void ContactHandler::retrieveContactsPhotos(const QVariant &argument) { if (!m_iface->canPerformTask()) { return; } const auto map = argument.value(); const auto collection = map[QStringLiteral("collection")].value(); const auto changedPhotos = map[QStringLiteral("modified")].toStringList(); m_iface->emitStatus(AgentBase::Running, i18ncp("@info:status", "Retrieving %1 contact photo for group '%2'", "Retrieving %1 contact photos for group '%2'", changedPhotos.count(), collection.displayName())); Item::List items; items.reserve(changedPhotos.size()); std::transform(changedPhotos.cbegin(), changedPhotos.cend(), std::back_inserter(items), [](const QString &contact){ Item item; item.setRemoteId(contact); return item; }); auto job = new ItemFetchJob(items, this); job->setCollection(collection); job->fetchScope().fetchFullPayload(true); connect(job, &ItemFetchJob::finished, this, &ContactHandler::slotUpdatePhotosItemsRetrieved); } void ContactHandler::slotUpdatePhotosItemsRetrieved(KJob *job) { // Make sure account is still valid if (!m_iface->canPerformTask()) { return; } const Item::List items = qobject_cast(job)->items(); qCDebug(GOOGLE_CONTACTS_LOG) << "Fetched" << items.count() << "contacts for photo update"; ContactsList contacts; contacts.reserve(items.size()); std::transform(items.cbegin(), items.cend(), std::back_inserter(contacts), [](const Item &item){ return ContactPtr(new Contact(item.payload())); }); auto photoJob = new ContactFetchPhotoJob(contacts, m_settings->accountPtr(), this); photoJob->setProperty("processedItems", 0); connect(photoJob, &ContactFetchPhotoJob::photoFetched, this, [this, items](KGAPI2::Job *job, const ContactPtr &contact){ qCDebug(GOOGLE_CONTACTS_LOG) << " - fetched photo for contact" << contact->uid(); int processedItems = job->property("processedItems").toInt(); processedItems++; job->setProperty("processedItems", processedItems); m_iface->emitPercent(100.0f*processedItems / items.count()); auto it = std::find_if(items.cbegin(), items.cend(), [&contact](const Item &item){ return item.remoteId() == contact->uid(); }); if (it != items.cend()) { Item newItem(*it); newItem.setPayload(*contact.dynamicCast()); new ItemModifyJob(newItem, this); } }); connect(photoJob, &ContactFetchPhotoJob::finished, this, &ContactHandler::slotGenericJobFinished); } void ContactHandler::itemAdded(const Item &item, const Collection &collection) { m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Adding contact to group '%1'", collection.displayName())); ContactPtr contact(new Contact(item.payload())); qCDebug(GOOGLE_CONTACTS_LOG) << "Creating contact"; if (collection.remoteId() == myContactsRemoteId()) { contact->addGroup(myContactsRemoteId()); } auto job = new ContactCreateJob(contact, m_settings->accountPtr(), this); connect(job, &ContactCreateJob::finished, this, [this, item](KGAPI2::Job *job){ if (!m_iface->handleError(job)) { return; } ContactPtr contact = qobject_cast(job)->items().first().dynamicCast(); Item newItem = item; qCDebug(GOOGLE_CONTACTS_LOG) << "Contact" << contact->uid() << "created"; newItem.setRemoteId(contact->uid()); newItem.setRemoteRevision(contact->etag()); m_iface->itemChangeCommitted(newItem); newItem.setPayload(*contact.dynamicCast()); new ItemModifyJob(newItem, this); emitReadyStatus(); }); } void ContactHandler::itemChanged(const Item &item, const QSet< QByteArray > & /*partIdentifiers*/) { m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Changing contact")); qCDebug(GOOGLE_CONTACTS_LOG) << "Changing contact" << item.remoteId(); ContactPtr contact(new Contact(item.payload())); auto job = new ContactModifyJob(contact, m_settings->accountPtr(), this); job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item)); connect(job, &ContactModifyJob::finished, this, &ContactHandler::slotGenericJobFinished); } void ContactHandler::itemsRemoved(const Item::List &items) { m_iface->emitStatus(AgentBase::Running, i18ncp("@info:status", "Removing %1 contact", "Removing %1 contacts", items.count())); QStringList contactIds; contactIds.reserve(items.count()); std::transform(items.cbegin(), items.cend(), std::back_inserter(contactIds), [](const Item &item){ return item.remoteId(); }); qCDebug(GOOGLE_CONTACTS_LOG) << "Removing contacts" << contactIds; auto job = new ContactDeleteJob(contactIds, m_settings->accountPtr(), this); job->setProperty(ITEMS_PROPERTY, QVariant::fromValue(items)); connect(job, &ContactDeleteJob::finished, this, &ContactHandler::slotGenericJobFinished); } void ContactHandler::itemsMoved(const Item::List &items, const Collection &collectionSource, const Collection &collectionDestination) { qCDebug(GOOGLE_CONTACTS_LOG) << "Moving contacts from" << collectionSource.remoteId() << "to" << collectionDestination.remoteId(); if (!(((collectionSource.remoteId() == myContactsRemoteId()) && (collectionDestination.remoteId() == OTHERCONTACTS_REMOTEID)) || ((collectionSource.remoteId() == OTHERCONTACTS_REMOTEID) && (collectionDestination.remoteId() == myContactsRemoteId())))) { m_iface->cancelTask(i18n("Invalid source or destination collection")); } m_iface->emitStatus(AgentBase::Running, i18ncp("@info:status", "Moving %1 contact from group '%2' to '%3'", "Moving %1 contacts from group '%2' to '%3'", items.count(), collectionSource.remoteId(), collectionDestination.remoteId())); ContactsList contacts; contacts.reserve(items.count()); std::transform(items.cbegin(), items.cend(), std::back_inserter(contacts), [this, &collectionSource, &collectionDestination](const Item &item){ ContactPtr contact(new Contact(item.payload())); // MyContacts -> OtherContacts if (collectionSource.remoteId() == myContactsRemoteId() && collectionDestination.remoteId() == OTHERCONTACTS_REMOTEID) { contact->clearGroups(); // OtherContacts -> MyContacts } else if (collectionSource.remoteId() == OTHERCONTACTS_REMOTEID && collectionDestination.remoteId() == myContactsRemoteId()) { contact->addGroup(myContactsRemoteId()); } return contact; }); qCDebug(GOOGLE_CONTACTS_LOG) << "Moving contacts from" << collectionSource.remoteId() << "to" << collectionDestination.remoteId(); auto job = new ContactModifyJob(contacts, m_settings->accountPtr(), this); job->setProperty(ITEMS_PROPERTY, QVariant::fromValue(items)); connect(job, &ContactModifyJob::finished, this, &ContactHandler::slotGenericJobFinished); } void ContactHandler::itemsLinked(const Item::List &items, const Collection &collection) { m_iface->emitStatus(AgentBase::Running, i18ncp("@info:status", "Linking %1 contact", "Linking %1 contacts", items.count())); qCDebug(GOOGLE_CONTACTS_LOG) << "Linking" << items.count() << "contacts to group" << collection.remoteId(); ContactsList contacts; contacts.reserve(items.count()); std::transform(items.cbegin(), items.cend(), std::back_inserter(contacts), [this, &collection](const Item &item){ ContactPtr contact(new Contact(item.payload())); contact->addGroup(collection.remoteId()); return contact; }); auto job = new ContactModifyJob(contacts, m_settings->accountPtr(), this); job->setProperty(ITEMS_PROPERTY, QVariant::fromValue(items)); connect(job, &ContactModifyJob::finished, this, &ContactHandler::slotGenericJobFinished); } void ContactHandler::itemsUnlinked(const Item::List &items, const Collection &collection) { m_iface->emitStatus(AgentBase::Running, i18ncp("@info:status", "Unlinking %1 contact", "Unlinking %1 contacts", items.count())); qCDebug(GOOGLE_CONTACTS_LOG) << "Unlinking" << items.count() << "contacts from group" << collection.remoteId(); ContactsList contacts; contacts.reserve(items.count()); std::transform(items.cbegin(), items.cend(), std::back_inserter(contacts), - [this, &collection](const Item &item){ + [&collection](const Item &item){ ContactPtr contact(new Contact(item.payload())); contact->removeGroup(collection.remoteId()); return contact; }); auto job = new ContactModifyJob(contacts, m_settings->accountPtr(), this); job->setProperty(ITEMS_PROPERTY, QVariant::fromValue(items)); connect(job, &ContactModifyJob::finished, this, &ContactHandler::slotGenericJobFinished); } void ContactHandler::collectionAdded(const Collection &collection, const Collection & /*parent*/) { m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Creating new contact group '%1'", collection.displayName())); qCDebug(GOOGLE_CONTACTS_LOG) << "Adding contact group" << collection.displayName(); ContactsGroupPtr group(new ContactsGroup); group->setTitle(collection.name()); group->setIsSystemGroup(false); auto job = new ContactsGroupCreateJob(group, m_settings->accountPtr(), this); connect(job, &ContactsGroupCreateJob::finished, this, [this, collection](KGAPI2::Job *job){ if (!m_iface->handleError(job)) { return; } ContactsGroupPtr group = qobject_cast(job)->items().first().dynamicCast(); qCDebug(GOOGLE_CONTACTS_LOG) << "Contact group created:" << group->id(); Collection newCollection(collection); setupCollection(newCollection, group); m_collections[ newCollection.remoteId() ] = newCollection; m_iface->collectionChangeCommitted(newCollection); emitReadyStatus(); }); } void ContactHandler::collectionChanged(const Collection &collection) { m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Changing contact group '%1'", collection.displayName())); qCDebug(GOOGLE_CONTACTS_LOG) << "Changing contact group" << collection.remoteId(); ContactsGroupPtr group(new ContactsGroup()); group->setId(collection.remoteId()); group->setTitle(collection.displayName()); auto job = new ContactsGroupModifyJob(group, m_settings->accountPtr(), this); job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection)); connect(job, &ContactsGroupModifyJob::finished, this, &ContactHandler::slotGenericJobFinished); } void ContactHandler::collectionRemoved(const Collection &collection) { m_iface->emitStatus(AgentBase::Running, i18nc("@info:status", "Removing contact group '%1'", collection.displayName())); qCDebug(GOOGLE_CONTACTS_LOG) << "Removing contact group" << collection.remoteId(); auto job = new ContactsGroupDeleteJob(collection.remoteId(), m_settings->accountPtr(), this); job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection)); connect(job, &ContactsGroupDeleteJob::finished, this, &ContactHandler::slotGenericJobFinished); } diff --git a/resources/google-groupware/googleresourcestate.cpp b/resources/google-groupware/googleresourcestate.cpp index 91d0cabc8..c40010bfa 100644 --- a/resources/google-groupware/googleresourcestate.cpp +++ b/resources/google-groupware/googleresourcestate.cpp @@ -1,179 +1,179 @@ /* Copyright (c) 2020 Igor Poboiko 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 "googleresourcestate.h" #include "googleresource.h" using namespace Akonadi; GoogleResourceState::GoogleResourceState(GoogleResource *resource) : m_resource(resource) { } // Items handling void GoogleResourceState::itemRetrieved(const Item &item) { m_resource->itemRetrieved(item); } void GoogleResourceState::itemsRetrieved(const Item::List &items) { m_resource->itemsRetrieved(items); } void GoogleResourceState::itemsRetrievedIncremental(const Item::List &changed, const Item::List &removed) { m_resource->itemsRetrievedIncremental(changed, removed); } void GoogleResourceState::itemsRetrievalDone() { m_resource->itemsRetrievalDone(); } void GoogleResourceState::setTotalItems(int amount) { m_resource->setTotalItems(amount); } void GoogleResourceState::itemChangeCommitted(const Item &item) { m_resource->changeCommitted(item); } void GoogleResourceState::itemsChangesCommitted(const Item::List &items) { m_resource->changesCommitted(items); } Item::List GoogleResourceState::currentItems() { return m_resource->currentItems(); } // Collections handling void GoogleResourceState::collectionsRetrieved(const Collection::List &collections) { m_resource->collectionsRetrieved(collections); } void GoogleResourceState::collectionAttributesRetrieved(const Collection &collection) { m_resource->collectionAttributesRetrieved(collection); } void GoogleResourceState::collectionChangeCommitted(const Collection &collection) { m_resource->changeCommitted(collection); } void GoogleResourceState::collectionsRetrievedFromHandler(const Collection::List &collections) { m_resource->collectionsRetrievedFromHandler(collections); } Collection GoogleResourceState::currentCollection() { return m_resource->currentCollection(); } // Tags handling void GoogleResourceState::tagsRetrieved(const Tag::List &tags, const QHash &tagMembers) { m_resource->tagsRetrieved(tags, tagMembers); } void GoogleResourceState::tagChangeCommitted(const Tag &tag) { m_resource->changeCommitted(tag); } // Relations handling void GoogleResourceState::relationsRetrieved(const Relation::List &relations) { m_resource->relationsRetrieved(relations); } // FreeBusy handling void GoogleResourceState::freeBusyRetrieved(const QString &email, const QString &freeBusy, bool success, const QString &errorText = QString()) { m_resource->freeBusyRetrieved(email, freeBusy, success, errorText); } void GoogleResourceState::handlesFreeBusy(const QString &email, bool handles) { m_resource->handlesFreeBusy(email, handles); } // Result reporting void GoogleResourceState::changeProcessed() { m_resource->changeProcessed(); } void GoogleResourceState::cancelTask(const QString &errorString) { m_resource->cancelTask(errorString); } void GoogleResourceState::deferTask() { m_resource->deferTask(); } void GoogleResourceState::taskDone() { m_resource->taskDone(); } void GoogleResourceState::emitStatus(int status, const QString &message) { Q_EMIT m_resource->status(status, message); } void GoogleResourceState::emitError(const QString &message) { Q_EMIT m_resource->error(message); } void GoogleResourceState::emitWarning(const QString &message) { Q_EMIT m_resource->warning(message); } void GoogleResourceState::emitPercent(int percent) { Q_EMIT m_resource->percent(percent); } bool GoogleResourceState::canPerformTask() { return m_resource->canPerformTask(); } bool GoogleResourceState::handleError(KGAPI2::Job *job, bool _cancelTask) { return m_resource->handleError(job, _cancelTask); } void GoogleResourceState::scheduleCustomTask(QObject *receiver, const char *method, const QVariant &argument) { - return m_resource->scheduleCustomTask(receiver, method, argument); + m_resource->scheduleCustomTask(receiver, method, argument); } diff --git a/resources/mbox/compactpage.cpp b/resources/mbox/compactpage.cpp index 28a686530..4b39e0e15 100644 --- a/resources/mbox/compactpage.cpp +++ b/resources/mbox/compactpage.cpp @@ -1,139 +1,139 @@ /* Copyright (c) 2009 Bertjan Broeksema 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 "compactpage.h" #include #include #include #include #include #include "deleteditemsattribute.h" #include using namespace Akonadi; CompactPage::CompactPage(const QString &collectionId, QWidget *parent) : QWidget(parent) , mCollectionId(collectionId) { ui.setupUi(this); connect(ui.compactButton, &QPushButton::clicked, this, &CompactPage::compact); checkCollectionId(); } void CompactPage::checkCollectionId() { if (!mCollectionId.isEmpty()) { Collection collection; collection.setRemoteId(mCollectionId); CollectionFetchJob *fetchJob = new CollectionFetchJob(collection, CollectionFetchJob::Base); connect(fetchJob, &CollectionFetchJob::result, this, &CompactPage::onCollectionFetchCheck); } } void CompactPage::compact() { ui.compactButton->setEnabled(false); Collection collection; collection.setRemoteId(mCollectionId); CollectionFetchJob *fetchJob = new CollectionFetchJob(collection, CollectionFetchJob::Base); connect(fetchJob, &CollectionFetchJob::result, this, &CompactPage::onCollectionFetchCompact); } void CompactPage::onCollectionFetchCheck(KJob *job) { if (job->error()) { // If we cannot fetch the collection, than also disable compacting. ui.compactButton->setEnabled(false); return; } CollectionFetchJob *fetchJob = dynamic_cast(job); Q_ASSERT(fetchJob); Q_ASSERT(fetchJob->collections().size() == 1); Collection mboxCollection = fetchJob->collections().at(0); DeletedItemsAttribute *attr = mboxCollection.attribute(Akonadi::Collection::AddIfMissing); if (!attr->deletedItemOffsets().isEmpty()) { ui.compactButton->setEnabled(true); ui.messageLabel->setText(i18np("(1 message marked for deletion)", "(%1 messages marked for deletion)", attr->deletedItemOffsets().size())); } } void CompactPage::onCollectionFetchCompact(KJob *job) { if (job->error()) { ui.messageLabel->setText(i18n("Failed to fetch the collection.")); ui.compactButton->setEnabled(true); return; } - CollectionFetchJob *fetchJob = dynamic_cast(job); + CollectionFetchJob *fetchJob = qobject_cast(job); Q_ASSERT(fetchJob); Q_ASSERT(fetchJob->collections().size() == 1); Collection mboxCollection = fetchJob->collections().at(0); DeletedItemsAttribute *attr = mboxCollection.attribute(Akonadi::Collection::AddIfMissing); KMBox::MBox mbox; // TODO: Set lock method. const QString fileName = QUrl::fromLocalFile(mCollectionId).toLocalFile(); if (!mbox.load(fileName)) { ui.messageLabel->setText(i18n("Failed to load the mbox file")); } else { ui.messageLabel->setText(i18np("(Deleting 1 message)", "(Deleting %1 messages)", attr->offsetCount())); // TODO: implement and connect to messageProcessed signal. if (mbox.purge(attr->deletedItemEntries()) || (QFileInfo(fileName).size() == 0)) { // even if purge() failed but the file is now empty. // it was probably deleted/emptied by an external prog. For whatever reason // doesn't matter here. We know the file is empty so we can get rid // of our stored DeletedItemsAttribute mboxCollection.removeAttribute(); CollectionModifyJob *modifyJob = new CollectionModifyJob(mboxCollection); connect(modifyJob, &CollectionModifyJob::result, this, &CompactPage::onCollectionModify); } else { ui.messageLabel->setText(i18n("Failed to compact the mbox file.")); } } } void CompactPage::onCollectionModify(KJob *job) { if (job->error()) { ui.messageLabel->setText(i18n("Failed to compact the mbox file.")); } else { ui.messageLabel->setText(i18n("MBox file compacted.")); } } diff --git a/resources/pop3/autotests/pop3test.cpp b/resources/pop3/autotests/pop3test.cpp index 2198b3e80..3c0e09426 100644 --- a/resources/pop3/autotests/pop3test.cpp +++ b/resources/pop3/autotests/pop3test.cpp @@ -1,962 +1,964 @@ /* Copyright 2009 Thomas McGuire 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 ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), 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 "pop3test.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include QTEST_AKONADIMAIN(Pop3Test) using namespace Akonadi; constexpr int serverSettleTimeout = 200; /* ms */ void Pop3Test::initTestCase() { AkonadiTest::checkTestIsIsolated(); QVERIFY(Akonadi::Control::start()); // switch all resources offline to reduce interference from them foreach (Akonadi::AgentInstance agent, Akonadi::AgentManager::self()->instances()) { agent.setIsOnline(false); } /* qDebug() << "==========================================================="; qDebug() << "============ Stopping for debugging ======================="; qDebug() << "==========================================================="; kill( qApp->applicationPid(), SIGSTOP ); */ // // Create the maildir and pop3 resources // AgentType maildirType = AgentManager::self()->type(QStringLiteral("akonadi_maildir_resource")); AgentInstanceCreateJob *agentCreateJob = new AgentInstanceCreateJob(maildirType); QVERIFY(agentCreateJob->exec()); mMaildirIdentifier = agentCreateJob->instance().identifier(); AgentType popType = AgentManager::self()->type(QStringLiteral("akonadi_pop3_resource")); agentCreateJob = new AgentInstanceCreateJob(popType); QVERIFY(agentCreateJob->exec()); mPop3Identifier = agentCreateJob->instance().identifier(); // // Configure the maildir resource // QString maildirRootPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + QLatin1String("tester"); mMaildirPath = maildirRootPath + QLatin1String("/new"); QDir::current().mkpath(mMaildirPath); QDir::current().mkpath(maildirRootPath + QLatin1String("/tmp")); QString service = QLatin1String("org.freedesktop.Akonadi.Resource.") + mMaildirIdentifier; if (Akonadi::ServerManager::hasInstanceIdentifier()) { service += QLatin1Char('.') + Akonadi::ServerManager::instanceIdentifier(); } mMaildirSettingsInterface = new OrgKdeAkonadiMaildirSettingsInterface( service, QStringLiteral("/Settings"), QDBusConnection::sessionBus(), this); QDBusReply setPathReply = mMaildirSettingsInterface->setPath(maildirRootPath); QVERIFY(setPathReply.isValid()); AgentManager::self()->instance(mMaildirIdentifier).reconfigure(); QDBusReply getPathReply = mMaildirSettingsInterface->path(); QCOMPARE(getPathReply.value(), maildirRootPath); AgentManager::self()->instance(mMaildirIdentifier).synchronize(); // // Find the root maildir collection // bool found = false; QElapsedTimer time; time.start(); while (!found) { CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive); QVERIFY(job->exec()); Collection::List collections = job->collections(); foreach (const Collection &col, collections) { if (col.resource() == AgentManager::self()->instance(mMaildirIdentifier).identifier() && col.remoteId() == maildirRootPath) { mMaildirCollection = col; found = true; break; } } QVERIFY(time.elapsed() < 10 * 1000); // maildir should not need more than 10 secs to sync } // // Start the fake POP3 server // mFakeServerThread = new FakeServerThread(this); mFakeServerThread->start(); QTest::qWait(100); QVERIFY(mFakeServerThread->server() != nullptr); // // Configure the pop3 resource // mPOP3SettingsInterface = new OrgKdeAkonadiPOP3SettingsInterface( Akonadi::ServerManager::agentServiceName(Akonadi::ServerManager::Resource, mPop3Identifier), QStringLiteral("/Settings"), QDBusConnection::sessionBus(), this); QDBusReply reply0 = mPOP3SettingsInterface->port(); QVERIFY(reply0.isValid()); QCOMPARE(reply0.value(), 110u); mPOP3SettingsInterface->setPort(5989).waitForFinished(); AgentManager::self()->instance(mPop3Identifier).reconfigure(); QDBusReply reply = mPOP3SettingsInterface->port(); QVERIFY(reply.isValid()); QCOMPARE(reply.value(), 5989u); mPOP3SettingsInterface->setHost(QStringLiteral("localhost")).waitForFinished(); AgentManager::self()->instance(mPop3Identifier).reconfigure(); QDBusReply reply2 = mPOP3SettingsInterface->host(); QVERIFY(reply2.isValid()); QCOMPARE(reply2.value(), QLatin1String("localhost")); mPOP3SettingsInterface->setLogin(QStringLiteral("HansWurst")).waitForFinished(); AgentManager::self()->instance(mPop3Identifier).reconfigure(); QDBusReply reply3 = mPOP3SettingsInterface->login(); QVERIFY(reply3.isValid()); QCOMPARE(reply3.value(), QLatin1String("HansWurst")); mPOP3SettingsInterface->setUnitTestPassword(QStringLiteral("Geheim")).waitForFinished(); AgentManager::self()->instance(mPop3Identifier).reconfigure(); QDBusReply reply4 = mPOP3SettingsInterface->unitTestPassword(); QVERIFY(reply4.isValid()); QCOMPARE(reply4.value(), QLatin1String("Geheim")); mPOP3SettingsInterface->setTargetCollection(mMaildirCollection.id()).waitForFinished(); AgentManager::self()->instance(mPop3Identifier).reconfigure(); QDBusReply reply5 = mPOP3SettingsInterface->targetCollection(); QVERIFY(reply5.isValid()); QCOMPARE(reply5.value(), mMaildirCollection.id()); } void Pop3Test::cleanupTestCase() { mFakeServerThread->quit(); if (!mFakeServerThread->wait(10000)) { qWarning() << "The fake server thread has not yet finished, what is wrong!?"; } } static const QByteArray simpleMail1 = "From: \"Bill Lumbergh\" \r\n" "To: \"Peter Gibbons\" \r\n" "Subject: TPS Reports - New Cover Sheets\r\n" "MIME-Version: 1.0\r\n" "Content-Type: text/plain\r\n" "Date: Mon, 23 Mar 2009 18:04:05 +0300\r\n" "\r\n" "Hi, Peter. What's happening? We need to talk about your TPS reports.\r\n"; static const QByteArray simpleMail2 = "From: \"Amy McCorkell\" \r\n" "To: gov.palin@yaho.com\r\n" "Subject: HI SARAH\r\n" "MIME-Version: 1.0\r\n" "Content-Type: text/plain\r\n" "Date: Mon, 23 Mar 2009 18:04:05 +0300\r\n" "\r\n" "Hey Sarah,\r\n" "bla bla bla bla bla\r\n"; static const QByteArray simpleMail3 = "From: chunkylover53@aol.com\r\n" "To: tylerdurden@paperstreetsoapcompany.com\r\n" "Subject: ILOVEYOU\r\n" "MIME-Version: 1.0\r\n" "Content-Type: text/plain\r\n" "Date: Mon, 23 Mar 2009 18:04:05 +0300\r\n" "\r\n" "kindly check the attached LOVELETTER coming from me.\r\n"; static const QByteArray simpleMail4 = "From: karl@aol.com\r\n" "To: lenny@aol.com\r\n" "Subject: Who took the donuts?\r\n" "\r\n" "Hi Lenny, do you know who took all the donuts?\r\n"; static const QByteArray simpleMail5 = "From: foo@bar.com\r\n" "To: bar@foo.com\r\n" "Subject: Hello\r\n" "\r\n" "Hello World!!\r\n"; void Pop3Test::cleanupMaildir(const Akonadi::Item::List &items) { // Delete all mails so the maildir is clean for the next test if (!items.isEmpty()) { ItemDeleteJob *job = new ItemDeleteJob(items); QVERIFY(job->exec()); } QElapsedTimer time; time.start(); int lastCount = -1; forever { qApp->processEvents(); QTest::qWait(500); QDir maildir(mMaildirPath); maildir.refresh(); int curCount = maildir.entryList(QDir::Files | QDir::NoDotAndDotDot).count(); // Restart the timer when a mail arrives, as it shows that the maildir resource is // still alive and kicking. if (curCount != lastCount) { time.restart(); lastCount = curCount; } if (curCount == 0) { break; } QVERIFY(time.elapsed() < 60000 || time.elapsed() > 80000000); } } void Pop3Test::checkMailsInMaildir(const QList &mails) { // Now, test that all mails actually ended up in the maildir. Since the maildir resource // might be slower, give it a timeout so it can write the files to disk QElapsedTimer time; time.start(); int lastCount = -1; forever { qApp->processEvents(); QTest::qWait(500); QDir maildir(mMaildirPath); maildir.refresh(); int curCount = static_cast(maildir.entryList(QDir::Files | QDir::NoDotAndDotDot).count()); // Restart the timer when a mail arrives, as it shows that the maildir resource is // still alive and kicking. if (curCount != lastCount) { time.start(); lastCount = curCount; } if (curCount == mails.count()) { break; } QVERIFY(static_cast(maildir.entryList(QDir::NoDotAndDotDot).count()) <= mails.count()); QVERIFY(time.elapsed() < 60000 || time.elapsed() > 80000000); } // TODO: check file contents as well or is this overkill? } Akonadi::Item::List Pop3Test::checkMailsOnAkonadiServer(const QList &mails) { // The fake server got disconnected, which means the pop3 resource has entered the QUIT // stage. That means all messages should be on the server now, so test that. ItemFetchJob *job = new ItemFetchJob(mMaildirCollection); job->fetchScope().fetchFullPayload(); Q_ASSERT(job->exec()); Item::List items = job->items(); Q_ASSERT(mails.size() == items.size()); QSet ourMailBodies; QSet itemMailBodies; foreach (const Item &item, items) { KMime::Message::Ptr itemMail = item.payload(); QByteArray itemMailBody = itemMail->body(); // For some reason, the body in the maildir has one additional newline. // Get rid of this so we can compare them. // FIXME: is this a bug? Find out where the newline comes from! itemMailBody.chop(1); itemMailBodies.insert(itemMailBody); } foreach (const QByteArray &mail, mails) { KMime::Message::Ptr ourMail(new KMime::Message()); ourMail->setContent(KMime::CRLFtoLF(mail)); ourMail->parse(); QByteArray ourMailBody = ourMail->body(); ourMailBodies.insert(ourMailBody); } Q_ASSERT(ourMailBodies == itemMailBodies); return items; } void Pop3Test::syncAndWaitForFinish() { AgentManager::self()->instance(mPop3Identifier).synchronize(); // The pop3 resource, ioslave and the fakeserver are all in different processes or threads. // We simply wait until the FakeServer got disconnected or until a timeout. // Since POP3 fetching can take longer, we reset the timeout timer when the FakeServer // does some processing. QElapsedTimer time; time.start(); int lastProgress = -1; forever { qApp->processEvents(); // Finish correctly when the connection got closed if (mFakeServerThread->server()->gotDisconnected()) { break; } // Reset the timeout when the server is working const int newProgress = mFakeServerThread->server()->progress(); if (newProgress != lastProgress) { time.restart(); lastProgress = newProgress; } // Assert when nothing happens for a certain timeout, that indicates something went // wrong and is stuck somewhere if (time.elapsed() >= 60000) { Q_ASSERT_X(false, "poptest", "FakeServer timed out."); break; } } // Once the messages are processed give the Akonadi server and the maildir resource some time to // process the item operations. Do this by running a monitor together with a timer. Each captured // item operation bumps the timer to wait longer. After 200ms of inactivity the state is considered // stable and the test case can proceed. Akonadi::Monitor mon(this); mon.setResourceMonitored(mPop3Identifier.toLatin1()); mon.setResourceMonitored(mMaildirIdentifier.toLatin1()); QEventLoop settleLoop; QTimer settleTimer; settleTimer.setSingleShot(true); connect(&mon, &Akonadi::Monitor::itemAdded, this, [&](const Akonadi::Item &, const Akonadi::Collection &) { settleTimer.start(serverSettleTimeout); }); connect(&mon, &Akonadi::Monitor::itemChanged, this, [&](const Akonadi::Item &, const QSet< QByteArray > &) { settleTimer.start(serverSettleTimeout); }); connect(&mon, &Akonadi::Monitor::itemRemoved, this, [&](const Akonadi::Item &) { settleTimer.start(serverSettleTimeout); }); settleTimer.start(serverSettleTimeout); connect(&settleTimer, &QTimer::timeout, this, [&]() { settleLoop.exit(0); }); settleLoop.exec(); } QString Pop3Test::loginSequence() const { return QStringLiteral("C: USER HansWurst\r\n" "S: +OK May I have your password, please?\r\n" "C: PASS Geheim\r\n" "S: +OK Mailbox locked and ready\r\n"); } QString Pop3Test::retrieveSequence(const QList &mails, const QList &exceptions) const { QString result; for (int i = 1; i <= mails.size(); i++) { if (!exceptions.contains(i)) { result += QLatin1String( "C: RETR %RETR%\r\n" "S: +OK Here is your spam\r\n" "%MAIL%\r\n" ".\r\n"); } } return result; } QString Pop3Test::deleteSequence(int numToDelete) const { QString result; for (int i = 0; i < numToDelete; i++) { result += QLatin1String("C: DELE %DELE%\r\n" "S: +OK message sent to /dev/null\r\n"); } return result; } QString Pop3Test::quitSequence() const { return QStringLiteral("C: QUIT\r\n" "S: +OK Have a nice day.\r\n"); } QString Pop3Test::listSequence(const QList &mails) const { QString result = QStringLiteral("C: LIST\r\n" "S: +OK You got new spam\r\n"); for (int i = 1; i <= mails.size(); i++) { result += QStringLiteral("%1 %MAILSIZE%\r\n").arg(i); } result += QLatin1String(".\r\n"); return result; } QString Pop3Test::uidSequence(const QStringList &uids) const { QString result = QStringLiteral("C: UIDL\r\n" "S: +OK\r\n"); for (int i = 1; i <= uids.size(); i++) { result += QStringLiteral("%1 %2\r\n").arg(i).arg(uids[i - 1]); } result += QLatin1String(".\r\n"); return result; } static bool sortedEqual(const QStringList &list1, const QStringList &list2) { QStringList sorted1 = list1; sorted1.sort(); QStringList sorted2 = list2; sorted2.sort(); return std::equal(sorted1.begin(), sorted1.end(), sorted2.begin()); } void Pop3Test::lowerTimeOfSeenMail(const QString &uidOfMail, int secondsToLower) { const int index = mPOP3SettingsInterface->seenUidList().value().indexOf(uidOfMail); QList seenTimeList = mPOP3SettingsInterface->seenUidTimeList().value(); int msgTime = seenTimeList.at(index); msgTime -= secondsToLower; seenTimeList.replace(index, msgTime); mPOP3SettingsInterface->setSeenUidTimeList(seenTimeList).waitForFinished(); } void Pop3Test::testSimpleDownload() { QList mails; mails << simpleMail1 << simpleMail2 << simpleMail3; QStringList uids; uids << QStringLiteral("UID1") << QStringLiteral("UID2") << QStringLiteral("UID3"); mFakeServerThread->server()->setAllowedDeletions(QStringLiteral("1,2,3")); mFakeServerThread->server()->setAllowedRetrieves(QStringLiteral("1,2,3")); mFakeServerThread->server()->setMails(mails); mFakeServerThread->server()->setNextConversation( loginSequence() +listSequence(mails) +uidSequence(uids) +retrieveSequence(mails) +deleteSequence(mails.size()) +quitSequence() ); syncAndWaitForFinish(); Akonadi::Item::List items = checkMailsOnAkonadiServer(mails); checkMailsInMaildir(mails); cleanupMaildir(items); mPOP3SettingsInterface->setSeenUidList(QStringList()).waitForFinished(); mPOP3SettingsInterface->setSeenUidTimeList(QList()).waitForFinished(); } void Pop3Test::testBigFetch() { QList mails; QStringList uids; QString allowedRetrs; for (int i = 0; i < 1000; i++) { QByteArray newMail = simpleMail1; newMail.append(QString::number(i + 1).toLatin1()); mails << newMail; uids << QStringLiteral("UID%1").arg(i + 1); allowedRetrs += QString::number(i + 1) + QLatin1Char(','); } allowedRetrs.chop(1); mFakeServerThread->server()->setMails(mails); mFakeServerThread->server()->setAllowedRetrieves(allowedRetrs); mFakeServerThread->server()->setAllowedDeletions(allowedRetrs); mFakeServerThread->server()->setNextConversation( loginSequence() +listSequence(mails) +uidSequence(uids) +retrieveSequence(mails) +deleteSequence(mails.size()) +quitSequence() ); syncAndWaitForFinish(); Akonadi::Item::List items = checkMailsOnAkonadiServer(mails); checkMailsInMaildir(mails); cleanupMaildir(items); mPOP3SettingsInterface->setSeenUidList(QStringList()).waitForFinished(); mPOP3SettingsInterface->setSeenUidTimeList(QList()).waitForFinished(); } void Pop3Test::testSeenUIDCleanup() { // // First, fetch 3 normal mails, but leave them on the server. // mPOP3SettingsInterface->setLeaveOnServer(true).waitForFinished(); QList mails; mails << simpleMail1 << simpleMail2 << simpleMail3; QStringList uids; uids << QStringLiteral("UID1") << QStringLiteral("UID2") << QStringLiteral("UID3"); mFakeServerThread->server()->setAllowedDeletions(QString()); mFakeServerThread->server()->setAllowedRetrieves(QStringLiteral("1,2,3")); mFakeServerThread->server()->setMails(mails); mFakeServerThread->server()->setNextConversation( loginSequence() +listSequence(mails) +uidSequence(uids) +retrieveSequence(mails) +quitSequence() ); syncAndWaitForFinish(); Akonadi::Item::List items = checkMailsOnAkonadiServer(mails); checkMailsInMaildir(mails); cleanupMaildir(items); QVERIFY(sortedEqual(uids, mPOP3SettingsInterface->seenUidList().value())); QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() == mPOP3SettingsInterface->seenUidList().value().size()); // // Now, pretend that the messages were removed from the server in the meantime // by having no mails on the fake server. // mFakeServerThread->server()->setMails(QList()); mFakeServerThread->server()->setAllowedRetrieves(QString()); mFakeServerThread->server()->setAllowedDeletions(QString()); mFakeServerThread->server()->setNextConversation( loginSequence() +listSequence(QList()) +uidSequence(QStringList()) +quitSequence() ); syncAndWaitForFinish(); items = checkMailsOnAkonadiServer(QList()); checkMailsInMaildir(QList()); cleanupMaildir(items); QVERIFY(mPOP3SettingsInterface->seenUidList().value().isEmpty()); QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() == mPOP3SettingsInterface->seenUidList().value().size()); mPOP3SettingsInterface->setLeaveOnServer(false).waitForFinished(); mPOP3SettingsInterface->setSeenUidList(QStringList()).waitForFinished(); mPOP3SettingsInterface->setSeenUidTimeList(QList()).waitForFinished(); } void Pop3Test::testSimpleLeaveOnServer() { mPOP3SettingsInterface->setLeaveOnServer(true).waitForFinished(); QList mails; mails << simpleMail1 << simpleMail2 << simpleMail3; QStringList uids; uids << QStringLiteral("UID1") << QStringLiteral("UID2") << QStringLiteral("UID3"); mFakeServerThread->server()->setMails(mails); mFakeServerThread->server()->setAllowedRetrieves(QStringLiteral("1,2,3")); mFakeServerThread->server()->setNextConversation( loginSequence() +listSequence(mails) +uidSequence(uids) +retrieveSequence(mails) +quitSequence() ); syncAndWaitForFinish(); Akonadi::Item::List items = checkMailsOnAkonadiServer(mails); checkMailsInMaildir(mails); // The resource should have saved the UIDs of the seen messages QVERIFY(sortedEqual(uids, mPOP3SettingsInterface->seenUidList().value())); QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() == mPOP3SettingsInterface->seenUidList().value().size()); foreach (int seenTime, mPOP3SettingsInterface->seenUidTimeList().value()) { // Those message were just downloaded from the fake server, so they are at maximum // 10 minutes old (for slooooow running tests) QVERIFY(seenTime >= time(nullptr) - 10 * 60); } // // OK, next mail check: We have to check that the old seen messages are not downloaded again, // only new mails. // QList newMails(mails); newMails << simpleMail4; QStringList newUids(uids); newUids << QStringLiteral("newUID"); QList idsToNotDownload; idsToNotDownload << 1 << 2 << 3; mFakeServerThread->server()->setMails(newMails); mFakeServerThread->server()->setAllowedRetrieves(QStringLiteral("4")); mFakeServerThread->server()->setNextConversation( loginSequence() +listSequence(newMails) +uidSequence(newUids) +retrieveSequence(newMails, idsToNotDownload) +quitSequence(), idsToNotDownload ); syncAndWaitForFinish(); items = checkMailsOnAkonadiServer(newMails); checkMailsInMaildir(newMails); QVERIFY(sortedEqual(newUids, mPOP3SettingsInterface->seenUidList().value())); QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() == mPOP3SettingsInterface->seenUidList().value().size()); // // Ok, next test: When turning off leaving on the server, all mails should be deleted, but // none downloaded. // mPOP3SettingsInterface->setLeaveOnServer(false).waitForFinished(); mFakeServerThread->server()->setAllowedDeletions(QStringLiteral("1,2,3,4")); mFakeServerThread->server()->setAllowedRetrieves(QString()); mFakeServerThread->server()->setNextConversation( loginSequence() +listSequence(newMails) +uidSequence(newUids) +deleteSequence(newMails.size()) +quitSequence() ); syncAndWaitForFinish(); items = checkMailsOnAkonadiServer(newMails); checkMailsInMaildir(newMails); cleanupMaildir(items); QVERIFY(mPOP3SettingsInterface->seenUidList().value().isEmpty()); QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() == mPOP3SettingsInterface->seenUidList().value().size()); mPOP3SettingsInterface->setSeenUidList(QStringList()).waitForFinished(); mPOP3SettingsInterface->setSeenUidTimeList(QList()).waitForFinished(); } void Pop3Test::testTimeBasedLeaveRule() { mPOP3SettingsInterface->setLeaveOnServer(true).waitForFinished(); mPOP3SettingsInterface->setLeaveOnServerDays(2).waitForFinished(); // // First download 3 mails and leave them on the server // QList mails; mails << simpleMail1 << simpleMail2 << simpleMail3; QStringList uids; uids << QStringLiteral("UID1") << QStringLiteral("UID2") << QStringLiteral("UID3"); mFakeServerThread->server()->setMails(mails); mFakeServerThread->server()->setAllowedRetrieves(QStringLiteral("1,2,3")); mFakeServerThread->server()->setNextConversation( loginSequence() +listSequence(mails) +uidSequence(uids) +retrieveSequence(mails) +quitSequence() ); syncAndWaitForFinish(); Akonadi::Item::List items = checkMailsOnAkonadiServer(mails); checkMailsInMaildir(mails); QVERIFY(sortedEqual(uids, mPOP3SettingsInterface->seenUidList().value())); QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() == mPOP3SettingsInterface->seenUidList().value().size()); // // Now, modify the seenUidTimeList on the server for UID2 to pretend it // was downloaded 3 days ago, which means it should be deleted. // lowerTimeOfSeenMail(QStringLiteral("UID2"), 60 * 60 * 24 * 3); QList idsToNotDownload; idsToNotDownload << 1 << 3; mFakeServerThread->server()->setAllowedDeletions(QStringLiteral("2")); mFakeServerThread->server()->setAllowedRetrieves(QString()); mFakeServerThread->server()->setNextConversation( loginSequence() +listSequence(mails) +uidSequence(uids) +deleteSequence(1) +quitSequence(), idsToNotDownload ); syncAndWaitForFinish(); items = checkMailsOnAkonadiServer(mails); checkMailsInMaildir(mails); cleanupMaildir(items); uids.removeAll(QStringLiteral("UID2")); QVERIFY(sortedEqual(uids, mPOP3SettingsInterface->seenUidList().value())); QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() == mPOP3SettingsInterface->seenUidList().value().size()); foreach (int seenTime, mPOP3SettingsInterface->seenUidTimeList().value()) { QVERIFY(seenTime >= time(nullptr) - 10 * 60); } mPOP3SettingsInterface->setLeaveOnServer(false).waitForFinished(); mPOP3SettingsInterface->setLeaveOnServerDays(0).waitForFinished(); mPOP3SettingsInterface->setSeenUidTimeList(QList()).waitForFinished(); mPOP3SettingsInterface->setSeenUidList(QStringList()).waitForFinished(); } void Pop3Test::testCountBasedLeaveRule() { mPOP3SettingsInterface->setLeaveOnServer(true).waitForFinished(); mPOP3SettingsInterface->setLeaveOnServerCount(3).waitForFinished(); // // First download 3 mails and leave them on the server // QList mails; mails << simpleMail1 << simpleMail2 << simpleMail3; QStringList uids; uids << QStringLiteral("UID1") << QStringLiteral("UID2") << QStringLiteral("UID3"); mFakeServerThread->server()->setMails(mails); mFakeServerThread->server()->setAllowedRetrieves(QStringLiteral("1,2,3")); mFakeServerThread->server()->setNextConversation( loginSequence() +listSequence(mails) +uidSequence(uids) +retrieveSequence(mails) +quitSequence() ); syncAndWaitForFinish(); checkMailsOnAkonadiServer(mails); checkMailsInMaildir(mails); // Make the 3 just downloaded mails appear older than they are lowerTimeOfSeenMail(QStringLiteral("UID1"), 60 * 60 * 24 * 2); lowerTimeOfSeenMail(QStringLiteral("UID2"), 60 * 60 * 24 * 1); lowerTimeOfSeenMail(QStringLiteral("UID3"), 60 * 60 * 24 * 3); // // Now, download 2 more mails. Since only 3 mails are allowed to be left // on the server, the oldest ones, UID1 and UID3, should be deleted // QList moreMails; moreMails << simpleMail4 << simpleMail5; QStringList moreUids; moreUids << QStringLiteral("UID4") << QStringLiteral("UID5"); mFakeServerThread->server()->setMails(mails + moreMails); mFakeServerThread->server()->setAllowedRetrieves(QStringLiteral("4,5")); mFakeServerThread->server()->setAllowedDeletions(QStringLiteral("1,3")); mFakeServerThread->server()->setNextConversation( loginSequence() +listSequence(mails + moreMails) +uidSequence(uids + moreUids) +retrieveSequence(moreMails) +deleteSequence(2) +quitSequence(), QList() << 1 << 2 << 3 ); syncAndWaitForFinish(); Akonadi::Item::List items = checkMailsOnAkonadiServer(mails + moreMails); checkMailsInMaildir(mails + moreMails); cleanupMaildir(items); QStringList uidsLeft; uidsLeft << QStringLiteral("UID2") << QStringLiteral("UID4") << QStringLiteral("UID5"); QVERIFY(sortedEqual(uidsLeft, mPOP3SettingsInterface->seenUidList().value())); QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() == mPOP3SettingsInterface->seenUidList().value().size()); mPOP3SettingsInterface->setLeaveOnServer(false).waitForFinished(); mPOP3SettingsInterface->setLeaveOnServerCount(0).waitForFinished(); mPOP3SettingsInterface->setSeenUidTimeList(QList()).waitForFinished(); mPOP3SettingsInterface->setSeenUidList(QStringList()).waitForFinished(); } void Pop3Test::testSizeBasedLeaveRule() { mPOP3SettingsInterface->setLeaveOnServer(true).waitForFinished(); mPOP3SettingsInterface->setLeaveOnServerSize(10).waitForFinished(); // 10 MB // // First download 3 mails and leave them on the server. // QList mails; mails << simpleMail1 << simpleMail2 << simpleMail3; QStringList uids; uids << QStringLiteral("UID1") << QStringLiteral("UID2") << QStringLiteral("UID3"); mFakeServerThread->server()->setMails(mails); mFakeServerThread->server()->setAllowedRetrieves(QStringLiteral("1,2,3")); mFakeServerThread->server()->setNextConversation( loginSequence() +listSequence(mails) +uidSequence(uids) +retrieveSequence(mails) +quitSequence() ); syncAndWaitForFinish(); checkMailsOnAkonadiServer(mails); checkMailsInMaildir(mails); // Make the 3 just downloaded mails appear older than they are lowerTimeOfSeenMail(QStringLiteral("UID1"), 60 * 60 * 24 * 2); lowerTimeOfSeenMail(QStringLiteral("UID2"), 60 * 60 * 24 * 1); lowerTimeOfSeenMail(QStringLiteral("UID3"), 60 * 60 * 24 * 3); // Now, do another mail check, but with no new mails on the server. // Instead we let the server pretend that the mails have a fake size, // each 7 MB. That means the two oldest get deleted, because the total // mail size is over 10 MB with them. mFakeServerThread->server()->setMails(mails); mFakeServerThread->server()->setAllowedRetrieves(QString()); mFakeServerThread->server()->setAllowedDeletions(QStringLiteral("1,3")); mFakeServerThread->server()->setNextConversation( loginSequence() +QLatin1String("C: LIST\r\n" "S: +OK You got new spam\r\n" "1 7340032\r\n" "2 7340032\r\n" "3 7340032\r\n" ".\r\n") +uidSequence(uids) +deleteSequence(2) +quitSequence() ); syncAndWaitForFinish(); Akonadi::Item::List items = checkMailsOnAkonadiServer(mails); checkMailsInMaildir(mails); cleanupMaildir(items); QStringList uidsLeft; uidsLeft << QStringLiteral("UID2"); QVERIFY(sortedEqual(uidsLeft, mPOP3SettingsInterface->seenUidList().value())); QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() == mPOP3SettingsInterface->seenUidList().value().size()); mPOP3SettingsInterface->setLeaveOnServer(false).waitForFinished(); mPOP3SettingsInterface->setLeaveOnServerCount(0).waitForFinished(); mPOP3SettingsInterface->setLeaveOnServerSize(0).waitForFinished(); mPOP3SettingsInterface->setSeenUidTimeList(QList()).waitForFinished(); mPOP3SettingsInterface->setSeenUidList(QStringList()).waitForFinished(); } void Pop3Test::testMixedLeaveRules() { mPOP3SettingsInterface->setLeaveOnServer(true).waitForFinished(); // // Generate 10 mails // QList mails; + mails.reserve(10); QStringList uids; + uids.reserve(10); QString allowedRetrs; for (int i = 0; i < 10; i++) { QByteArray newMail = simpleMail1; newMail.append(QString::number(i + 1).toLatin1()); mails << newMail; uids << QStringLiteral("UID%1").arg(i + 1); allowedRetrs += QString::number(i + 1) + QLatin1Char(','); } allowedRetrs.chop(1); // // Now, download these 10 mails // mFakeServerThread->server()->setMails(mails); mFakeServerThread->server()->setAllowedRetrieves(allowedRetrs); mFakeServerThread->server()->setNextConversation( loginSequence() +listSequence(mails) +uidSequence(uids) +retrieveSequence(mails) +quitSequence() ); syncAndWaitForFinish(); checkMailsOnAkonadiServer(mails); checkMailsInMaildir(mails); // Fake the time of the messages, UID1 is one day old, UID2 is two days old, etc for (int i = 1; i <= 10; i++) { lowerTimeOfSeenMail(QStringLiteral("UID%1").arg(i), 60 * 60 * 24 * i); } mPOP3SettingsInterface->setLeaveOnServer(true).waitForFinished(); mPOP3SettingsInterface->setLeaveOnServerSize(25).waitForFinished(); // UID 4, 5 oldest here mPOP3SettingsInterface->setLeaveOnServerCount(5).waitForFinished(); // UID 6, 7 oldest here mPOP3SettingsInterface->setLeaveOnServerDays(7).waitForFinished(); // UID 8, 9 and 10 too old // Ok, now we do another mail check that only deletes stuff from the server. // Above are the UIDs that should be deleted. mFakeServerThread->server()->setMails(mails); mFakeServerThread->server()->setAllowedRetrieves(QString()); mFakeServerThread->server()->setAllowedDeletions(QStringLiteral("4,5,6,7,8,9,10")); mFakeServerThread->server()->setNextConversation( loginSequence() +QLatin1String("C: LIST\r\n" "S: +OK You got new spam\r\n" "1 7340032\r\n" "2 7340032\r\n" "3 7340032\r\n" "4 7340032\r\n" "5 7340032\r\n" "6 7340032\r\n" "7 7340032\r\n" "8 7340032\r\n" "9 7340032\r\n" "10 7340032\r\n" ".\r\n") +uidSequence(uids) +deleteSequence(7) +quitSequence() ); syncAndWaitForFinish(); Akonadi::Item::List items = checkMailsOnAkonadiServer(mails); checkMailsInMaildir(mails); cleanupMaildir(items); QStringList uidsLeft; uidsLeft << QStringLiteral("UID1") << QStringLiteral("UID2") << QStringLiteral("UID3"); QVERIFY(sortedEqual(uidsLeft, mPOP3SettingsInterface->seenUidList().value())); QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() == mPOP3SettingsInterface->seenUidList().value().size()); mPOP3SettingsInterface->setLeaveOnServer(false).waitForFinished(); mPOP3SettingsInterface->setLeaveOnServerCount(0).waitForFinished(); mPOP3SettingsInterface->setLeaveOnServerSize(0).waitForFinished(); mPOP3SettingsInterface->setSeenUidTimeList(QList()).waitForFinished(); mPOP3SettingsInterface->setSeenUidList(QStringList()).waitForFinished(); }